Skip to content

Newsletter: scaffold the wp-build dashboard chassis behind the flag#48574

Merged
dhasilva merged 9 commits intotrunkfrom
try/newsletter-modernization-page-shell
May 6, 2026
Merged

Newsletter: scaffold the wp-build dashboard chassis behind the flag#48574
dhasilva merged 9 commits intotrunkfrom
try/newsletter-modernization-page-shell

Conversation

@keoshi
Copy link
Copy Markdown
Contributor

@keoshi keoshi commented May 6, 2026

Part of #48530 — second of six PRs to modernize Jetpack's Newsletter into a unified wp-admin product. Builds on #48532 (the empty wp-build scaffold + feature flag) by establishing the runtime chassis the next four PRs sit inside.

Proposed changes

  • Replace the placeholder <h1>Newsletter</h1> stage with Page chrome from @wordpress/admin-ui. Body is still empty — Subscribers DataView lands in PR 3, the Settings tab in PR 4.
  • Wrap the Stage in a @tanstack/react-query QueryClientProvider with a shared queryClient (30 s staleTime, no refetch-on-focus). Picking React Query here, not @wordpress/data core-data, matches the prototype (#48420) so PR 3 can drop the existing Subscribers data hooks in unchanged.
  • Lift analytics.initialize() from NewsletterSettingsApp into the route Stage, so future Tracks events (jetpack_newsletter_tab_view etc.) fire regardless of which tab a visitor lands on.
  • Add an empty Inspector slot at routes/dashboard/inspector.tsx so the route hook can target a stable surface. The subscriber detail panel lands inside it in PR 3.
  • Wire the route.inspector hook to a subscriber / u URL param (Boolean( search?.subscriber || search?.u )) — boot's router calls this on every navigation and uses the boolean to decide whether to render the inspector. Returns false at runtime today (no subscriber selection UI yet); PR 3 enables it.

All gated behind rsm_jetpack_ui_modernization_newsletter. With the flag off, the legacy Newsletter Settings page renders unchanged.

Related product discussion/links

Does this pull request change what data or activity we track or use?

No. The analytics initializer is moved, not changed; the only Tracks-related delta is where analytics.initialize runs — same call, same args. No new events fire.

Testing instructions

Pull this branch, run pnpm install and pnpm run build from projects/packages/newsletter/. Then verify both flag states:

Flag OFF (regression check)

  • Make sure no add_filter( 'rsm_jetpack_ui_modernization_newsletter', ... ) exists in any mu-plugin / wp-config.php / theme.
  • Visit wp-admin/admin.php?page=jetpack-newsletter.
  • Confirm today's Newsletter Settings page renders exactly as before — analytics.initialize still fires from NewsletterSettingsApp (untouched by this PR), <div id="newsletter-settings-root"> mount point is present.

Flag ON (new chassis)

  • Add the following to a mu-plugin, wp-config.php, or use the Code Snippet plugin:
    add_filter( 'rsm_jetpack_ui_modernization_newsletter', '__return_true' );
  • Hard-refresh wp-admin/admin.php?page=jetpack-newsletter.
  • Confirm a "Newsletter" page header renders inside the standard wp-admin chrome (sidebar visible). The body is still empty — that's expected; PR 3 fills it in.
  • DevTools → Network: wp-build's bundle (under build/scripts/ or build/pages/jetpack-newsletter-dashboard/) loads, not the legacy newsletter.js.
  • No console errors. analytics.initialize fires exactly once per page load (check Tracks in DevTools → Network or window._tkq to confirm a single record payload on init).

Toggle round-trip

  • Comment out the add_filter line and refresh — legacy Newsletter Settings page should return immediately.
  • Re-enable the filter and refresh — new chassis should return. Both directions reversible without cache clears.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.

  • To test on WoA, go to the Plugins menu on a WoA dev site. Click on the "Upload" button and follow the upgrade flow to be able to upload, install, and activate the Jetpack Beta plugin. Once the plugin is active, go to Jetpack > Jetpack Beta, select your plugin (Jetpack or WordPress.com Site Helper), and enable the try/newsletter-modernization-page-shell branch.
  • To test on Simple, run the following command on your sandbox:
bin/jetpack-downloader test jetpack try/newsletter-modernization-page-shell
bin/jetpack-downloader test jetpack-mu-wpcom-plugin try/newsletter-modernization-page-shell

Interested in more tips and information?

  • In your local development environment, use the jetpack rsync command to sync your changes to a WoA dev blog.
  • Read more about our development workflow here: PCYsg-eg0-p2
  • Figure out when your changes will be shipped to customers here: PCYsg-eg5-p2

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

Thank you for your PR!

When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:

  • ✅ Include a description of your PR changes.
  • ✅ Add a "[Status]" label (In Progress, Needs Review, ...).
  • ✅ Add testing instructions.
  • ✅ Specify whether this PR includes any changes to data or privacy.
  • ✅ Add changelog entries to affected projects

This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖


Follow this PR Review Process:

  1. Ensure all required checks appearing at the bottom of this PR are passing.
  2. Make sure to test your changes on all platforms that it applies to. You're responsible for the quality of the code you ship.
  3. You can use GitHub's Reviewers functionality to request a review.
  4. When it's reviewed and merged, you will be pinged in Slack to deploy the changes to WordPress.com simple once the build is done.

If you have questions about anything, reach out in #jetpack-developers for guidance!

@github-actions github-actions Bot added the [Status] Needs Author Reply We need more details from you. This label will be auto-added until the PR meets all requirements. label May 6, 2026
@keoshi keoshi added the [Status] Needs Review This PR is ready for review. label May 6, 2026
@jp-launch-control
Copy link
Copy Markdown

jp-launch-control Bot commented May 6, 2026

Code Coverage Summary

Coverage changed in 1 file.

File Coverage Δ% Δ Uncovered
projects/packages/wp-build-polyfills/src/class-wp-build-polyfills.php 73/82 (89.02%) -3.28% 3 ❤️‍🩹

Full summary · PHP report · JS report

@keoshi keoshi removed the [Status] Needs Author Reply We need more details from you. This label will be auto-added until the PR meets all requirements. label May 6, 2026
@keoshi keoshi requested review from CGastrell, dhasilva and ilonagl May 6, 2026 17:42
keoshi added a commit that referenced this pull request May 6, 2026
…ready fired

`WP_Build_Polyfills::register()` adds its registration callback to
`wp_default_scripts`, but that action only fires once — when
`wp_scripts()` is first instantiated. On admin requests where any
earlier-loading plugin touches `wp_scripts()` before `admin_menu`
priority 1 (where consumers like Newsletter call `register()`), the
hook arrives too late and the polyfills never register: dependent
script modules like `@wordpress/boot` and classic scripts like
`wp-theme` go missing, the boot loader's `<script type="module">` tag
is never printed, and the wp-build chassis fails to mount on a blank
page.

Detect that case via `did_action( 'wp_default_scripts' )` and run
`register_scripts()` + `register_modules()` synchronously on the live
`wp_scripts()` instance, so the polyfills land regardless of init
order.

Surfaced while testing Newsletter's modernization chassis (PR #48574)
on a Docker site where Jetpack JITM enqueues scripts during
`plugins_loaded`, instantiating `wp_scripts()` before `admin_menu`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
keoshi added a commit that referenced this pull request May 6, 2026
…ready fired

`WP_Build_Polyfills::register()` adds its registration callback to
`wp_default_scripts`, but that action only fires once — when
`wp_scripts()` is first instantiated. On admin requests where any
earlier-loading plugin touches `wp_scripts()` before `admin_menu`
priority 1 (where consumers like Newsletter call `register()`), the
hook arrives too late and the polyfills never register: dependent
script modules like `@wordpress/boot` and classic scripts like
`wp-theme` go missing, the boot loader's `<script type="module">` tag
is never printed, and the wp-build chassis fails to mount on a blank
page.

Detect that case via `did_action( 'wp_default_scripts' )` and run
`register_scripts()` + `register_modules()` synchronously on the live
`wp_scripts()` instance, so the polyfills land regardless of init
order.

Surfaced while testing Newsletter's modernization chassis (PR #48574)
on a Docker site where Jetpack JITM enqueues scripts during
`plugins_loaded`, instantiating `wp_scripts()` before `admin_menu`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@keoshi keoshi force-pushed the try/newsletter-modernization-page-shell branch from 432598e to 126ad0b Compare May 6, 2026 18:10
@keoshi keoshi self-assigned this May 6, 2026
keoshi and others added 9 commits May 6, 2026 20:14
…ready fired

`WP_Build_Polyfills::register()` adds its registration callback to
`wp_default_scripts`, but that action only fires once — when
`wp_scripts()` is first instantiated. On admin requests where any
earlier-loading plugin touches `wp_scripts()` before `admin_menu`
priority 1 (where consumers like Newsletter call `register()`), the
hook arrives too late and the polyfills never register: dependent
script modules like `@wordpress/boot` and classic scripts like
`wp-theme` go missing, the boot loader's `<script type="module">` tag
is never printed, and the wp-build chassis fails to mount on a blank
page.

Detect that case via `did_action( 'wp_default_scripts' )` and run
`register_scripts()` + `register_modules()` synchronously on the live
`wp_scripts()` instance, so the polyfills land regardless of init
order.

Surfaced while testing Newsletter's modernization chassis (PR #48574)
on a Docker site where Jetpack JITM enqueues scripts during
`plugins_loaded`, instantiating `wp_scripts()` before `admin_menu`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@dhasilva dhasilva force-pushed the try/newsletter-modernization-page-shell branch from 126ad0b to 1eb4e7c Compare May 6, 2026 23:14
Copy link
Copy Markdown
Contributor

@dhasilva dhasilva left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Rebased,

@dhasilva dhasilva merged commit 5a1be23 into trunk May 6, 2026
94 checks passed
@dhasilva dhasilva deleted the try/newsletter-modernization-page-shell branch May 6, 2026 23:39
@github-actions github-actions Bot removed the [Status] Needs Review This PR is ready for review. label May 6, 2026
dhasilva pushed a commit that referenced this pull request May 7, 2026
The page-shell scaffolded in #48574 now renders the Subscribers list:
the Stage wraps `<SubscribersBody>` (selection, modals, header
actions) inside the existing `<Page>` chrome and `QueryClientProvider`,
adds the page subtitle, and pulls in `route.scss` for the identity
cell, inspector chrome, and detail layout styles.

The route still has no tabs — Settings stays on its legacy surface
behind the modernization filter until a later PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dhasilva pushed a commit that referenced this pull request May 7, 2026
Replaces the empty inspector with `<SubscriberDetailContent>` reading
`?subscriber=…&u=…` from the URL — boot's router already gates the
mount on those params via `route.inspector` (added in #48574).

Wraps the slot in its own `QueryClientProvider` so subscriber-detail
queries share the cache the stage has already warmed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dhasilva added a commit that referenced this pull request May 7, 2026
…#48581)

* Newsletter + Forms: lift Gravatar into @automattic/jetpack-components

The Gravatar component (with hovercard support, SHA-256 hashing, and a
configurable defaultImage) moves into @automattic/jetpack-components
exposed at the `./gravatar` subpath. Forms switches to it; the local
copy in `projects/packages/forms/src/dashboard/components/gravatar/`
is removed and the `@gravatar-com/hovercards` and `js-sha256` deps move
with it.

Sets up Newsletter's Subscribers port to consume the same shared
component.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Newsletter: add REST proxy for the Subscribers admin page

Adds `WPCOM_REST_API_V2_Endpoint_Subscribers_List` registering
`/wpcom/v2/subscribers/{list,individual,totals,stats,add,remove,products,comp,remove-comp}`.
Each route proxies to the corresponding WP.com endpoint via
`Automattic\Jetpack\Connection\Client::wpcom_json_api_request_as_user`.

The wp-admin Newsletter page (next commit) consumes these endpoints to
render the Subscribers DataViews table and per-subscriber actions on
Jetpack-connected self-hosted sites. Glob-loaded by
`load-wpcom-endpoints.php`, so no loader changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Newsletter: add the Subscribers data layer + library helpers

Ports the non-rendering plumbing from #48420's Phase 2 into
`_inc/subscribers/`:

- `data/api.ts` + `data/types.ts` — REST client + DataViews/API types.
- React Query hooks for list, individual, totals, stats, add, remove,
  comp / remove-comp, and memberships products.
- `lib/` helpers: CSV parsing, DataViews i18n, site context, subscriber
  helpers, subscription plans + status, Tracks events, and the
  DataViews `View` <-> URL state hook.

No UI yet. The list, cells, modals, and detail pane (next commit)
consume these hooks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Newsletter: add the Subscribers DataViews UI

Drops in the rendering layer ported from #48420:

- `subscribers-data-views.tsx` — the table/list with fields, filters,
  pagination, row actions, and bulk actions.
- `subscribers-body.tsx` — selection, modal orchestration, and the
  `<SubscribersDataViews>` mount.
- `cells/{subscriber-identity,subscription-status-cell,subscription-type-cell}`
  — column renderers (identity uses the shared `Gravatar`).
- `detail/subscriber-detail-content.tsx` — sliding inspector content
  with joined date, status, and email engagement stats.
- `modals/{add-subscribers,comp,remove-comp,unsubscribe}-modal` — row
  + bulk action dialogs.
- `header-actions.tsx`, `empty-state.tsx`, `jetpack-logo.tsx` — page
  chrome accents.

Not yet mounted; the next commit wires `<SubscribersBody />` into the
route Stage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Newsletter: mount the Subscribers DataViews in the dashboard Stage

The page-shell scaffolded in #48574 now renders the Subscribers list:
the Stage wraps `<SubscribersBody>` (selection, modals, header
actions) inside the existing `<Page>` chrome and `QueryClientProvider`,
adds the page subtitle, and pulls in `route.scss` for the identity
cell, inspector chrome, and detail layout styles.

The route still has no tabs — Settings stays on its legacy surface
behind the modernization filter until a later PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Newsletter: render the subscriber detail in the inspector slot

Replaces the empty inspector with `<SubscriberDetailContent>` reading
`?subscriber=…&u=…` from the URL — boot's router already gates the
mount on those params via `route.inspector` (added in #48574).

Wraps the slot in its own `QueryClientProvider` so subscriber-detail
queries share the cache the stage has already warmed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Newsletter: changelog for the Subscribers DataViews port

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Newsletter Subscribers REST: gate routes behind the modernization filter + add tests

Until the dashboard UI ships, register no /wpcom/v2/subscribers/* routes unless
`rsm_jetpack_ui_modernization_newsletter` is on — same gate the wp-build page
already uses. The check lives inside `register_routes()` (which fires on
`rest_api_init`) so theme- or plugin-added filters are applied before it
evaluates, instead of at file-load time when only mu-plugins have run.

Adds a focused test class covering the new gate, the cap check (anon and
under-privileged users), and the handler-level input validation paths in
`add` / `remove` / `individual` / `stats` — the layers most likely to silently
regress without coverage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Newsletter Subscribers: harden CSV download link + cover remove-mutation

Pass `noopener,noreferrer` to `window.open` for the page-header CSV download —
matches the pattern used everywhere else we open a new tab.

Adds Jest coverage for `useSubscriberRemoveMutation`: singular vs bulk
snackbar copy, the partial-failure path that emits both success and error
notices, and the MAX_BULK_REMOVE cap that protects WP.com from a runaway
selection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix changelog

* Jest configs: allow @gravatar-com CSS through transformIgnorePatterns

Lifting `Gravatar` into `@automattic/jetpack-components` made every package
whose tests touch that components index transitively evaluate
`gravatar/index.tsx`, which side-effect imports
`@gravatar-com/hovercards/dist/style.css`. Jest's default behavior is to skip
`node_modules` for transformation, so the CSS reached the JS parser and blew
up with `SyntaxError: Unexpected token '.'` across publicize, social, and
the plugins/jetpack admin-page tests.

Two configs maintain their own allowlists: the shared base config in
`tools/js-tools/jest/config.base.js`, and the plugins/jetpack gui runner at
`projects/plugins/jetpack/tests/jest.config.gui.js`. The gui runner's local
pattern overrides the base via `transformIgnorePatterns` set semantics
(union — any matching pattern wins, so a local "ignore" beats a base
"unignore"), so both need the same allowlist entry.

Adds `@gravatar-com` to both alongside `uplot` and `@wordpress/admin-ui`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Douglas <douglas.henri@automattic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants