Skip to content

Core pipeline: Export an MVP PDF #12536

@benbowler

Description

@benbowler

Feature Description

Using the vertical slice approach for this epic, this first core PDF generation ticket should wire the end-to-end pipeline from button click in the implemented sidesheet through the new PDF Orchestrator, progressing through each stage of the state machine to generate a "blank" or "Hello World" PDF and triggering download.

This ticket builds on #12655's core/pdf datastore. Orchestration state (stage, AbortController, per-stage timeout, errorMessage) stays component-local; the orchestrator dispatches setStatus / setProgress / setBlob / clearCancelRequest to core/pdf on each transition so the export snackbar (rendered separately by #12508/9/10) can read getStatus / getProgress / getBlob and respond independently. Cancellation reaches the orchestrator via core/pdf's isCancelRequested() rather than direct callback wiring.


Do not alter or remove anything below. The following sections will be managed by moderators only.

Acceptance Criteria

  • Clicking "Download report" in the side sheet closes the panel and starts the orchestrator.
  • The orchestrator progresses through LOADING → BUILDING → COMPLETE automatically.
  • A progress snackbar is visible during LOADING and BUILDING, showing mock or no progress data
  • A PDF file downloads automatically with a sanitized filename (e.g. site-kit-example-com-2026-04-17.pdf).
  • The downloaded PDF opens in any PDF viewer and contains at minimum a header with the site name and a body with placeholder text (e.g. "Site Kit Dashboard Report").
  • On COMPLETE, a success snackbar confirms the download was triggered.
  • If the user clicks Cancel during generation, the orchestrator aborts and no file downloads.
  • If the orchestrator encounters an error at any stage, an error snackbar is shown and no file downloads.
  • The feature remains gated behind the dashboardPdfExport feature flag.

Implementation

