feat(emails): unified email registry + DataViews list#4727
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.
| function Emails() { | ||
| return ( | ||
| <WizardsTab title={ __( 'Emails', 'newspack-plugin' ) }> | ||
| <WizardsTab className="newspack-emails-tab"> |
| apiFetch< EmailSettings >( { | ||
| path: '/newspack/v1/wizard/newspack-settings/emails', | ||
| } ) |
| const updateStatus = useCallback( | ||
| ( postId: number, status: string ) => { | ||
| setError( null ); | ||
| // Optimistic update. | ||
| setData( prev => | ||
| prev.map( email => { | ||
| if ( email.post_id === postId ) { | ||
| return { ...email, status }; | ||
| } | ||
| return email; | ||
| } ) | ||
| ); | ||
| apiFetch( { | ||
| path: `/wp/v2/${ postType }/${ postId }`, | ||
| method: 'POST', | ||
| data: { status }, | ||
| } ).catch( () => { | ||
| // Revert on failure. | ||
| setData( prev => | ||
| prev.map( email => { | ||
| if ( email.post_id === postId ) { | ||
| return { | ||
| ...email, | ||
| status: status === 'publish' ? 'draft' : 'publish', | ||
| }; | ||
| } | ||
| return email; | ||
| } ) | ||
| ); | ||
| setError( __( 'Failed to update email status.', 'newspack-plugin' ) ); | ||
| } ); |
| const resetEmail = useCallback( | ||
| ( postId: number ) => { | ||
| setError( null ); | ||
| apiFetch( { | ||
| path: `/newspack/v1/wizard/newspack-audience-donations/emails/${ postId }`, | ||
| method: 'DELETE', | ||
| } ) | ||
| .then( () => { | ||
| fetchData(); | ||
| } ) | ||
| .catch( () => { | ||
| setError( __( 'Failed to reset email. Please try again.', 'newspack-plugin' ) ); | ||
| } ); |
| foreach ( $emails as $type => $email ) { | ||
| if ( isset( $registry_lookup[ $type ] ) ) { | ||
| $match = $registry_lookup[ $type ]; | ||
| $email['recommended'] = $match['recommended']; | ||
| $email['view_category'] = $match['recommended'] ? 'essentials' : 'all-enabled'; | ||
| $email['trigger_description'] = $match['trigger_description']; | ||
| $email['registry_slug'] = $match['registry_slug']; | ||
| $email['recipient'] = $match['recipient']; | ||
| } else { | ||
| $email['recommended'] = false; | ||
| $email['view_category'] = 'available'; | ||
| $email['trigger_description'] = ''; | ||
| $email['registry_slug'] = ''; | ||
| $email['recipient'] = 'reader'; | ||
| } | ||
| $newspack_emails[] = $email; |
| usort( | ||
| $newspack_emails, | ||
| function ( $a, $b ) use ( $category_order ) { | ||
| $order_a = $category_order[ $a['category'] ?? '' ] ?? 2; | ||
| $order_b = $category_order[ $b['category'] ?? '' ] ?? 2; | ||
| return $order_a - $order_b; | ||
| } |
There was a problem hiding this comment.
Fixed in e16b00a. The previous comparator returned 0 for same-category items, and usort is not stable, so within-category order wasn't actually preserved despite the comment. Added a slug-to-index map from array_flip( array_keys( $registry ) ) and used it as a tiebreaker. Unregistered emails fall back to PHP_INT_MAX so they sort to the end. No API schema change — the order map is captured by the closure.
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.
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.
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: