Skip to content

Drive the PDF orchestrator and side sheet from the widget registry and core/pdf store #12631

Description

@benbowler

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/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.

Acceptance criteria

  • The side sheet's section list is driven by the widget registry - a "Traffic" entry appears because the All Traffic widget declares a pdf configuration (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.
  • 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>.
    • signal is the orchestrator's existing per-export AbortController.signal (created on each export attempt by Core pipeline: Export an MVP PDF #12536 - no new wiring needed). Keep checking signal.aborted between every await and swallow AbortError silently per Core pipeline: Export an MVP PDF #12536's contract.
  • 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.
  • Update file assets/js/components/pdf-generation/PDFSectionsSelectionPanel/PanelContent.js (Create the PDF Generation menu item and sidesheet #12507) - three changes:
    • 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 getWidgetAreasgetWidgets( 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Medium priorityTeam 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