You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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.
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:
googlesitekit.data.select('core/pdf').getStatus() should return 'idle'.
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.
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/pdfdatastore. Orchestration state (stage,AbortController, per-stage timeout,errorMessage) stays component-local; the orchestrator dispatchessetStatus/setProgress/setBlob/clearCancelRequesttocore/pdfon each transition so the export snackbar (rendered separately by #12508/9/10) can readgetStatus/getProgress/getBloband respond independently. Cancellation reaches the orchestrator viacore/pdf'sisCancelRequested()rather than direct callback wiring.Do not alter or remove anything below. The following sections will be managed by moderators only.
Acceptance Criteria
site-kit-example-com-2026-04-17.pdf).dashboardPdfExportfeature 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 anonCompletecallback so the parent can unmount it once the export ends.useReducerwith five internal stages:IDLE,LOADING,BUILDING,COMPLETE,ERROR. Validates transitions via aVALID_TRANSITIONSmap; the reducer silently ignores invalid transitions. The internal stage stays component-local per the design doc; the store'sstatusis the public projection.LOADINGand dispatchessetStatus( 'progress' )tocore/pdfso the export snackbar shows.requestAnimationFrameyield) and dispatches a mocksetProgressvalue to drive the snackbar's progress bar before transitioning toBUILDING.pdffrom@react-pdf/rendererandDashboardReport(static imports). Callspdf( <DashboardReport ... /> ).toBlob()to produce the blob. Generates the filename viagetPDFFilename. Creates a blob URL viaURL.createObjectURLand dispatchessetBlob( { url, filename } )tocore/pdf. Triggers the browser download via a programmatic<a>element withdownloadattribute and.click(). DefersURL.revokeObjectURLwithsetTimeout( ..., 30000 )to avoid a race with the browser's download pipeline.setStatus( 'success' ). CallsonCompleteafter a brief delay (2 s) so the parent can unmount.setStatus( 'error' ). The error snackbar (Create the Error snackbar component #12510) handles user-facing rendering.AbortControllerper export attempt. Checkssignal.abortedbetween everyawait. CatchesAbortErrorsilently (expected flow, not a user-facing error).select( CORE_PDF ).isCancelRequested()viauseEffect. When the flag flips totrue, callsabortControllerRef.current.abort()and dispatchesclearCancelRequest()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 ofrequestCancel(); the orchestrator never receives the cancel as a prop.LOADING45 s,BUILDING15 s. On timeout, callsabort()and transitions to ERROR (which dispatchessetStatus( 'error' )per above). Timeouts are cleared on stage transition and on unmount.beforeunloadlistener 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/rendererprimitives.siteName,dateRange,userName,generatedAt, andpageHeightprops.<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).size={[ 612, pageHeight ]}andwrap={false}.Create file
assets/js/components/PDFExport/pdf-utils.js- ExportsgetPDFFilename( siteName, dateRange ). Sanitizes the site name (strips protocol, replaces non-alphanumeric characters with hyphens) and returns a string likesite-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.jsor equivalent) - wire the "Download report" button'sonClickto:<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 fromcore/pdfand renders independently.getSelection()fromcore/pdfand receives no section list. Selection-driven loading lands in Drive the PDF orchestrator and side sheet from the widget registry andcore/pdfstore #12631.Test Coverage
QA Brief
npm install && npm run devand reload the dashboard.pdfGenerationin the tester plugin (Override always, checkpdfGeneration, Save).site-kit-<sanitized-site>-<date-range>-YYYY-MM-DD.pdf. Note: some Chromium-based browsers (Arc, Brave) may ignore thedownloadattribute on blob URLs and generate their own filename. Test in Chrome for reliable verification.const origRAF = window.requestAnimationFrame; window.requestAnimationFrame = () => 0;googlesitekit.data.dispatch('core/pdf').startExporting()googlesitekit.data.select('core/pdf').getStatus()should return'idle'.window.requestAnimationFrame = origRAF;to restore. Reload before the next test.URL.createObjectURL = () => { throw new Error('test'); }then click Download report. Confirm the error snackbar appears with Retry and Get help. Reload to restore.pdfGenerationoff, reload. Confirm download icon and side sheet are gone.Changelog entry