Frontend (JavaScript)

  • Create file assets/js/components/PDFExport/PDFExportOrchestrator.js - the state machine component. Logic-only: it does not render progress, success, or error UI itself. Accepts an onComplete callback so the parent can unmount it once the export ends.

    • Uses useReducer with five internal stages: IDLE, LOADING, BUILDING, COMPLETE, ERROR. Validates transitions via a VALID_TRANSITIONS map; the reducer silently ignores invalid transitions. The internal stage stays component-local per the design doc; the store's status is the public projection.
    • On mount, immediately transitions to LOADING and dispatches setStatus( 'progress' ) to core/pdf so the export snackbar shows.
    • LOADING stage: For this ticket no real data is fetched. The stage simulates loading by awaiting a resolved promise (or a short requestAnimationFrame yield) and dispatches a mock setProgress value to drive the snackbar's progress bar before transitioning to BUILDING.
    • BUILDING stage: Imports pdf from @react-pdf/renderer and DashboardReport (static imports). Calls pdf( <DashboardReport ... /> ).toBlob() to produce the blob. Generates the filename via getPDFFilename. Creates a blob URL via URL.createObjectURL and dispatches setBlob( { url, filename } ) to core/pdf. Triggers the browser download via a programmatic <a> element with download attribute and .click(). Defers URL.revokeObjectURL with setTimeout( ..., 30000 ) to avoid a race with the browser's download pipeline.
    • COMPLETE stage: Dispatches setStatus( 'success' ). Calls onComplete after a brief delay (2 s) so the parent can unmount.
    • ERROR stage: Catches any exception in the async pipeline, transitions to ERROR internally, dispatches setStatus( 'error' ). The error snackbar (Create the Error snackbar component #12510) handles user-facing rendering.
    • Creates a fresh AbortController per export attempt. Checks signal.aborted between every await. Catches AbortError silently (expected flow, not a user-facing error).
    • Subscribes to select( CORE_PDF ).isCancelRequested() via useEffect. When the flag flips to true, calls abortControllerRef.current.abort() and dispatches clearCancelRequest() so the flag does not retrigger on the next attempt. The snackbar's Cancel button (Create the Progress snackbar component #12508) is the upstream caller of requestCancel(); the orchestrator never receives the cancel as a prop.
    • Per-stage timeouts: LOADING 45 s, BUILDING 15 s. On timeout, calls abort() and transitions to ERROR (which dispatches setStatus( 'error' ) per above). Timeouts are cleared on stage transition and on unmount.
    • Registers beforeunload listener during LOADING and BUILDING; removes on completion/error/unmount.
  • Create file assets/js/components/PDFExport/components/DashboardReport.js - Minimal top-level PDF <Document> component using @react-pdf/renderer primitives.

    • Accepts siteName, dateRange, userName, generatedAt, and pageHeight props.
    • Renders <Document><Page> with a header (<Text> showing site name and date range), a body (<Text> showing "Site Kit Dashboard Report"), and a footer (<Text> showing generation timestamp and user name).
    • Uses a single page with size={[ 612, pageHeight ]} and wrap={false}.
    • No sections, no TOC, no shared primitives - those come in later tickets.
  • Create file assets/js/components/PDFExport/pdf-utils.js - Exports getPDFFilename( siteName, dateRange ). Sanitizes the site name (strips protocol, replaces non-alphanumeric characters with hyphens) and returns a string like site-kit-example-com-last-28-days-2026-04-17.pdf.

  • Update file per Create the PDF Generation menu item and sidesheet #12507's side sheet component (e.g. PDFDownloadPanel.js or equivalent) - wire the "Download report" button's onClick to:

    1. Close the side sheet panel.
    2. Mount <PDFExportOrchestrator> by setting a parent state flag (e.g. isExporting = true). The export snackbar (Create the Progress snackbar component #12508/9/10), mounted separately at the dashboard root, picks up the orchestrator's status from core/pdf and renders independently.

Test Coverage

  • No test coverage is required here as the orchestrator lacks it's full testible structure.

QA Brief

  • Run npm install && npm run dev and reload the dashboard.
  • Enable pdfGeneration in the tester plugin (Override always, check pdfGeneration, Save).
  • Open the dashboard. Confirm the download icon is in the header.
  • Click the download icon. Confirm the side sheet opens with all six sections checked.
  • Click Download report. Verify the panel closes, progress snackbar appears, and a PDF downloads automatically.
  • Open the PDF. Confirm site name in header, "Site Kit Dashboard Report" in body, timestamp and user name in footer.
  • Verify filename matches site-kit-<sanitized-site>-<date-range>-YYYY-MM-DD.pdf. Note: some Chromium-based browsers (Arc, Brave) may ignore the download attribute on blob URLs and generate their own filename. Test in Chrome for reliable verification.
  • Confirm the success snackbar appears after the download completes. Wait 10 seconds and verify it disappears on its own. Also close it early with the X icon to confirm manual dismiss works.
  • QA:Eng: Test cancellation. In Console, freeze the export so you have time to cancel:
    1. const origRAF = window.requestAnimationFrame; window.requestAnimationFrame = () => 0;
    2. Close the side sheet if open.
    3. googlesitekit.data.dispatch('core/pdf').startExporting()
    4. The progress snackbar appears. Click Cancel.
    5. googlesitekit.data.select('core/pdf').getStatus() should return 'idle'.
    6. window.requestAnimationFrame = origRAF; to restore. Reload before the next test.
  • QA:Eng: Test error path. In Console, run URL.createObjectURL = () => { throw new Error('test'); } then click Download report. Confirm the error snackbar appears with Retry and Get help. Reload to restore.
  • Toggle pdfGeneration off, reload. Confirm download icon and side sheet are gone.
  • Uncheck all sections. Confirm "Select at least 1 topic" notice and disabled button.

Changelog entry

  • Implement an MVP that exports a basic PDF document.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Medium priorityQA: EngRequires specialized QA by an engineerTeam SIssues for Squad 1Type: EnhancementImprovement of an existing feature

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions