feat(emails): unified email registry + DataViews list#4727
feat(emails): unified email registry + DataViews list#4727kmwilkerson wants to merge 21 commits into
Conversation
The joshtronic/php-loremipsum package source moved from GitHub to git.sherver.org. Same version (2.1.0), same commit hash. The dist and support blocks referencing old GitHub URLs were dropped. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…e 1) First slice of the unified email management UI (NPPD-945). This PR: - Adds `Emails_Section::get_email_registry()` with 23 curated entries covering Newspack and WooCommerce transactional emails, with metadata for default-view filtering and plugin-conditional surfacing. - Extends `api_get_email_settings()` to return enriched `newspack_emails` joined against the registry. - Replaces the existing WizardsActionCard list with a DataViews list following the institutions view pattern. Adds a "Show all emails" link to reveal lower-priority entries. - Preserves the existing Reset action (receipt/welcome only) and inactive- email notification copy. - Does not yet merge WooCommerce email surfacing into the unified view — that's a follow-up. The Woo block editor toggle remains a separate section. Known follow-ups (out of scope for this PR): - Re-add `login-otp-oauth` registry entry once the OAuth OTP email type is registered in `Reader_Activation_Emails::EMAIL_TYPES`. - PRD rationale for `woo-processing-order`, `woo-completed-order`, and `woo-on-hold-order` should be updated; these are customer-facing, not admin-facing. See TODO comments inline. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Drop tabs and show-all toggle; list all 24 emails in single view sorted by category - Add recipient column (Reader/Admin) - Move email actions (Edit/Activate/Deactivate/Reset) into kebab menu on every row - Add secondary status filter (Enabled/Disabled) - Restore inactive-email notification copy via Note column - Add Edit template button and page subtitle - Use trigger_description from registry as sub-text under email title - Various polish: button variants, color overrides, fixed stale-closure bugs
There was a problem hiding this comment.
Pull request overview
This PR introduces the first slice of a unified email management experience for Newspack Settings by replacing the existing card-based email list with a DataViews-powered list backed by a curated PHP email registry.
Changes:
- Adds a curated email registry and enriches the Emails settings REST response with registry metadata.
- Replaces the Emails settings UI with a searchable/filterable DataViews grid/table.
- Adds PHPUnit and Jest coverage for registry basics and list rendering.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
includes/wizards/newspack/class-emails-section.php |
Adds the registry, always registers the GET route, and enriches/sorts Newspack email response data. |
src/wizards/newspack/views/settings/emails/index.tsx |
Updates the Emails tab wrapper for the new full-width DataViews layout. |
src/wizards/newspack/views/settings/emails/emails.tsx |
Replaces action cards with DataViews, fetches email data, and wires edit/status/reset actions. |
src/wizards/newspack/views/settings/emails/emails.scss |
Adds styling for the DataViews layout, preview placeholder, descriptions, and status indicators. |
src/wizards/newspack/views/settings/emails/emails.test.js |
Adds Jest tests for basic DataViews rendering and visible row metadata. |
tests/unit-tests/emails-section.php |
Adds PHPUnit tests for registry counts and required registry fields. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Address Copilot review comment on PR #4727. The previous usort comparator returned 0 for items in the same category, but PHP's usort is not stable, so within-category ordering was undefined despite the comment claiming registry order was preserved. Add a slug-to-index map from the registry and use it as a secondary sort key. Unregistered emails sort to the end via PHP_INT_MAX fallback. No API schema change — sort is internal to the comparator.
Address Copilot review comment on PR #4727. Dropping the WizardsTab title prop in slice 1 removed the page h1, which screen readers rely on as a landmark. Adds a visually-hidden h1 directly in the emails view using the WordPress core .screen-reader-text utility class. Visual layout unchanged. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address Copilot review comment on PR #4727. Existing tests cover the registry structure but not the api_get_email_settings() enrichment logic that the frontend depends on. Adds a test asserting the response includes the expected top-level keys and that each Newspack email is enriched with recommended, view_category, trigger_description, registry_slug, and recipient fields. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address Copilot review comments on PR #4727. Existing tests render the DataViews grid but don't exercise the activate, deactivate, or reset action callbacks. Adds happy-path coverage that asserts apiFetch is called with the expected path and method for each action. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix Prettier formatting error from the previous a11y h1 commit. Short JSX content should be inline, not on its own line. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
thomasguillot
left a comment
There was a problem hiding this comment.
Status column: please use newspack's <Badge> instead of the custom dot + text.
The current rendering (src/wizards/newspack/views/settings/emails/emails.tsx Status column + .newspack-emails__status-dot--enabled/--disabled rules in emails.scss) reinvents a pattern Newspack already has, and we have a direct precedent on the same surface type.
Why
- Exact precedent in another DataView status column.
src/wizards/audience/views/integrations/logs-view.jsdefines aSTATUS_MAPand renders<Badge text={ mapped.label } level={ mapped.level } />in the column. Same shape as what this PR needs. - Used for enabled/disabled state elsewhere too.
src/wizards/audience/views/content-gates/content-gate-settings.tsxdoes<Badge level={ getGateStatusBadgeLevel( gate.status ) } text={ getGateStatus( gate.status ) } />. Same binary on/off domain. - Design tokens vs hardcoded colors. The dot styles hardcode
#1a7e5aand#949494.<Badge>goes throughcolors.$primary-600/wp-colors.$alert-green/$gray-100viacolor-mix, so it stays in step with theme tweaks. - Cross-surface consistency. A reader moving between Audience > Integrations, Audience > Content Gates, and Settings > Emails should see the same visual for "status," not three variations.
Concrete change
src/wizards/newspack/views/settings/emails/emails.tsx — the status field render:
import { Badge } from 'newspack-components';
// ...
{
id: 'status',
label: __( 'Status', 'newspack-plugin' ),
getValue: ( { item }: { item: EmailItem } ) => item.status,
render: ( { item }: { item: EmailItem } ) => {
const isEnabled = item.status === 'publish';
return (
<Badge
level={ isEnabled ? 'success' : 'default' }
text={
isEnabled
? __( 'Enabled', 'newspack-plugin' )
: __( 'Disabled', 'newspack-plugin' )
}
/>
);
},
},src/wizards/newspack/views/settings/emails/emails.scss — remove the now-unused rules:
-// Status indicator dot + text.
-.newspack-emails__status {
- display: inline-flex;
- align-items: center;
- gap: 6px;
-}
-
-.newspack-emails__status-dot {
- display: inline-block;
- width: 8px;
- height: 8px;
- border-radius: 50%;
- flex-shrink: 0;
-
- &--enabled {
- background: #1a7e5a;
- }
-
- &--disabled {
- background: transparent;
- border: 1.5px solid #949494;
- }
-}Tests
Update src/wizards/newspack/views/settings/emails/emails.test.js — the existing assertion ("renders status as Enabled / Disabled") should still pass against rendered text. If it asserts on the newspack-emails__status-dot class, switch to asserting the Badge text or the newspack-badge.is-success / newspack-badge.is-default classes.
Tradeoff (worth naming)
Badge is visually heavier than a tiny dot. In a narrow Status column the pill takes more horizontal space than ● Enabled. That's fine — <Badge> already has flex: 0 0 auto so it won't grow beyond its text, and if the column ends up cramped, widen the column rather than invent a slimmer pattern.
Out of scope (don't do)
- Don't add new Badge levels or styles —
defaultandsuccessare enough for binary status here. - Don't extend
<Badge>itself; if it lacks something, raise a separate issue againstnewspack-components.
- Replace custom status dots with <Badge> component (thomasguillot) - Add visually-hidden h1 to pluginsReady early return (Copilot) - Apply registry label in enrichment loop so curated names display (Copilot) - Fix activate test to use eligible non-reader-activation email (Copilot) - Rename capturedActions to mockCapturedActions for Jest hoisting (Copilot) - Assert at least one enriched email exists in PHPUnit test (Copilot) - Remove unused status dot SCSS rules Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Addressed in a4ae443. Status column now uses |
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Code review — slice 1 of unified email managementReviewed at Important1. Reset action has no
|
- Add isEligible guard on Reset: excluded for woocommerce-source emails and emails without a registry_slug (finding #1) - Capture previous status before optimistic update so rollback doesn't assume binary publish/draft (finding #2) - Drop view_category from enrichment response — dead data not consumed by the frontend (finding #3) - Add @todo NPPD-1532 comment on reset endpoint coupling (finding #4) - Add NPPD-1531 comment on optimistic update / store consolidation (#5) - Replace brittle PHPUnit count assertions with structural invariants: required keys, valid source/recipient, exclusive type keys, no duplicates, sort order (findings #6, #7, #8) - Add optimistic-rollback failure test and reset eligibility test (#9, #11) - Add 6th mockEmail fixture (WC draft) for cleaner activate test (#10) - Coerce pluginsReady with Boolean() to handle undefined (#12) - Use noticeText prop consistently on Notice (#13) - Fix preview placeholder to use neutral gray tokens (#14) - Trim EmailItem interface to consumed fields, add source (#15) - Add source-of-truth comment on category_order strings (#16) - Consolidate three duplicate TODO comments into one (#17) - Add NPPD-1525 ticket reference on EmailPreview TODO (#18) - Extract duplicated screen-reader h1 into PageHeading component (#19) - Add text-overflow ellipsis on trigger description column (#20) - Return localized string from recipient getValue for search (#21) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Addressed all 22 findings in db4113d. Summary: Behavioral fixes:
Test improvements:
Polish:
Finding on group-subscription-invite unconditional registration — left as-is since the email config is loaded unconditionally; 213 Jest tests passing, SCSS lint clean, webpack build clean. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.
Comments suppressed due to low confidence (1)
src/wizards/newspack/views/settings/emails/emails.tsx:236
- This makes Reset eligible for every registered Newspack email, including reader-activation templates such as verification, password reset, and account deletion. The action is described as limited to receipt/welcome templates, so this exposes a destructive reset option on many more rows than intended; restrict the predicate to the resettable email types.
isEligible: ( item: EmailItem ) => item.source !== 'woocommerce' && Boolean( item.registry_slug ),
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Re-review at
|
| # | Finding | Where it landed | Notes |
|---|---|---|---|
| 1 | Reset isEligible guard |
emails.tsx:236 |
item.source !== 'woocommerce' && Boolean( item.registry_slug ) — exactly as recommended. Pinned by reset is eligible for newspack-source emails with a registry_slug test (emails.test.js:311-326). |
| 2 | Optimistic rollback captures previous status | emails.tsx:92-118 |
previousStatus captured inside the setter closure, restored on rejection. Reads cleanly. |
| 3 | view_category dead data dropped |
class-emails-section.php:316-330 |
Removed from both enrichment and fallback branches. PHPUnit assertions no longer mention it. |
| 4 | Reset endpoint coupling tagged | emails.tsx:126-128 |
@todo NPPD-1532 comment on the donations-namespace reach. Right answer for the slice. |
| 5 | Store-consolidation marker | emails.tsx:93 |
// Optimistic update — see NPPD-1531 for consolidating with the wizard data store. |
| 6 | Brittle count assertions → invariants | tests/unit-tests/emails-section.php:17-117 |
Eight discrete invariant tests (required keys, valid source/recipient, recommended is bool, exclusive newspack_type XOR woo_email_id, source↔type consistency, no duplicates). Replaces the three assertCount mirrors with behaviour. |
| 7 | Fallback enrichment branch tested | emails-section.php:138-145 |
The loop now asserts the unmatched path (empty registry_slug) has recommended === false and source set. |
| 8 | Sort-order test | emails-section.php:157-172 |
test_api_get_email_settings_sort_order walks array_column( ..., 'category' ) and asserts group ranks are monotonic. |
| 9 | Rollback failure test | emails.test.js:247-266 |
apiFetch.mockRejectedValueOnce triggers the error path; notice asserted. |
| 10 | Activate fixture detour | emails.test.js:139-150, 277-278 |
Added mockEmails[ 5 ] (woo-on-hold-order, draft) so the activate test uses a real eligible item. |
| 11 | Reset eligibility pinned | emails.test.js:311-326 |
True for newspack-source-with-slug, false for woocommerce, false for empty registry_slug. |
| 12 | pluginsReady undefined coercion |
emails.tsx:59 |
Boolean( … ) wrap. |
| 13 | Notice consistency |
emails.tsx:253-257, 275 |
Both call sites use noticeText prop. |
| 14 | Preview placeholder neutral | emails.scss:32-34 |
wp-colors.$gray-100 background, wp-colors.$gray-700 icon. Opacity removed — icon stays legible. |
| 15 | EmailItem trimmed |
emails.tsx:22-33 |
Down to consumed fields plus the new source. |
| 16 | category_order source-of-truth comment |
class-emails-section.php:336-337 |
Comment points at Reader_Revenue_Emails::add_email_configs() etc. |
| 17 | Three duplicate woo TODOs consolidated | class-emails-section.php:244-246 |
Single block comment above the three entries. |
| 18 | Placeholder TODO has a ticket | emails.tsx:151 |
@todo NPPD-1525 Replace with <EmailPreview> component. (Stays in sync with PR #4730 work.) |
| 19 | <PageHeading /> extracted |
emails.tsx:55, 252, 274 |
Single component, single __() call. |
| 20 | Description ellipsis | emails.scss:38-47 |
text-overflow: ellipsis; white-space: nowrap; max-width: 360px; — see note (C) below. |
| 21 | Recipient getValue returns localised |
emails.tsx:178-179 |
getValue and render now both return the localised string. |
| 22 | group-subscription-invite plugin_dependency: null |
class-emails-section.php:226-234 |
Author kept as-is because the config is registered unconditionally; reflects reality, accepted. |
Small new observations
A. Rollback test asserts the notice but not the row's status
src/wizards/newspack/views/settings/emails/emails.test.js:247-266
deactivate.callback( [ mockEmails[ 0 ] ] );
await waitFor( () => {
expect( screen.getByTestId( 'notice' ) ).toBeInTheDocument();
} );Proves "an error notice is rendered on failure." Doesn't prove "the row's optimistic flip was reverted." A regression where updateStatus dropped the rollback branch but still set the error would still pass.
One extra assertion would close it:
// Before rejection resolves: status should be optimistically 'draft' (Disabled).
// After rejection: status reverts to 'publish' (Enabled).
await waitFor( () => {
const row = screen.getByText( 'Payment receipt' ).closest( 'tr' );
expect( row ).toHaveTextContent( 'Enabled' );
} );B. Description ellipsis now applies in both grid and table view
src/wizards/newspack/views/settings/emails/emails.scss:38-47
.newspack-emails__trigger-description {
color: wp-colors.$gray-900;
font-size: 12px;
margin-top: 4px;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 360px;
}The original concern (finding 20) was a wide table row bloating on multi-sentence descriptions. The fix correctly truncates in table view, but the same rule now hits grid view's card body — where multi-line descriptions are usually fine. Some descriptions ("Sent when a failed subscription payment is about to be retried.") will get clipped in cards that have vertical room for a second line.
Two-line clamp keeps grid-view legibility while still bounding table-view height:
.newspack-emails__trigger-description {
color: wp-colors.$gray-900;
font-size: 12px;
margin-top: 4px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}Or scope the single-line rule to a table-mode selector if DataViews exposes one.
Not a blocker — the ellipsis is correct table behaviour. Just worth a visual pass at narrow viewports.
C. Sort test asserts group order, not within-group registry order
tests/unit-tests/emails-section.php:157-172
Group-rank monotonicity is what most of the bug surface lives on, so this is a reasonable coverage choice. But the stability fix (e16b00a5b) added the array_flip( array_keys( $registry ) ) tiebreaker specifically so registry insertion order survives usort non-stability. That tiebreaker is currently un-asserted.
Cheap addition: within reader-revenue, the registry order is receipt → welcome → cancellation. Assert that array_column( $reader_revenue_subset, 'registry_slug' ) matches [ 'receipt', 'welcome', 'cancellation' ].
D. Fallback source = 'newspack' rationale isn't documented
includes/wizards/newspack/class-emails-section.php:329
Unmatched emails (Emails::get_emails() returned a type the registry doesn't know about) default to source = 'newspack'. This is correct (the endpoint only returns Newspack-managed emails today) but unobvious — a future reader looking at the fallback block won't know that source is being used to gate the reset action and that 'newspack' is the right default. One-line comment:
// Default to 'newspack' source — Emails::get_emails() only returns
// Newspack-managed emails, so unmatched types are by definition newspack.E. Badge display: block → display: inline-block is a shared-component change
packages/components/src/badge/style.scss:19
Verified the blast radius: no external repo (newspack-popups, newspack-newsletters, newspack-ads, newspack-network) imports Badge from newspack-components. Internal consumers (card-form, section-header, card-feature, audience/views/integrations/logs-view, audience/views/content-gates/content-gate-settings) all benefit from inline-block (badges sit next to text). Change is safe.
Flagging it because a one-line shared-component style tweak inside an emails-focused PR is the kind of thing that gets missed on cherry-pick conversations later. Worth noting in the PR description's changelog so the next person searching the Badge history finds the rationale.
Verdict
Ship-ready. The fixup is comprehensive and the test additions are well-targeted. The remaining notes (A-E) are optional polish — none would block merge. A and B are the most worth doing; C is a 4-line PHPUnit add; D is a comment line; E is documentation only.
Original review for the audit trail: #4727 (comment) (then patched to remove #N auto-links → (N)).
- Tighten activate/deactivate isEligible to exclude WooCommerce and reader-activation emails from status toggling. - Add enableGlobalSearch to trigger_description field so descriptions are searchable. - Switch trigger-description CSS from single-line ellipsis to 2-line clamp for better grid-view legibility. - Strengthen rollback test to assert status reverts after failure. - Add eligibility test for activate/deactivate/woocommerce gating. - Fix activate test to use a Newspack email (not WooCommerce). - Add PHPUnit tests for admin-recipient classification and within-group registry insertion order. - Add explanatory comment on fallback `source = 'newspack'` default. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…NPPD-1533 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
dkoo
left a comment
There was a problem hiding this comment.
@kmwilkerson this is an excellent start to a complex new UI. The new DataViews-driven interface is way more intuitive, usable, and better-looking than what we have now.
I have some feedback in inline comments below, and I think the idea of the email registry needs further discussion before we move ahead with it as it's a pretty big change to how the data is handled currently. Otherwise, the new UI works great in practice and is a huge improvement over the current experience.
| * | ||
| * @return array Registry entries keyed by slug. | ||
| */ | ||
| public static function get_email_registry(): array { |
There was a problem hiding this comment.
🔴 Discussion needed: the email registry
I understand that the plan to integrate WooCommerce transactional emails into our UI is planned for a future PR, but I'd like to take some time to discuss the implementation of the email registry here. Maybe I'm missing something, but at first glance the idea of the registry seems a little more complex than it needs to be. Am I understanding the purpose correctly?
- Newspack-specific transactional emails have their own config schema and are registered via the
newspack_email_configsfilter andEmails::get_email_configs()method - Certain transactional emails from WooCommerce and its extensions need to be integrated so that our UI can pick those up and display/interact with them in a similar way to Newspack emails
- The idea of the registry is to fetch Newspack email configs, combine them with the WooCommerce emails we want, and then normalize all of them into a single email config schema
If the normalization of data is the key requirement here, then rather than introducing the registry with a new type of data schema, why not define and extend the data schema based on the existing Newspack email configs? What that would look like:
- A new method in the
WooCommerce_Emailsclass to get configs for WooCommerce emails would define the configs for the Woo emails we want, based on the same config schema as Newspack emails. I also wonder if there's a WooCommerce function somewhere that would get all available transactional emails, which we could use instead of hard-coding the emails we want (which may or may not exist in the way we expect). Then this method could transform the data from whatever format WooCommerce provides into our own schema. - In the
Emails::get_email_configs()method, we would define a "default" config object that contains all of the possible keys with reasonable default values, then fetch email configs for both Newspack and Woo emails - Each email config would get merged with the default object so their defined values override the defaults, while missing values get the defaults
- The end result is a single config object containing both Newspack and Woo email configs without introducing a new data schema
This to me seems to be a simpler and more easily maintainable way to combine Newspack and Woo emails with a single data schema based on how Newspack emails are already configured. WDYT?
There was a problem hiding this comment.
Good architectural callout — agreed the registry adds a parallel schema where extending the existing config schema would be cleaner. I'd like to defer this refactor to a focused follow-up PR rather than restructure slice 1's data layer at this stage. The follow-up would cover:
- Folding trigger_description, recipient, recommended, chip into the existing newspack_email_configs schema with sensible defaults
- Designing the WC integration approach (whether to add WooCommerce_Emails::get_email_configs() that registers via the filter, or extend Emails::serialize_email() to handle the live WC_Email resolution path slice 2 currently uses)
- Replacing Emails_Section::get_email_registry() with config-driven enrichment
Filed as NPPD-1550. Happy to do it in this PR instead if you'd prefer — let me know which way you want to go.
There was a problem hiding this comment.
Since this is such a major architectural change, I'd rather not merge to trunk as-is with the parallel schema. Let's do this:
- Open another PR based off this branch to handle the refactor to unify the schema centered around the existing
newspack_email_configsschema. - Pause work on descendent PRs that are based off this branch until the refactor is complete.
- Once that refactor PR gets merged to this PR (without squashing since it's not merging to a protected branch), we can update any other descendent PRs based off this PR's branch with the unified schema.
There was a problem hiding this comment.
Before I start, one architecture question - the spec sketches three options for how WC emails plug into the unified config schema:
Option A — Your suggestion in full. A new WooCommerce_Emails::get_email_configs() method produces config-schema entries for the WC emails we surface (with source='woocommerce', woo_email_id, chip, plugin gating), registered via the newspack_email_configs filter. Single unified schema across Newspack and WC. Tradeoff: we lose direct access to WC_Email instance methods (get_subject(), get_recipient(), enable()/disable()) — we'd need accessor wrappers or store enough metadata in the config to drive the UI without the live instance.
Option B — Keep slice 2's current path: live WC_Email resolution via WC()->mailer()->get_emails(), extended through Emails::serialize_email() (or a parallel serialize_wc_email()) to produce the same output shape. Tradeoff: no parallel registry, but two code paths into the same response.
Option C — Hybrid. WC emails get config-style entries for discovery + UI metadata, but the toggle endpoint still resolves to the live WC_Email for enable()/disable(). Unified schema for reads; WC stays source of truth for writes.
Option A is cleanest if we're committed to a single schema, but it asks us to wrap or duplicate enough of WC_Email's API to drive activate/deactivate and recipient resolution without the live instance — adding surface area we'd otherwise get for free. C preserves the schema win where it matters (API response, UI driving off it) without re-implementing WC's write path.
Which do you want me to build against?
- Remove dead test asserting absence of never-present strings - Drop optimistic status update in favor of server refetch - Make preview thumbnail and email name clickable edit links - Remove redundant `all` key from localized settings data - Add enableGlobalSearch to recipient field for search filtering Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
) Per dkoo's review of #4727, the optimistic status update on toggleWcEmail added specialized fetch + manual rollback logic that didn't match how status updates work in the rest of the wizards. Fall back to the same pattern as updateStatus in this file: fire the API call, refetch on success, surface a notice on failure.
dkoo
left a comment
There was a problem hiding this comment.
@kmwilkerson I confirmed that most of the open threads are now resolved by the the latest changes. However, I left two threads open that I still think need attention. The biggest one is the thread about refactoring the email registry data schema, which I think is worth doing ASAP rather than handling later, since there are several descendant PRs based off this branch already.
Once we resolve these threads, we can consider this PR approved (but hold off merging to trunk until the descendant PRs are merged first, to avoid merge conflictpocalypse).
Replace direct apiFetch calls with useWizardApiFetch hook so that toggle actions (activate/deactivate) and the initial data fetch surface a unified loading state through the hook's isFetching flag. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Closing in favor of the monorepo rebuild: Automattic/newspack-workspace#137 With the legacy repo frozen for the monorepo migration, we rebuilt slice 1 in The descendant slices (#4732, #4756, #4758, #4759) will be rebuilt in the monorepo the same way and closed as their replacements open. Thanks for the review here — it directly shaped the cleaner architecture in the rebuild. |
What this PR does
First slice of the unified email management UI (NPPD-945). Replaces the existing
WizardsActionCard-based email list at Newspack → Settings → Emails with a DataViews-based list, backed by a new curated email registry in PHP.This slice surfaces only Newspack-registered emails. WooCommerce email surfacing is deliberately deferred to follow-up PRs to keep the diff scoped and the engineering risk concentrated where it belongs.
Companion PR (newspack-manager): Automattic/newspack-manager#567
Code changes
Backend (
includes/wizards/newspack/class-emails-section.php)Emails_Section::get_email_registry()— a curated registry of 23 transactional emails, keyed by stable slug. Each entry includessource,newspack_typeorwoo_email_id,recommended(bool),plugin_dependency,recipient,label, andtrigger_description.api_get_email_settings()to return anewspack_emailsarray — existing Newspack emails joined against the registry, with metadata for recipient labeling and trigger descriptions. Emails not in the registry still appear with sensible defaults.Frontend (
src/wizards/newspack/views/settings/emails/)WizardsActionCard.map()with aDataViewslist, following the pattern fromsrc/wizards/audience/views/content-gates/institutions/index.tsx.WizardsTab className="newspack-emails-tab").emails.scss: aligns DataViews controls (matchingsrc/wizards/audience/views/integrations/style.scsspattern), styles status dots, trigger description color, preview placeholder tile.Preview placeholder
The preview field renders an envelope icon on a neutral gray tile. A
// TODO: Replace with <EmailPreview> component when builtcomment marks where a real preview component would go. Building that component is out of scope for this PR.Tests
tests/unit-tests/emails-section.php): registry entry counts, recommended counts, plugin-dependency counts, label/trigger/recipient completeness.src/wizards/newspack/views/settings/emails/emails.test.js): DataViews renders with mock data, recipient column resolves, status enabled/disabled, subtitle absence, tabs/show-all absence.Why slice this PR
The full PRD scope (Newspack + WooCommerce + WC Subscriptions email surfacing, with plugin-conditional logic, status toggling across different storage backends) is meaty. Splitting along the registry boundary keeps each PR independently shippable and reviewable, and concentrates the trickier Woo-discovery logic in follow-up PRs where it can get focused attention.
Manual testing
Tested locally with Newspack, Newspack Newsletters, WooCommerce, and WooCommerce Subscriptions activated. Verified:
enable_woocommerce_email_editortoggle (separate section below) still works untouchedPHPUnit not run locally.
Note: pre-existing TypeScript generic variance errors on the DataViews wrapper types are not introduced by this PR.
All Submissions: