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
Final ticket of the 3-way split that lights up end-to-end PDF generation for the first widget. Replaces the orchestrator's stub LOADING / BUILDING stages from #12536 with real registry-driven work, drives the side sheet's section list from the registry, and replaces the temporary "Download report" stub with real orchestrator invocation.
After this ticket lands (on top of #12537 for the registry contract, #12630 for the first registered widget, and #12655 for the core/pdf datastore):
Opening the export side sheet shows a Traffic checkbox - because the All Traffic widget declares a pdf configuration. Other sections do not appear.
Selecting Traffic and clicking Download generates a PDF with one section containing one All Visitors metric tile and a fixed-size empty placeholder where the line chart will land (chart fill-in is Create PDF Chart capture utilities #12629).
Cancellation, view-only filtering, and the existing error snackbar all behave correctly.
This ticket also migrates the sidesheet's selection storage from CORE_FORMS (FORM_PDF_DOWNLOAD_SELECTED_SECTIONS, introduced as a temporary home in #12507) to #12655's core/pdf datastore, and rewires the panel's Download-button disabled state to read core/pdf's status instead of the orchestrator's component-local stage. After this ticket, core/pdf is the single bridge between the sidesheet and the orchestrator: selection flows sidesheet → store → orchestrator, and status flows orchestrator → store → sidesheet (and snackbar). Neither side holds a direct reference to the other.
Per the design doc's Widget Rendering Architecture section: the orchestrator walks contexts → areas → widgets itself (no centralised PDF-aware selector) and produces a grouped areas array - not a flat widget list - that DashboardReport maps to <PDFSection> blocks.
Do not alter or remove anything below. The following sections will be managed by moderators only.
Selecting Traffic and clicking "Download report" produces a PDF with one section containing one "All Visitors" metric tile (total users + period-over-period change + comparison label) followed by a fixed-size empty placeholder block where the line chart will be filled in by follow-up Create PDF Chart capture utilities #12629.
The metric tile values match the dashboard's All Traffic widget for the same date range, with the date range adjusted to exclude the current day per the design doc.
Deselecting Traffic produces a PDF without the Traffic section and fires no GA4 report requests for it.
Section selection persists across closing and reopening the side sheet within the same dashboard session: ticking Traffic, closing the panel, then reopening it shows Traffic still ticked.
View-only users see Traffic only when Analytics 4 is shared with their role.
Feature gated behind the PDF export feature flag.
Implementation Brief
Files to modify
Frontend - PDF orchestrator & side sheet
Update file assets/js/components/pdf-export/PDFExportOrchestrator.js (created in Core pipeline: Export an MVP PDF #12536) - replace the stub LOADING / BUILDING stages with real registry-driven work. Per the design doc, the orchestrator walks contexts → areas → widgets and produces a grouped areas array (NOT a flat widget list):
Read selectedSections from core/pdf via select( CORE_PDF ).getSelectedContextSlugs(). Core pipeline: Export an MVP PDF #12536's orchestrator currently ignores any selection - this ticket introduces the registry lookup driven by store state. Section slugs are dashboard context slugs (mainDashboardTraffic, etc.). The earlier FORM_PDF_DOWNLOAD_SELECTED_SECTIONS storage from Create the PDF Generation menu item and sidesheet #12507 is migrated to core/pdf as part of the PanelContent update below.
Compute the PDF-adjusted dates once at the start of LOADING (date range from CORE_USER.getDateRangeDates(), then shift the end date back one day per the design doc's Reporting Period rule). Pass to every getData call.
Resolve modules as CORE_USER.getViewableModules() on a view-only dashboard, undefined otherwise.
Discovery (synchronous walk). For each contextSlug in selectedSections, call select( CORE_WIDGETS ).getWidgetAreas( contextSlug ). For each area, call select( CORE_WIDGETS ).getWidgets( areaSlug, { modules } ).filter( ( w ) => !! w.pdf ) to narrow to PDF-capable widgets. Drop areas that resolve to zero PDF widgets. Annotate each remaining widget with { contextSlug, areaSlug, areaTitle } for grouping.
LOADING. Iterate the discovered widgets sequentially and call widget.pdf.getData( { registry, dates, signal } ), collecting per-widget { data, chartImages }. Catch any non-AbortError thrown by a single widget into a per-widget error list and continue (the failing widget renders a "Data unavailable" placeholder later) per the design doc's failure-handling rule. If every widget fails, transition to ERROR.
BUILDING. Group the loaded entries by areaSlug and produce an ordered areas: Array<{ areaSlug, areaTitle, widgets: Array<{ slug, Component, data, chartImages, label }> }>. Outer area order follows getWidgetAreas priority; inner widget order follows getWidgets priority. Pass areas to <DashboardReport>.
Update file assets/js/components/pdf-export/shared-react-pdf-components/DashboardReport.js (created in Core pipeline: Export an MVP PDF #12536) - replace the placeholder body with the design doc's two-level mapper. Accepts an areas: Array<{ areaSlug, areaTitle, widgets: Array<{ slug, Component, data, chartImages, label }> }> prop and renders one <PDFSection id="section-${areaSlug}" title={areaTitle}> per area, with each widget's Component rendered inside (passing { data, chartImages }). The component knows nothing about which areas or widgets exist; it just iterates. Sub-section heading wrapping (PDFSubSection) is deferred to the first later ticket that adds a second PDF widget to a shared area - this ticket has only one widget per area, and the design doc explicitly suppresses sub-headings for single-widget areas.
Drop the hard-coded section list from constants.js. Resolve sections by iterating getContexts() for mainDashboard* contexts and, for each, walking getWidgetAreas( contextSlug ) → getWidgets( areaSlug, { modules } ).filter( ( w ) => !! w.pdf ). Any context that has at least one PDF-capable widget becomes a top-level checkbox (label from the context registration). Falls back to [] while resolving.
Migrate the section selection state from CORE_FORMS to core/pdf. The panel reads the current selection via select( CORE_PDF ).getSelectedContextSlugs() (defaulting to all available section slugs on first open) and writes via dispatch( CORE_PDF ).setSelection( { contextSlugs, widgetSlugs } ). The FORM_PDF_DOWNLOAD_SELECTED_SECTIONS form key is no longer used.
Pass the resolved [{ slug, label }] array and the current selectedContextSlugs as props to the (presentational) <PDFSectionCheckboxes />. PDFSectionCheckboxes.js itself does not change beyond accepting the new selection-source prop wiring.
Confirm the in-panel "Select at least 1 topic" notice (rendered inline when selectedSections.length === 0) automatically tracks the new source, since selectedSections now derives from select( CORE_PDF ).getSelectedContextSlugs(). The notice's render condition stays the same; only the underlying selector changes.
Update file assets/js/components/pdf-generation/PDFSectionsSelectionPanel/PDFGeneratingNotice.tsx (Create the PDF Generation menu item and sidesheet #12507) - replace the select( CORE_UI ).getValue( PDF_GENERATING_KEY ) read with select( CORE_PDF ).getStatus() === 'progress'. The notice continues to render only while an export is actively running (LOADING or BUILDING stage). This matches the design doc's intent: opening the side sheet while the orchestrator is busy shows the "Your report is being generated" warning so the user understands why the Download button is disabled. Drop the PDF_GENERATING_KEY import and the CORE_UI import (no longer used). Title and description copy stay unchanged.
Update file assets/js/components/pdf-generation/PDFSectionsSelectionPanel/Footer.js (Create the PDF Generation menu item and sidesheet #12507) - replace the temporary "Download report" callback (which flipped PDF_GENERATING_KEY) with real orchestrator invocation. Mount the orchestrator via the parent state flag pattern established in Core pipeline: Export an MVP PDF #12536 (the orchestrator dispatches setStatus( 'progress' ) to core/pdf on its own as it starts) and close the panel. The button's disabled state derives from select( CORE_PDF ).getStatus() === 'progress', not from the orchestrator's component-local stage (which is private to the orchestrator).
Update file assets/js/components/pdf-generation/constants.js (Create the PDF Generation menu item and sidesheet #12507) - remove PDF_GENERATING_KEY (now unused after PDFGeneratingNotice.tsx switches its source to core/pdf per the bullet above) and FORM_PDF_DOWNLOAD_SELECTED_SECTIONS (now superseded by core/pdf's selection.contextSlugs). The remaining FORM_PDF_DOWNLOAD form key (if any non-selection state was stored there) stays in place.
Test Coverage
Jest: assets/js/components/pdf-export/PDFExportOrchestrator.test.js (created in Core pipeline: Export an MVP PDF #12536) - extend to assert: it reads the selection from core/pdf via getSelectedContextSlugs (not from CORE_FORMS), walks getWidgetAreas → getWidgets( areaSlug, { modules } ).filter( ( w ) => !! w.pdf ) for each selected context, computes PDF-adjusted dates and threads them through every getData call, calls each widget's getData sequentially, produces the grouped areas shape passed to DashboardReport, captures per-widget non-AbortError exceptions into an error list and continues the loop, and transitions to ERROR only when every widget fails.
Jest: assets/js/components/pdf-export/shared-react-pdf-components/DashboardReport.test.js (created in Core pipeline: Export an MVP PDF #12536) - extend to assert it iterates areas, renders one <PDFSection> per area with the correct id and title, and renders each widget's Component with the right { data, chartImages } props.
Jest: assets/js/components/pdf-generation/PDFSectionsSelectionPanel/PanelContent.test.js (Create the PDF Generation menu item and sidesheet #12507) - sources sections from the registry walk; falls back to [] while resolving; respects view-only modules filtering; reads and writes selection via core/pdf (not CORE_FORMS); selection persists across mount/unmount cycles within the same session; passes the resolved sections and current selection as props to <PDFSectionCheckboxes />; the "Select at least 1 topic" notice appears when getSelectedContextSlugs() returns an empty array and disappears when at least one section is selected.
Jest: assets/js/components/pdf-generation/PDFSectionsSelectionPanel/Footer.test.js (Create the PDF Generation menu item and sidesheet #12507) - the Download button's disabled state tracks select( CORE_PDF ).getStatus() === 'progress'; clicking Download mounts the orchestrator via the parent state flag.
Jest: assets/js/components/pdf-generation/PDFSectionsSelectionPanel/PDFGeneratingNotice.test.tsx (extend or add) - the notice renders when select( CORE_PDF ).getStatus() === 'progress' and renders nothing when status is 'idle', 'success', or 'error'.
No Storybook / VRT - the orchestrator is a stateful pipeline component with no idle UI; the DashboardReport and section components render to a PDF document, not the DOM.
QA Brief
On the main dashboard with Analytics 4 connected and PDF export enabled, open the export side sheet from "Download report".
Confirm the section list is two-level: a "Traffic" parent with a "Site traffic over time" sub-item, all checked by default.
Tick/untick the "Traffic" parent and confirm its "Site traffic over time" sub-item follows, and unticking the sub-item also unchecks the parent.
Deselect everything and confirm "Download report" is disabled with a "Select at least 1 topic" notice; reselect and confirm it re-enables.
Change the selection, close and reopen the side sheet, and confirm the selection is retained.
Click "Download report" and confirm a generating/processing state appears, followed by a success state, and the PDF downloads.
Open the PDF and confirm a Traffic section showing "Your site traffic over time" with an "All visitors" value and a green change chip, plus a placeholder block where the chart will go.
Note: do not QA the PDF as pixel-perfect. Fonts and charts are added in follow-up tickets, and final pixel-perfect styling of this widget is owned by its completion ticket. Note: no progress bar appears yet because only one section is generated.
Changelog entry
Update the PDF orchestrator and side sheet to be driven from the widget registry and core/pdf store.
Feature Description
Final ticket of the 3-way split that lights up end-to-end PDF generation for the first widget. Replaces the orchestrator's stub LOADING / BUILDING stages from #12536 with real registry-driven work, drives the side sheet's section list from the registry, and replaces the temporary "Download report" stub with real orchestrator invocation.
After this ticket lands (on top of #12537 for the registry contract, #12630 for the first registered widget, and #12655 for the
core/pdfdatastore):pdfconfiguration. Other sections do not appear.This ticket also migrates the sidesheet's selection storage from
CORE_FORMS(FORM_PDF_DOWNLOAD_SELECTED_SECTIONS, introduced as a temporary home in #12507) to #12655'score/pdfdatastore, and rewires the panel's Download-button disabled state to readcore/pdf'sstatusinstead of the orchestrator's component-local stage. After this ticket,core/pdfis the single bridge between the sidesheet and the orchestrator: selection flows sidesheet → store → orchestrator, and status flows orchestrator → store → sidesheet (and snackbar). Neither side holds a direct reference to the other.Per the design doc's Widget Rendering Architecture section: the orchestrator walks
contexts → areas → widgetsitself (no centralised PDF-aware selector) and produces a groupedareasarray - not a flat widget list - thatDashboardReportmaps to<PDFSection>blocks.Do not alter or remove anything below. The following sections will be managed by moderators only.
Acceptance criteria
pdfconfiguration (registered in Register the first PDF widget via the registry: Traffic → All Visitors (excluding chart generation) #12630). Sections without PDF-capable widgets do not appear.Implementation Brief
Files to modify
Frontend - PDF orchestrator & side sheet
assets/js/components/pdf-export/PDFExportOrchestrator.js(created in Core pipeline: Export an MVP PDF #12536) - replace the stub LOADING / BUILDING stages with real registry-driven work. Per the design doc, the orchestrator walkscontexts → areas → widgetsand produces a groupedareasarray (NOT a flat widget list):selectedSectionsfromcore/pdfviaselect( CORE_PDF ).getSelectedContextSlugs(). Core pipeline: Export an MVP PDF #12536's orchestrator currently ignores any selection - this ticket introduces the registry lookup driven by store state. Section slugs are dashboard context slugs (mainDashboardTraffic, etc.). The earlierFORM_PDF_DOWNLOAD_SELECTED_SECTIONSstorage from Create the PDF Generation menu item and sidesheet #12507 is migrated tocore/pdfas part of the PanelContent update below.datesonce at the start of LOADING (date range fromCORE_USER.getDateRangeDates(), then shift the end date back one day per the design doc's Reporting Period rule). Pass to everygetDatacall.modulesasCORE_USER.getViewableModules()on a view-only dashboard,undefinedotherwise.contextSluginselectedSections, callselect( CORE_WIDGETS ).getWidgetAreas( contextSlug ). For each area, callselect( CORE_WIDGETS ).getWidgets( areaSlug, { modules } ).filter( ( w ) => !! w.pdf )to narrow to PDF-capable widgets. Drop areas that resolve to zero PDF widgets. Annotate each remaining widget with{ contextSlug, areaSlug, areaTitle }for grouping.widget.pdf.getData( { registry, dates, signal } ), collecting per-widget{ data, chartImages }. Catch any non-AbortErrorthrown by a single widget into a per-widget error list and continue (the failing widget renders a "Data unavailable" placeholder later) per the design doc's failure-handling rule. If every widget fails, transition to ERROR.areaSlugand produce an orderedareas: Array<{ areaSlug, areaTitle, widgets: Array<{ slug, Component, data, chartImages, label }> }>. Outer area order followsgetWidgetAreaspriority; inner widget order followsgetWidgetspriority. Passareasto<DashboardReport>.signalis the orchestrator's existing per-exportAbortController.signal(created on each export attempt by Core pipeline: Export an MVP PDF #12536 - no new wiring needed). Keep checkingsignal.abortedbetween everyawaitand swallowAbortErrorsilently per Core pipeline: Export an MVP PDF #12536's contract.assets/js/components/pdf-export/shared-react-pdf-components/DashboardReport.js(created in Core pipeline: Export an MVP PDF #12536) - replace the placeholder body with the design doc's two-level mapper. Accepts anareas: Array<{ areaSlug, areaTitle, widgets: Array<{ slug, Component, data, chartImages, label }> }>prop and renders one<PDFSection id="section-${areaSlug}" title={areaTitle}>per area, with each widget'sComponentrendered inside (passing{ data, chartImages }). The component knows nothing about which areas or widgets exist; it just iterates. Sub-section heading wrapping (PDFSubSection) is deferred to the first later ticket that adds a second PDF widget to a shared area - this ticket has only one widget per area, and the design doc explicitly suppresses sub-headings for single-widget areas.assets/js/components/pdf-generation/PDFSectionsSelectionPanel/PanelContent.js(Create the PDF Generation menu item and sidesheet #12507) - three changes:constants.js. Resolve sections by iteratinggetContexts()formainDashboard*contexts and, for each, walkinggetWidgetAreas( contextSlug )→getWidgets( areaSlug, { modules } ).filter( ( w ) => !! w.pdf ). Any context that has at least one PDF-capable widget becomes a top-level checkbox (label from the context registration). Falls back to[]while resolving.CORE_FORMStocore/pdf. The panel reads the current selection viaselect( CORE_PDF ).getSelectedContextSlugs()(defaulting to all available section slugs on first open) and writes viadispatch( CORE_PDF ).setSelection( { contextSlugs, widgetSlugs } ). TheFORM_PDF_DOWNLOAD_SELECTED_SECTIONSform key is no longer used.[{ slug, label }]array and the currentselectedContextSlugsas props to the (presentational)<PDFSectionCheckboxes />.PDFSectionCheckboxes.jsitself does not change beyond accepting the new selection-source prop wiring.selectedSections.length === 0) automatically tracks the new source, sinceselectedSectionsnow derives fromselect( CORE_PDF ).getSelectedContextSlugs(). The notice's render condition stays the same; only the underlying selector changes.assets/js/components/pdf-generation/PDFSectionsSelectionPanel/PDFGeneratingNotice.tsx(Create the PDF Generation menu item and sidesheet #12507) - replace theselect( CORE_UI ).getValue( PDF_GENERATING_KEY )read withselect( CORE_PDF ).getStatus() === 'progress'. The notice continues to render only while an export is actively running (LOADING or BUILDING stage). This matches the design doc's intent: opening the side sheet while the orchestrator is busy shows the "Your report is being generated" warning so the user understands why the Download button is disabled. Drop thePDF_GENERATING_KEYimport and theCORE_UIimport (no longer used). Title and description copy stay unchanged.assets/js/components/pdf-generation/PDFSectionsSelectionPanel/Footer.js(Create the PDF Generation menu item and sidesheet #12507) - replace the temporary "Download report" callback (which flippedPDF_GENERATING_KEY) with real orchestrator invocation. Mount the orchestrator via the parent state flag pattern established in Core pipeline: Export an MVP PDF #12536 (the orchestrator dispatchessetStatus( 'progress' )tocore/pdfon its own as it starts) and close the panel. The button's disabled state derives fromselect( CORE_PDF ).getStatus() === 'progress', not from the orchestrator's component-local stage (which is private to the orchestrator).assets/js/components/pdf-generation/constants.js(Create the PDF Generation menu item and sidesheet #12507) - removePDF_GENERATING_KEY(now unused afterPDFGeneratingNotice.tsxswitches its source tocore/pdfper the bullet above) andFORM_PDF_DOWNLOAD_SELECTED_SECTIONS(now superseded bycore/pdf'sselection.contextSlugs). The remainingFORM_PDF_DOWNLOADform key (if any non-selection state was stored there) stays in place.Test Coverage
assets/js/components/pdf-export/PDFExportOrchestrator.test.js(created in Core pipeline: Export an MVP PDF #12536) - extend to assert: it reads the selection fromcore/pdfviagetSelectedContextSlugs(not fromCORE_FORMS), walksgetWidgetAreas→getWidgets( areaSlug, { modules } ).filter( ( w ) => !! w.pdf )for each selected context, computes PDF-adjusteddatesand threads them through everygetDatacall, calls each widget'sgetDatasequentially, produces the groupedareasshape passed toDashboardReport, captures per-widget non-AbortErrorexceptions into an error list and continues the loop, and transitions to ERROR only when every widget fails.assets/js/components/pdf-export/shared-react-pdf-components/DashboardReport.test.js(created in Core pipeline: Export an MVP PDF #12536) - extend to assert it iteratesareas, renders one<PDFSection>per area with the correctidandtitle, and renders each widget'sComponentwith the right{ data, chartImages }props.assets/js/components/pdf-generation/PDFSectionsSelectionPanel/PanelContent.test.js(Create the PDF Generation menu item and sidesheet #12507) - sources sections from the registry walk; falls back to[]while resolving; respects view-onlymodulesfiltering; reads and writes selection viacore/pdf(notCORE_FORMS); selection persists across mount/unmount cycles within the same session; passes the resolved sections and current selection as props to<PDFSectionCheckboxes />; the "Select at least 1 topic" notice appears whengetSelectedContextSlugs()returns an empty array and disappears when at least one section is selected.assets/js/components/pdf-generation/PDFSectionsSelectionPanel/Footer.test.js(Create the PDF Generation menu item and sidesheet #12507) - the Download button's disabled state tracksselect( CORE_PDF ).getStatus() === 'progress'; clicking Download mounts the orchestrator via the parent state flag.assets/js/components/pdf-generation/PDFSectionsSelectionPanel/PDFGeneratingNotice.test.tsx(extend or add) - the notice renders whenselect( CORE_PDF ).getStatus() === 'progress'and renders nothing when status is'idle','success', or'error'.No Storybook / VRT - the orchestrator is a stateful pipeline component with no idle UI; the
DashboardReportand section components render to a PDF document, not the DOM.QA Brief
Note: do not QA the PDF as pixel-perfect. Fonts and charts are added in follow-up tickets, and final pixel-perfect styling of this widget is owned by its completion ticket.
Note: no progress bar appears yet because only one section is generated.
Changelog entry