feat(emails): unified email registry + DataViews list#4727
feat(emails): unified email registry + DataViews list#4727kmwilkerson wants to merge 19 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?
| expect( screen.queryByText( 'Essentials' ) ).not.toBeInTheDocument(); | ||
| expect( screen.queryByText( 'All enabled' ) ).not.toBeInTheDocument(); | ||
| expect( screen.queryByText( 'Show all emails' ) ).not.toBeInTheDocument(); | ||
| expect( screen.queryByText( 'Manage the transactional emails your readers receive.' ) ).not.toBeInTheDocument(); |
There was a problem hiding this comment.
🟡 Nit: dead tests
I'm not seeing these strings anywhere in the code, so won't these tests always pass?
| ( postId: number, nextStatus: string ) => { | ||
| setError( null ); | ||
| let previousStatus: string | undefined; | ||
| // Optimistic update — see NPPD-1531 for consolidating with the wizard data store. |
There was a problem hiding this comment.
🔴 Optimistic update is unnecessary bloat
I don't think the optimistic status update is worth the trouble. We're losing the convenience of using wizardApiFetch from our hook and adding a bunch of complexity that doesn't exist for other REST update calls throughout the wizards. Since we're not doing optimistic UI updates in most other wizard components, it makes this feature require a bunch of specialized code. If we want optimistic UI updates I'd suggest we look into adding that functionality to the wizardApiFetch function instead, so that it can be used the same way in all wizard UI.
| } | ||
| return email; | ||
| } ) | ||
| id: 'name', |
There was a problem hiding this comment.
🟡 Suggestion: clickable thumbnail + title
Can we make the email thumbnail and name clickable to the edit view for that email?
|
|
||
| const [ emails, setEmails ] = useState( Object.values( emailSections.emails.all ) ); | ||
| useEffect( () => { | ||
| fetchData(); |
There was a problem hiding this comment.
🔴 Double data fetch on initial load
Since we're now fetching the emails via REST API on view mount, we should no longer need to have this same data localized at the window.newspackSettings.emails.all object. Removing the localized data would help avoid a redundant double fetch for the initial page load.
| enableSorting: false, | ||
| }, | ||
| { | ||
| id: 'recipient', |
There was a problem hiding this comment.
🟡 Nit: enableGlobalSearch for recipient?
enableGlobalSearch is set on name and trigger_description but not recipient. Should it be?
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: