From f411cd13e8eb720c97da644c7e1caa845900b402 Mon Sep 17 00:00:00 2001 From: Thomas Guillot Date: Wed, 13 May 2026 10:38:37 +0100 Subject: [PATCH 01/21] feat(rolling-content): scaffold hidden demo wizard with conditional menu --- includes/class-newspack.php | 2 + includes/class-wizards.php | 1 + .../wizards/class-rolling-content-demo.php | 163 ++++++++++++++++++ 3 files changed, 166 insertions(+) create mode 100644 includes/wizards/class-rolling-content-demo.php diff --git a/includes/class-newspack.php b/includes/class-newspack.php index 606fcf9762..67297b2495 100644 --- a/includes/class-newspack.php +++ b/includes/class-newspack.php @@ -159,6 +159,7 @@ private function includes() { include_once NEWSPACK_ABSPATH . 'includes/wizards/class-setup-wizard.php'; include_once NEWSPACK_ABSPATH . 'includes/wizards/class-components-demo.php'; + include_once NEWSPACK_ABSPATH . 'includes/wizards/class-rolling-content-demo.php'; include_once NEWSPACK_ABSPATH . 'includes/wizards/traits/trait-wizards-admin-header.php'; @@ -176,6 +177,7 @@ private function includes() { include_once NEWSPACK_ABSPATH . 'includes/wizards/class-setup-wizard.php'; include_once NEWSPACK_ABSPATH . 'includes/wizards/class-components-demo.php'; + include_once NEWSPACK_ABSPATH . 'includes/wizards/class-rolling-content-demo.php'; // Listings Wizard. include_once NEWSPACK_ABSPATH . 'includes/wizards/class-listings-wizard.php'; diff --git a/includes/class-wizards.php b/includes/class-wizards.php index 873b72558f..d222257914 100644 --- a/includes/class-wizards.php +++ b/includes/class-wizards.php @@ -42,6 +42,7 @@ public static function init() { public static function init_wizards() { self::$wizards = [ 'components-demo' => new Components_Demo(), + 'rolling-content-demo' => new Rolling_Content_Demo(), // v2 Information Architecture. 'newspack-dashboard' => new Newspack_Dashboard(), 'setup' => new Setup_Wizard(), diff --git a/includes/wizards/class-rolling-content-demo.php b/includes/wizards/class-rolling-content-demo.php new file mode 100644 index 0000000000..eea19a8a71 --- /dev/null +++ b/includes/wizards/class-rolling-content-demo.php @@ -0,0 +1,163 @@ +slug, self::SLUG_ADD ], true ); + } + + /** + * Register admin pages. + * + * Both pages are ALWAYS registered as hidden so direct URL navigation works. + * When the user is on either page, a visible top-level menu with sub-items is + * also registered so the demo's IA can be observed in the sidebar. + */ + public function add_page() { + // Always register both slugs as hidden so direct URL access works. + add_submenu_page( + 'hidden', + $this->get_name(), + $this->get_name(), + $this->capability, + $this->slug, + [ $this, 'render_wizard' ] + ); + add_submenu_page( + 'hidden', + __( 'Add Rolling Content', 'newspack-plugin' ), + __( 'Add Rolling Content', 'newspack-plugin' ), + $this->capability, + self::SLUG_ADD, + [ $this, 'render_wizard' ] + ); + + if ( ! $this->is_wizard_page() ) { + return; + } + + $icon = sprintf( + 'data:image/svg+xml;base64,%s', + base64_encode( Newspack_UI_Icons::get_svg( 'collections' ) ) + ); + + add_menu_page( + $this->get_name(), + $this->get_name(), + $this->capability, + $this->slug, + [ $this, 'render_wizard' ], + $icon + ); + add_submenu_page( + $this->slug, + $this->get_name(), + __( 'All Rolling Content', 'newspack-plugin' ), + $this->capability, + $this->slug, + [ $this, 'render_wizard' ] + ); + add_submenu_page( + $this->slug, + __( 'Add Rolling Content', 'newspack-plugin' ), + __( 'Add Rolling Content', 'newspack-plugin' ), + $this->capability, + self::SLUG_ADD, + [ $this, 'render_wizard' ] + ); + } + + /** + * Enqueue scripts and styles. + * + * The parent class's slug check is too strict (single slug). We replicate the + * parts we need so both demo pages get the shared `newspack-wizards` chrome. + */ + public function enqueue_scripts_and_styles() { + if ( ! $this->is_wizard_page() ) { + return; + } + + Newspack::load_common_assets(); + + // Data carrier script (no source). + wp_register_script( 'newspack_data', '', [], '1.0', false ); + wp_localize_script( + 'newspack_data', + 'newspack_urls', + [ + 'public_path' => Newspack::plugin_url() . '/dist/', + 'site' => get_site_url(), + ] + ); + wp_enqueue_script( 'newspack_data' ); + + // Shared wizards bundle (contains the components map our routes are added to). + wp_register_script( + 'newspack-wizards', + Newspack::plugin_url() . '/dist/wizards.js', + $this->get_script_dependencies(), + NEWSPACK_PLUGIN_VERSION, + true + ); + wp_enqueue_script( 'newspack-wizards' ); + } +} From 5cdcce132caa9c837e297dcbb77b1e4f43fc662c Mon Sep 17 00:00:00 2001 From: Thomas Guillot Date: Wed, 13 May 2026 10:44:10 +0100 Subject: [PATCH 02/21] feat(rolling-content): wire up React routes with placeholder views --- src/wizards/index.tsx | 8 ++++++++ src/wizards/rollingContent/views/add.tsx | 17 +++++++++++++++++ src/wizards/rollingContent/views/all.tsx | 17 +++++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 src/wizards/rollingContent/views/add.tsx create mode 100644 src/wizards/rollingContent/views/all.tsx diff --git a/src/wizards/index.tsx b/src/wizards/index.tsx index 7e23bf9041..bbf7f51e32 100644 --- a/src/wizards/index.tsx +++ b/src/wizards/index.tsx @@ -60,6 +60,14 @@ const components: Record< string, any > = { label: __( 'Premium newsletters', 'newspack-plugin' ), component: lazy( () => import( /* webpackChunkName: "newsletters-wizards" */ './newsletters/views/premium-newsletters' ) ), }, + 'newspack-rolling-content': { + label: __( 'Rolling Content', 'newspack-plugin' ), + component: lazy( () => import( /* webpackChunkName: "rolling-content-wizards" */ './rollingContent/views/all' ) ), + }, + 'newspack-rolling-content-add': { + label: __( 'Add Rolling Content', 'newspack-plugin' ), + component: lazy( () => import( /* webpackChunkName: "rolling-content-wizards" */ './rollingContent/views/add' ) ), + }, } as const; // Conditionally add the Audience Integrations page if the feature is enabled. diff --git a/src/wizards/rollingContent/views/add.tsx b/src/wizards/rollingContent/views/add.tsx new file mode 100644 index 0000000000..a4d9b92b11 --- /dev/null +++ b/src/wizards/rollingContent/views/add.tsx @@ -0,0 +1,17 @@ +/** + * Rolling Content — Add view (placeholder; expanded in Task 5). + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +export default function Add() { + return ( +
+

{ __( 'Add Rolling Content', 'newspack-plugin' ) }

+

{ __( 'Placeholder — Final copy comes in Task 5.', 'newspack-plugin' ) }

+
+ ); +} diff --git a/src/wizards/rollingContent/views/all.tsx b/src/wizards/rollingContent/views/all.tsx new file mode 100644 index 0000000000..76ddf4fdee --- /dev/null +++ b/src/wizards/rollingContent/views/all.tsx @@ -0,0 +1,17 @@ +/** + * Rolling Content — All view (placeholder; replaced in Task 4). + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +export default function All() { + return ( +
+

{ __( 'All Rolling Content', 'newspack-plugin' ) }

+

{ __( 'Placeholder — DataViews comes in Task 4.', 'newspack-plugin' ) }

+
+ ); +} From 137814b8e2e7d0f80d771c0582fa85365d627a15 Mon Sep 17 00:00:00 2001 From: Thomas Guillot Date: Wed, 13 May 2026 11:02:45 +0100 Subject: [PATCH 03/21] feat(rolling-content): add types and fake dataset --- src/wizards/rollingContent/data.ts | 132 ++++++++++++++++++++++++++ src/wizards/rollingContent/types.d.ts | 29 ++++++ 2 files changed, 161 insertions(+) create mode 100644 src/wizards/rollingContent/data.ts create mode 100644 src/wizards/rollingContent/types.d.ts diff --git a/src/wizards/rollingContent/data.ts b/src/wizards/rollingContent/data.ts new file mode 100644 index 0000000000..34304fe1dc --- /dev/null +++ b/src/wizards/rollingContent/data.ts @@ -0,0 +1,132 @@ +/** + * Rolling Content demo — fake in-memory dataset. + * + * 30 rolling contents, each with 5-25 deterministic entries. All values are + * stable across renders (no Math.random, no Date.now at module scope). + */ + +const ROLLING_TITLES: string[] = [ + 'Election Night 2026: Live Updates', + 'City Council Budget Hearings', + 'Wildfire Response Coverage', + 'School Board Recall Effort', + 'Hurricane Season Live Coverage', + 'Mayoral Debate Aftermath', + 'Transit Strike Day Two', + 'Federal Inquiry Hearings', + 'Stadium Vote Live', + 'Climate Protest Coverage', + 'Tech Company Layoffs Tracker', + 'Inauguration Day', + 'Special Session Coverage', + 'Tax Reform Town Halls', + 'Water Restriction Updates', + 'Election Recount Results', + 'Public Health Emergency Briefings', + 'Budget Override Vote', + 'Police Reform Hearings', + 'Bond Measure Election Night', + 'School District Negotiations', + 'Housing Crisis Forum', + 'Power Grid Outage Updates', + 'Recall Petition Tracker', + 'City Charter Vote', + 'Special Counsel Updates', + 'Park District Vote', + 'Casino Referendum Coverage', + 'Voter Roll Audit Hearings', + 'Annexation Vote Live', +]; + +const ENTRY_TITLES: string[] = [ + 'Polls close in District 4', + 'Mayor responds to opposition statement', + 'Vote count delayed by ballot challenge', + 'Lawmakers reach tentative deal', + 'Live update: returns from precinct 12', + 'Statement from city attorney', + 'Crowd gathers outside city hall', + 'Press conference scheduled for 8 PM', + 'New witness called to testify', + 'Council moves into closed session', + 'Update: highway reopens to traffic', + 'Officials confirm timeline for vote', + 'Spokesperson denies allegations', + 'Crowd estimate revised upward', + 'Decision expected by tomorrow morning', +]; + +const AUTHORS: string[] = [ 'Alex Rivera', 'Jamie Chen', 'Morgan Patel', 'Sam Johnson', 'Riley Cooper', 'Jordan Kim' ]; + +const TAGS: string[] = [ 'breaking', 'politics', 'local', 'national', 'elections', 'transit', 'weather', 'courts', 'education', 'climate' ]; + +const BASE_DATE = new Date( '2026-05-01T12:00:00Z' ); + +function makeDate( seed: number ): string { + const dayOffset = ( seed * 17 ) % 90; + const d = new Date( BASE_DATE ); + d.setDate( d.getDate() - dayOffset ); + return d.toISOString(); +} + +function entryStatusFor( id: number ): EntryStatus { + const statusRoll = ( id * 13 ) % 100; + if ( statusRoll < 70 ) { + return 'published'; + } + if ( statusRoll < 90 ) { + return 'draft'; + } + return 'scheduled'; +} + +function rollingStatusFor( idx: number ): RollingContentStatus { + if ( idx < 12 ) { + return 'active'; + } + if ( idx < 22 ) { + return 'archived'; + } + return 'scheduled'; +} + +function makeEntries( parentId: number, count: number ): Entry[] { + const entries: Entry[] = []; + for ( let i = 0; i < count; i++ ) { + const id = parentId * 1000 + i; + const status: EntryStatus = entryStatusFor( id ); + + const tagCount = 1 + ( ( id * 3 ) % 3 ); + const tags: string[] = []; + for ( let t = 0; t < tagCount; t++ ) { + tags.push( TAGS[ ( id * 7 + t * 11 ) % TAGS.length ] ); + } + + entries.push( { + id, + title: ENTRY_TITLES[ ( id * 5 ) % ENTRY_TITLES.length ], + date: makeDate( id ), + author: AUTHORS[ id % AUTHORS.length ], + featuredImage: `https://picsum.photos/seed/entry-${ id }/200/120`, + status, + tags, + } ); + } + return entries; +} + +export const ROLLING_CONTENTS: RollingContent[] = ROLLING_TITLES.map( ( title, idx ) => { + const id = idx + 1; + const status: RollingContentStatus = rollingStatusFor( idx ); + const entryCount = 5 + ( ( id * 7 ) % 21 ); + return { + id, + title, + date: makeDate( id ), + featuredImage: `https://picsum.photos/seed/rolling-${ id }/200/120`, + status, + entries: makeEntries( id, entryCount ), + }; +} ); + +export const getRollingContent = ( id: number ): RollingContent | undefined => ROLLING_CONTENTS.find( r => r.id === id ); diff --git a/src/wizards/rollingContent/types.d.ts b/src/wizards/rollingContent/types.d.ts new file mode 100644 index 0000000000..fdf7c04a8f --- /dev/null +++ b/src/wizards/rollingContent/types.d.ts @@ -0,0 +1,29 @@ +/** + * Rolling Content demo — type definitions. + */ + +declare global { + type RollingContentStatus = 'active' | 'archived' | 'scheduled'; + type EntryStatus = 'published' | 'draft' | 'scheduled'; + + interface Entry { + id: number; + title: string; + date: string; + author: string; + featuredImage: string; + status: EntryStatus; + tags: string[]; + } + + interface RollingContent { + id: number; + title: string; + date: string; + featuredImage: string; + status: RollingContentStatus; + entries: Entry[]; + } +} + +export {}; From 3c4e334be33f84c80a8a3f30cb10e562456c07b7 Mon Sep 17 00:00:00 2001 From: Thomas Guillot Date: Wed, 13 May 2026 11:07:04 +0100 Subject: [PATCH 04/21] fix(rolling-content): use coprime multipliers for dataset variety --- src/wizards/rollingContent/data.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wizards/rollingContent/data.ts b/src/wizards/rollingContent/data.ts index 34304fe1dc..4285bc5fbc 100644 --- a/src/wizards/rollingContent/data.ts +++ b/src/wizards/rollingContent/data.ts @@ -96,7 +96,7 @@ function makeEntries( parentId: number, count: number ): Entry[] { const id = parentId * 1000 + i; const status: EntryStatus = entryStatusFor( id ); - const tagCount = 1 + ( ( id * 3 ) % 3 ); + const tagCount = 1 + ( id % 3 ); const tags: string[] = []; for ( let t = 0; t < tagCount; t++ ) { tags.push( TAGS[ ( id * 7 + t * 11 ) % TAGS.length ] ); @@ -104,7 +104,7 @@ function makeEntries( parentId: number, count: number ): Entry[] { entries.push( { id, - title: ENTRY_TITLES[ ( id * 5 ) % ENTRY_TITLES.length ], + title: ENTRY_TITLES[ ( id * 7 ) % ENTRY_TITLES.length ], date: makeDate( id ), author: AUTHORS[ id % AUTHORS.length ], featuredImage: `https://picsum.photos/seed/entry-${ id }/200/120`, @@ -118,7 +118,7 @@ function makeEntries( parentId: number, count: number ): Entry[] { export const ROLLING_CONTENTS: RollingContent[] = ROLLING_TITLES.map( ( title, idx ) => { const id = idx + 1; const status: RollingContentStatus = rollingStatusFor( idx ); - const entryCount = 5 + ( ( id * 7 ) % 21 ); + const entryCount = 5 + ( ( id * 11 ) % 21 ); return { id, title, From a3599928897ccff40751d183eca0b27aeb774d82 Mon Sep 17 00:00:00 2001 From: Thomas Guillot Date: Wed, 13 May 2026 11:10:56 +0100 Subject: [PATCH 05/21] feat(rolling-content): all rolling content dataviews --- src/wizards/rollingContent/views/all.tsx | 135 ++++++++++++++++++++++- 1 file changed, 130 insertions(+), 5 deletions(-) diff --git a/src/wizards/rollingContent/views/all.tsx b/src/wizards/rollingContent/views/all.tsx index 76ddf4fdee..706a89e63f 100644 --- a/src/wizards/rollingContent/views/all.tsx +++ b/src/wizards/rollingContent/views/all.tsx @@ -1,17 +1,142 @@ /** - * Rolling Content — All view (placeholder; replaced in Task 4). + * Rolling Content — All view (DataViews). */ /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; +import { useState, useEffect, useMemo } from '@wordpress/element'; +import { useDispatch } from '@wordpress/data'; +import { filterSortAndPaginate } from '@wordpress/dataviews'; +import type { Action, Field, View } from '@wordpress/dataviews'; + +/** + * Internal dependencies + */ +import { DataViews } from '../../../../packages/components/src'; +import { WIZARD_STORE_NAMESPACE } from '../../../../packages/components/src/wizard/store'; +import { ROLLING_CONTENTS } from '../data'; + +const STATUS_LABELS: Record< RollingContentStatus, string > = { + active: __( 'Active', 'newspack-plugin' ), + archived: __( 'Archived', 'newspack-plugin' ), + scheduled: __( 'Scheduled', 'newspack-plugin' ), +}; + +const STATUS_COLORS: Record< RollingContentStatus, { bg: string; fg: string } > = { + active: { bg: '#d1fae5', fg: '#065f46' }, + archived: { bg: '#e5e7eb', fg: '#374151' }, + scheduled: { bg: '#dbeafe', fg: '#1e40af' }, +}; + +function StatusPill( { status }: { status: RollingContentStatus } ) { + const c = STATUS_COLORS[ status ]; + return ( + + { STATUS_LABELS[ status ] } + + ); +} + +const DEFAULT_VIEW: View = { + type: 'table', + page: 1, + perPage: 20, + sort: { field: 'date', direction: 'desc' }, + search: '', + fields: [ 'date', 'status' ], + filters: [], + layout: {}, + titleField: 'title', + mediaField: 'featured_image', +}; export default function All() { + const { setHeaderData } = useDispatch( WIZARD_STORE_NAMESPACE ); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [ data, setData ] = useState< RollingContent[] >( ROLLING_CONTENTS ); + const [ view, setView ] = useState< View >( DEFAULT_VIEW ); + + useEffect( () => { + setHeaderData( { + sectionName: __( 'Rolling Content', 'newspack-plugin' ), + actions: [ + { + type: 'primary', + label: __( 'Add Rolling Content', 'newspack-plugin' ), + href: 'admin.php?page=newspack-rolling-content-add', + }, + ], + } ); + }, [ setHeaderData ] ); + + const fields: Field< RollingContent >[] = useMemo( + () => [ + { + id: 'featured_image', + label: __( 'Featured image', 'newspack-plugin' ), + type: 'media', + render: ( { item } ) => {, + enableSorting: false, + }, + { + id: 'title', + label: __( 'Title', 'newspack-plugin' ), + enableGlobalSearch: true, + getValue: ( { item } ) => item.title, + render: ( { item } ) => { item.title }, + }, + { + id: 'date', + label: __( 'Date', 'newspack-plugin' ), + getValue: ( { item } ) => item.date, + render: ( { item } ) => + new Date( item.date ).toLocaleDateString( undefined, { + year: 'numeric', + month: 'short', + day: 'numeric', + } ), + }, + { + id: 'status', + label: __( 'Status', 'newspack-plugin' ), + getValue: ( { item } ) => item.status, + render: ( { item } ) => , + elements: ( Object.keys( STATUS_LABELS ) as RollingContentStatus[] ).map( value => ( { + value, + label: STATUS_LABELS[ value ], + } ) ), + }, + ], + [] + ); + + const actions: Action< RollingContent >[] = useMemo( () => [], [] ); + + const { data: processedData, paginationInfo } = useMemo( () => filterSortAndPaginate( data, view, fields ), [ data, view, fields ] ); + return ( -
-

{ __( 'All Rolling Content', 'newspack-plugin' ) }

-

{ __( 'Placeholder — DataViews comes in Task 4.', 'newspack-plugin' ) }

-
+ String( item.id ) } + search + /> ); } From 1632dcd735008ef73dd25f1f38c6ffe24853c995 Mon Sep 17 00:00:00 2001 From: Thomas Guillot Date: Wed, 13 May 2026 11:13:51 +0100 Subject: [PATCH 06/21] feat(rolling-content): add rolling content placeholder view --- src/wizards/rollingContent/views/add.tsx | 38 ++++++++++++++++++++---- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/src/wizards/rollingContent/views/add.tsx b/src/wizards/rollingContent/views/add.tsx index a4d9b92b11..3abb0e7d94 100644 --- a/src/wizards/rollingContent/views/add.tsx +++ b/src/wizards/rollingContent/views/add.tsx @@ -1,17 +1,45 @@ /** - * Rolling Content — Add view (placeholder; expanded in Task 5). + * Rolling Content — Add view (placeholder). */ /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; +import { useEffect } from '@wordpress/element'; +import { useDispatch } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { WIZARD_STORE_NAMESPACE } from '../../../../packages/components/src/wizard/store'; +import WizardSection from '../../wizards-section'; export default function Add() { + const { setHeaderData } = useDispatch( WIZARD_STORE_NAMESPACE ); + + useEffect( () => { + setHeaderData( { + sectionName: __( 'Add Rolling Content', 'newspack-plugin' ), + actions: [ + { + type: 'secondary', + label: __( 'Back to All Rolling Content', 'newspack-plugin' ), + href: 'admin.php?page=newspack-rolling-content', + }, + ], + } ); + }, [ setHeaderData ] ); + return ( -
-

{ __( 'Add Rolling Content', 'newspack-plugin' ) }

-

{ __( 'Placeholder — Final copy comes in Task 5.', 'newspack-plugin' ) }

-
+ + <> + ); } From f32ae17713e7f3b0b02ed1c81ad45cfa719eae8d Mon Sep 17 00:00:00 2001 From: Thomas Guillot Date: Wed, 13 May 2026 11:17:34 +0100 Subject: [PATCH 07/21] feat(rolling-content): edit and delete row actions with shared modals --- .../rollingContent/modals/delete-confirm.tsx | 66 +++++++++++++++++++ .../rollingContent/modals/edit-info.tsx | 46 +++++++++++++ src/wizards/rollingContent/views/all.tsx | 30 ++++++++- 3 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 src/wizards/rollingContent/modals/delete-confirm.tsx create mode 100644 src/wizards/rollingContent/modals/edit-info.tsx diff --git a/src/wizards/rollingContent/modals/delete-confirm.tsx b/src/wizards/rollingContent/modals/delete-confirm.tsx new file mode 100644 index 0000000000..971509ba43 --- /dev/null +++ b/src/wizards/rollingContent/modals/delete-confirm.tsx @@ -0,0 +1,66 @@ +/** + * Rolling Content demo — Delete confirmation modal. + * + * Shared destructive confirmation used for both Rolling Content and Entry deletes. + * On confirm, calls `onConfirm`; the caller mutates local state. + */ + +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { Button, Modal } from '@wordpress/components'; + +type ItemType = 'rolling-content' | 'entry'; + +const ITEM_NOUN: Record< ItemType, string > = { + 'rolling-content': __( 'rolling content', 'newspack-plugin' ), + entry: __( 'entry', 'newspack-plugin' ), +}; + +export default function DeleteConfirmModal( { + itemType, + title, + onConfirm, + onClose, +}: { + itemType: ItemType; + title: string; + onConfirm: () => void; + onClose: () => void; +} ) { + return ( + +

+ { sprintf( + /* translators: %s: item type. */ + __( 'This will permanently delete this %s. This action cannot be undone.', 'newspack-plugin' ), + ITEM_NOUN[ itemType ] + ) } +

+
+ + +
+
+ ); +} diff --git a/src/wizards/rollingContent/modals/edit-info.tsx b/src/wizards/rollingContent/modals/edit-info.tsx new file mode 100644 index 0000000000..4e9df27791 --- /dev/null +++ b/src/wizards/rollingContent/modals/edit-info.tsx @@ -0,0 +1,46 @@ +/** + * Rolling Content demo — Edit info modal. + * + * Shared info modal used for both Rolling Content and Entry "Edit" row actions. + * Explains that, in a real implementation, this would open the block editor. + */ + +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { Button, Modal } from '@wordpress/components'; + +type ItemType = 'rolling-content' | 'entry'; + +const ITEM_NOUN: Record< ItemType, string > = { + 'rolling-content': __( 'rolling content', 'newspack-plugin' ), + entry: __( 'entry', 'newspack-plugin' ), +}; + +export default function EditInfoModal( { itemType, title, onClose }: { itemType: ItemType; title: string; onClose: () => void } ) { + return ( + +

+ { sprintf( + /* translators: %s: item title. */ + __( 'This would open the block editor for %s.', 'newspack-plugin' ), + title + ) } +

+
+ +
+
+ ); +} diff --git a/src/wizards/rollingContent/views/all.tsx b/src/wizards/rollingContent/views/all.tsx index 706a89e63f..4ff8b96ced 100644 --- a/src/wizards/rollingContent/views/all.tsx +++ b/src/wizards/rollingContent/views/all.tsx @@ -17,6 +17,8 @@ import type { Action, Field, View } from '@wordpress/dataviews'; import { DataViews } from '../../../../packages/components/src'; import { WIZARD_STORE_NAMESPACE } from '../../../../packages/components/src/wizard/store'; import { ROLLING_CONTENTS } from '../data'; +import EditInfoModal from '../modals/edit-info'; +import DeleteConfirmModal from '../modals/delete-confirm'; const STATUS_LABELS: Record< RollingContentStatus, string > = { active: __( 'Active', 'newspack-plugin' ), @@ -64,7 +66,6 @@ const DEFAULT_VIEW: View = { export default function All() { const { setHeaderData } = useDispatch( WIZARD_STORE_NAMESPACE ); - // eslint-disable-next-line @typescript-eslint/no-unused-vars const [ data, setData ] = useState< RollingContent[] >( ROLLING_CONTENTS ); const [ view, setView ] = useState< View >( DEFAULT_VIEW ); @@ -122,7 +123,32 @@ export default function All() { [] ); - const actions: Action< RollingContent >[] = useMemo( () => [], [] ); + const actions: Action< RollingContent >[] = useMemo( + () => [ + { + id: 'edit', + label: __( 'Edit', 'newspack-plugin' ), + isPrimary: true, + RenderModal: ( { items, closeModal } ) => ( + + ), + }, + { + id: 'delete', + label: __( 'Delete', 'newspack-plugin' ), + isDestructive: true, + RenderModal: ( { items, closeModal } ) => ( + setData( prev => prev.filter( r => r.id !== items[ 0 ].id ) ) } + onClose={ closeModal } + /> + ), + }, + ], + [] + ); const { data: processedData, paginationInfo } = useMemo( () => filterSortAndPaginate( data, view, fields ), [ data, view, fields ] ); From 2328fb6647be517c903f447122aae53c6fd5806c Mon Sep 17 00:00:00 2001 From: Thomas Guillot Date: Wed, 13 May 2026 11:21:27 +0100 Subject: [PATCH 08/21] fix(rolling-content): defer __() to render and mark actions non-bulk --- src/wizards/rollingContent/modals/delete-confirm.tsx | 9 +++------ src/wizards/rollingContent/modals/edit-info.tsx | 9 +++------ src/wizards/rollingContent/views/all.tsx | 2 ++ 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/wizards/rollingContent/modals/delete-confirm.tsx b/src/wizards/rollingContent/modals/delete-confirm.tsx index 971509ba43..75dfd70934 100644 --- a/src/wizards/rollingContent/modals/delete-confirm.tsx +++ b/src/wizards/rollingContent/modals/delete-confirm.tsx @@ -13,11 +13,6 @@ import { Button, Modal } from '@wordpress/components'; type ItemType = 'rolling-content' | 'entry'; -const ITEM_NOUN: Record< ItemType, string > = { - 'rolling-content': __( 'rolling content', 'newspack-plugin' ), - entry: __( 'entry', 'newspack-plugin' ), -}; - export default function DeleteConfirmModal( { itemType, title, @@ -29,6 +24,8 @@ export default function DeleteConfirmModal( { onConfirm: () => void; onClose: () => void; } ) { + const itemNoun = itemType === 'entry' ? __( 'entry', 'newspack-plugin' ) : __( 'rolling content', 'newspack-plugin' ); + return (
diff --git a/src/wizards/rollingContent/modals/edit-info.tsx b/src/wizards/rollingContent/modals/edit-info.tsx index 4e9df27791..811d1c8cb5 100644 --- a/src/wizards/rollingContent/modals/edit-info.tsx +++ b/src/wizards/rollingContent/modals/edit-info.tsx @@ -13,18 +13,15 @@ import { Button, Modal } from '@wordpress/components'; type ItemType = 'rolling-content' | 'entry'; -const ITEM_NOUN: Record< ItemType, string > = { - 'rolling-content': __( 'rolling content', 'newspack-plugin' ), - entry: __( 'entry', 'newspack-plugin' ), -}; - export default function EditInfoModal( { itemType, title, onClose }: { itemType: ItemType; title: string; onClose: () => void } ) { + const itemNoun = itemType === 'entry' ? __( 'entry', 'newspack-plugin' ) : __( 'rolling content', 'newspack-plugin' ); + return ( ( ), @@ -137,6 +138,7 @@ export default function All() { id: 'delete', label: __( 'Delete', 'newspack-plugin' ), isDestructive: true, + supportsBulk: false, RenderModal: ( { items, closeModal } ) => ( Date: Wed, 13 May 2026 11:23:57 +0100 Subject: [PATCH 09/21] feat(rolling-content): add new entry info modal --- .../rollingContent/modals/add-entry-info.tsx | 40 +++++++++++++++++++ src/wizards/rollingContent/views/all.tsx | 7 ++++ 2 files changed, 47 insertions(+) create mode 100644 src/wizards/rollingContent/modals/add-entry-info.tsx diff --git a/src/wizards/rollingContent/modals/add-entry-info.tsx b/src/wizards/rollingContent/modals/add-entry-info.tsx new file mode 100644 index 0000000000..f90e76d38f --- /dev/null +++ b/src/wizards/rollingContent/modals/add-entry-info.tsx @@ -0,0 +1,40 @@ +/** + * Rolling Content demo — Add Entry info modal. + * + * Explains the implicit parent association: in a real implementation, + * choosing "Add New Entry" from a Rolling Content row (or from inside the + * Manage Entries modal) would open the block editor with the chosen + * Rolling Content preselected as the parent. + */ + +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { Button, Modal } from '@wordpress/components'; +import { createInterpolateElement } from '@wordpress/element'; + +export default function AddEntryInfoModal( { parentTitle, onClose }: { parentTitle: string; onClose: () => void } ) { + return ( + +

+ { createInterpolateElement( + sprintf( + /* translators: %s: parent rolling content title, wrapped in . */ + __( + 'Selecting "Add New Entry" would open the post editor with %s preselected as the parent. The link between an entry and its parent is implicit in how you enter the editor — no manual selection required.', + 'newspack-plugin' + ), + parentTitle + ), + { name: } + ) } +

+
+ +
+
+ ); +} diff --git a/src/wizards/rollingContent/views/all.tsx b/src/wizards/rollingContent/views/all.tsx index ea7f09178d..6f0e28cc46 100644 --- a/src/wizards/rollingContent/views/all.tsx +++ b/src/wizards/rollingContent/views/all.tsx @@ -19,6 +19,7 @@ import { WIZARD_STORE_NAMESPACE } from '../../../../packages/components/src/wiza import { ROLLING_CONTENTS } from '../data'; import EditInfoModal from '../modals/edit-info'; import DeleteConfirmModal from '../modals/delete-confirm'; +import AddEntryInfoModal from '../modals/add-entry-info'; const STATUS_LABELS: Record< RollingContentStatus, string > = { active: __( 'Active', 'newspack-plugin' ), @@ -134,6 +135,12 @@ export default function All() { ), }, + { + id: 'add-entry', + label: __( 'Add New Entry', 'newspack-plugin' ), + supportsBulk: false, + RenderModal: ( { items, closeModal } ) => , + }, { id: 'delete', label: __( 'Delete', 'newspack-plugin' ), From f7e3d76654b700f13b04b65e34721f0facc417b9 Mon Sep 17 00:00:00 2001 From: Thomas Guillot Date: Wed, 13 May 2026 11:43:00 +0100 Subject: [PATCH 10/21] feat(rolling-content): manage entries modal with nested dataviews --- .../rollingContent/modals/manage-entries.tsx | 226 ++++++++++++++++++ src/wizards/rollingContent/views/all.tsx | 19 ++ 2 files changed, 245 insertions(+) create mode 100644 src/wizards/rollingContent/modals/manage-entries.tsx diff --git a/src/wizards/rollingContent/modals/manage-entries.tsx b/src/wizards/rollingContent/modals/manage-entries.tsx new file mode 100644 index 0000000000..fbd2d2b1f9 --- /dev/null +++ b/src/wizards/rollingContent/modals/manage-entries.tsx @@ -0,0 +1,226 @@ +/** + * Rolling Content demo — Manage Entries modal. + * + * Full-screen modal containing a nested DataViews of all entries belonging + * to one parent Rolling Content. Reuses the shared Edit Info, Delete + * Confirm, and Add Entry Info modals for row-level and toolbar actions. + */ + +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { useState, useMemo } from '@wordpress/element'; +import { Button, Modal } from '@wordpress/components'; +import { filterSortAndPaginate } from '@wordpress/dataviews'; +import type { Action, Field, View } from '@wordpress/dataviews'; + +/** + * Internal dependencies + */ +import { DataViews } from '../../../../packages/components/src'; +import EditInfoModal from './edit-info'; +import DeleteConfirmModal from './delete-confirm'; +import AddEntryInfoModal from './add-entry-info'; + +const STATUS_LABELS: Record< EntryStatus, string > = { + published: __( 'Published', 'newspack-plugin' ), + draft: __( 'Draft', 'newspack-plugin' ), + scheduled: __( 'Scheduled', 'newspack-plugin' ), +}; + +const STATUS_COLORS: Record< EntryStatus, { bg: string; fg: string } > = { + published: { bg: '#d1fae5', fg: '#065f46' }, + draft: { bg: '#e5e7eb', fg: '#374151' }, + scheduled: { bg: '#dbeafe', fg: '#1e40af' }, +}; + +function StatusPill( { status }: { status: EntryStatus } ) { + const c = STATUS_COLORS[ status ]; + return ( + + { STATUS_LABELS[ status ] } + + ); +} + +const DEFAULT_VIEW: View = { + type: 'table', + page: 1, + perPage: 20, + sort: { field: 'date', direction: 'desc' }, + search: '', + fields: [ 'date', 'author', 'status', 'tags' ], + filters: [], + layout: {}, + titleField: 'title', + mediaField: 'featured_image', +}; + +export default function ManageEntriesModal( { + parent, + entries, + onEntriesChange, + onClose, +}: { + parent: RollingContent; + entries: Entry[]; + onEntriesChange: ( next: Entry[] ) => void; + onClose: () => void; +} ) { + const [ view, setView ] = useState< View >( DEFAULT_VIEW ); + const [ isAddingEntry, setIsAddingEntry ] = useState( false ); + + const fields: Field< Entry >[] = useMemo( + () => [ + { + id: 'featured_image', + label: __( 'Featured image', 'newspack-plugin' ), + type: 'media', + render: ( { item } ) => {, + enableSorting: false, + }, + { + id: 'title', + label: __( 'Title', 'newspack-plugin' ), + enableGlobalSearch: true, + getValue: ( { item } ) => item.title, + render: ( { item } ) => { item.title }, + }, + { + id: 'date', + label: __( 'Date', 'newspack-plugin' ), + getValue: ( { item } ) => item.date, + render: ( { item } ) => + new Date( item.date ).toLocaleDateString( undefined, { + year: 'numeric', + month: 'short', + day: 'numeric', + } ), + }, + { + id: 'author', + label: __( 'Author', 'newspack-plugin' ), + getValue: ( { item } ) => item.author, + }, + { + id: 'status', + label: __( 'Status', 'newspack-plugin' ), + getValue: ( { item } ) => item.status, + render: ( { item } ) => , + elements: ( Object.keys( STATUS_LABELS ) as EntryStatus[] ).map( value => ( { + value, + label: STATUS_LABELS[ value ], + } ) ), + }, + { + id: 'tags', + label: __( 'Tags', 'newspack-plugin' ), + getValue: ( { item } ) => item.tags.join( ', ' ), + render: ( { item } ) => ( + + { item.tags.map( tag => ( + + { tag } + + ) ) } + + ), + enableSorting: false, + }, + ], + [] + ); + + const actions: Action< Entry >[] = useMemo( + () => [ + { + id: 'edit', + label: __( 'Edit', 'newspack-plugin' ), + isPrimary: true, + supportsBulk: false, + RenderModal: ( { items, closeModal } ) => , + }, + { + id: 'delete', + label: __( 'Delete', 'newspack-plugin' ), + isDestructive: true, + supportsBulk: false, + RenderModal: ( { items, closeModal } ) => ( + onEntriesChange( entries.filter( e => e.id !== items[ 0 ].id ) ) } + onClose={ closeModal } + /> + ), + }, + ], + [ entries, onEntriesChange ] + ); + + const { data: processedData, paginationInfo } = useMemo( () => filterSortAndPaginate( entries, view, fields ), [ entries, view, fields ] ); + + return ( + +
+

+ { sprintf( + /* translators: %s: parent rolling content title. */ + __( 'Entries for: %s', 'newspack-plugin' ), + parent.title + ) } +

+ +
+ + String( item.id ) } + search + /> + + { isAddingEntry && setIsAddingEntry( false ) } /> } +
+ ); +} diff --git a/src/wizards/rollingContent/views/all.tsx b/src/wizards/rollingContent/views/all.tsx index 6f0e28cc46..08e96df43a 100644 --- a/src/wizards/rollingContent/views/all.tsx +++ b/src/wizards/rollingContent/views/all.tsx @@ -20,6 +20,7 @@ import { ROLLING_CONTENTS } from '../data'; import EditInfoModal from '../modals/edit-info'; import DeleteConfirmModal from '../modals/delete-confirm'; import AddEntryInfoModal from '../modals/add-entry-info'; +import ManageEntriesModal from '../modals/manage-entries'; const STATUS_LABELS: Record< RollingContentStatus, string > = { active: __( 'Active', 'newspack-plugin' ), @@ -135,6 +136,24 @@ export default function All() { ), }, + { + id: 'manage-entries', + label: __( 'Manage Entries', 'newspack-plugin' ), + supportsBulk: false, + RenderModal: ( { items, closeModal } ) => { + const parent = items[ 0 ]; + return ( + + setData( prev => prev.map( r => ( r.id === parent.id ? { ...r, entries: nextEntries } : r ) ) ) + } + onClose={ closeModal } + /> + ); + }, + }, { id: 'add-entry', label: __( 'Add New Entry', 'newspack-plugin' ), From 0f64a6f22a318b210ef7874298bf88304e361fee Mon Sep 17 00:00:00 2001 From: Thomas Guillot Date: Wed, 13 May 2026 11:49:17 +0100 Subject: [PATCH 11/21] fix(rolling-content): unify status pill and remove nested modal chrome --- .../rollingContent/components/status-pill.tsx | 34 +++++++++++++++++++ .../rollingContent/modals/manage-entries.tsx | 21 ++---------- src/wizards/rollingContent/views/all.tsx | 24 +++---------- 3 files changed, 40 insertions(+), 39 deletions(-) create mode 100644 src/wizards/rollingContent/components/status-pill.tsx diff --git a/src/wizards/rollingContent/components/status-pill.tsx b/src/wizards/rollingContent/components/status-pill.tsx new file mode 100644 index 0000000000..3ad9d812e9 --- /dev/null +++ b/src/wizards/rollingContent/components/status-pill.tsx @@ -0,0 +1,34 @@ +/** + * Rolling Content demo — shared status pill. + * + * Used by both the All Rolling Content view and the Manage Entries modal. + * Generic over the status union so each caller passes its own labels and colors. + */ + +type Palette = { bg: string; fg: string }; + +export default function StatusPill< S extends string >( { + status, + labels, + colors, +}: { + status: S; + labels: Record< S, string >; + colors: Record< S, Palette >; +} ) { + const c = colors[ status ]; + return ( + + { labels[ status ] } + + ); +} diff --git a/src/wizards/rollingContent/modals/manage-entries.tsx b/src/wizards/rollingContent/modals/manage-entries.tsx index fbd2d2b1f9..4c1059d1c1 100644 --- a/src/wizards/rollingContent/modals/manage-entries.tsx +++ b/src/wizards/rollingContent/modals/manage-entries.tsx @@ -19,6 +19,7 @@ import type { Action, Field, View } from '@wordpress/dataviews'; * Internal dependencies */ import { DataViews } from '../../../../packages/components/src'; +import StatusPill from '../components/status-pill'; import EditInfoModal from './edit-info'; import DeleteConfirmModal from './delete-confirm'; import AddEntryInfoModal from './add-entry-info'; @@ -35,24 +36,6 @@ const STATUS_COLORS: Record< EntryStatus, { bg: string; fg: string } > = { scheduled: { bg: '#dbeafe', fg: '#1e40af' }, }; -function StatusPill( { status }: { status: EntryStatus } ) { - const c = STATUS_COLORS[ status ]; - return ( - - { STATUS_LABELS[ status ] } - - ); -} - const DEFAULT_VIEW: View = { type: 'table', page: 1, @@ -116,7 +99,7 @@ export default function ManageEntriesModal( { id: 'status', label: __( 'Status', 'newspack-plugin' ), getValue: ( { item } ) => item.status, - render: ( { item } ) => , + render: ( { item } ) => , elements: ( Object.keys( STATUS_LABELS ) as EntryStatus[] ).map( value => ( { value, label: STATUS_LABELS[ value ], diff --git a/src/wizards/rollingContent/views/all.tsx b/src/wizards/rollingContent/views/all.tsx index 08e96df43a..297edef40a 100644 --- a/src/wizards/rollingContent/views/all.tsx +++ b/src/wizards/rollingContent/views/all.tsx @@ -17,6 +17,7 @@ import type { Action, Field, View } from '@wordpress/dataviews'; import { DataViews } from '../../../../packages/components/src'; import { WIZARD_STORE_NAMESPACE } from '../../../../packages/components/src/wizard/store'; import { ROLLING_CONTENTS } from '../data'; +import StatusPill from '../components/status-pill'; import EditInfoModal from '../modals/edit-info'; import DeleteConfirmModal from '../modals/delete-confirm'; import AddEntryInfoModal from '../modals/add-entry-info'; @@ -34,25 +35,6 @@ const STATUS_COLORS: Record< RollingContentStatus, { bg: string; fg: string } > scheduled: { bg: '#dbeafe', fg: '#1e40af' }, }; -function StatusPill( { status }: { status: RollingContentStatus } ) { - const c = STATUS_COLORS[ status ]; - return ( - - { STATUS_LABELS[ status ] } - - ); -} - const DEFAULT_VIEW: View = { type: 'table', page: 1, @@ -115,7 +97,7 @@ export default function All() { id: 'status', label: __( 'Status', 'newspack-plugin' ), getValue: ( { item } ) => item.status, - render: ( { item } ) => , + render: ( { item } ) => , elements: ( Object.keys( STATUS_LABELS ) as RollingContentStatus[] ).map( value => ( { value, label: STATUS_LABELS[ value ], @@ -140,6 +122,8 @@ export default function All() { id: 'manage-entries', label: __( 'Manage Entries', 'newspack-plugin' ), supportsBulk: false, + hideModalHeader: true, + modalSize: 'fill', RenderModal: ( { items, closeModal } ) => { const parent = items[ 0 ]; return ( From 49cfd4ec7deea7310f7548f47517bbf6f453e9f7 Mon Sep 17 00:00:00 2001 From: Thomas Guillot Date: Wed, 13 May 2026 11:53:57 +0100 Subject: [PATCH 12/21] fix(rolling-content): drop inner manage-entries modal wrapper --- .../rollingContent/modals/manage-entries.tsx | 15 ++++----------- src/wizards/rollingContent/views/all.tsx | 1 - 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/wizards/rollingContent/modals/manage-entries.tsx b/src/wizards/rollingContent/modals/manage-entries.tsx index 4c1059d1c1..612982f4f9 100644 --- a/src/wizards/rollingContent/modals/manage-entries.tsx +++ b/src/wizards/rollingContent/modals/manage-entries.tsx @@ -11,7 +11,7 @@ */ import { __, sprintf } from '@wordpress/i18n'; import { useState, useMemo } from '@wordpress/element'; -import { Button, Modal } from '@wordpress/components'; +import { Button } from '@wordpress/components'; import { filterSortAndPaginate } from '@wordpress/dataviews'; import type { Action, Field, View } from '@wordpress/dataviews'; @@ -53,6 +53,7 @@ export default function ManageEntriesModal( { parent, entries, onEntriesChange, + // eslint-disable-next-line @typescript-eslint/no-unused-vars onClose, }: { parent: RollingContent; @@ -162,15 +163,7 @@ export default function ManageEntriesModal( { const { data: processedData, paginationInfo } = useMemo( () => filterSortAndPaginate( entries, view, fields ), [ entries, view, fields ] ); return ( - + <>
{ isAddingEntry && setIsAddingEntry( false ) } /> } - + ); } diff --git a/src/wizards/rollingContent/views/all.tsx b/src/wizards/rollingContent/views/all.tsx index 297edef40a..28df89f361 100644 --- a/src/wizards/rollingContent/views/all.tsx +++ b/src/wizards/rollingContent/views/all.tsx @@ -122,7 +122,6 @@ export default function All() { id: 'manage-entries', label: __( 'Manage Entries', 'newspack-plugin' ), supportsBulk: false, - hideModalHeader: true, modalSize: 'fill', RenderModal: ( { items, closeModal } ) => { const parent = items[ 0 ]; From b281a75af45cbeaad58d69e944b35dbc1d7de48d Mon Sep 17 00:00:00 2001 From: Thomas Guillot Date: Wed, 13 May 2026 12:04:57 +0100 Subject: [PATCH 13/21] fix(rolling-content): render correct page id and inline view headers --- .../wizards/class-rolling-content-demo.php | 12 ++++ .../rollingContent/modals/manage-entries.tsx | 6 +- src/wizards/rollingContent/views/add.tsx | 52 +++++++-------- src/wizards/rollingContent/views/all.tsx | 66 ++++++++++--------- 4 files changed, 75 insertions(+), 61 deletions(-) diff --git a/includes/wizards/class-rolling-content-demo.php b/includes/wizards/class-rolling-content-demo.php index eea19a8a71..646c52f9fa 100644 --- a/includes/wizards/class-rolling-content-demo.php +++ b/includes/wizards/class-rolling-content-demo.php @@ -64,6 +64,18 @@ public function is_wizard_page() { return in_array( $page, [ $this->slug, self::SLUG_ADD ], true ); } + /** + * Render the container div. Override the parent so the id matches the current page slug, + * not `$this->slug`. Required because React mounts into `getElementById(pageParam)`. + */ + public function render_wizard() { + $page = filter_input( INPUT_GET, 'page', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); + $id = in_array( $page, [ $this->slug, self::SLUG_ADD ], true ) ? $page : $this->slug; + ?> +
+ , + RenderModal: ( { items, closeModal }: { items: Entry[]; closeModal: () => void } ) => ( + + ), }, { id: 'delete', label: __( 'Delete', 'newspack-plugin' ), isDestructive: true, supportsBulk: false, - RenderModal: ( { items, closeModal } ) => ( + RenderModal: ( { items, closeModal }: { items: Entry[]; closeModal: () => void } ) => ( { - setHeaderData( { - sectionName: __( 'Add Rolling Content', 'newspack-plugin' ), - actions: [ - { - type: 'secondary', - label: __( 'Back to All Rolling Content', 'newspack-plugin' ), - href: 'admin.php?page=newspack-rolling-content', - }, - ], - } ); - }, [ setHeaderData ] ); - return ( - - <> - + <> +
+

{ __( 'Add Rolling Content', 'newspack-plugin' ) }

+ +
+ + <> + + ); } diff --git a/src/wizards/rollingContent/views/all.tsx b/src/wizards/rollingContent/views/all.tsx index 28df89f361..864b08d172 100644 --- a/src/wizards/rollingContent/views/all.tsx +++ b/src/wizards/rollingContent/views/all.tsx @@ -6,8 +6,8 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { useState, useEffect, useMemo } from '@wordpress/element'; -import { useDispatch } from '@wordpress/data'; +import { useState, useMemo } from '@wordpress/element'; +import { Button } from '@wordpress/components'; import { filterSortAndPaginate } from '@wordpress/dataviews'; import type { Action, Field, View } from '@wordpress/dataviews'; @@ -15,7 +15,6 @@ import type { Action, Field, View } from '@wordpress/dataviews'; * Internal dependencies */ import { DataViews } from '../../../../packages/components/src'; -import { WIZARD_STORE_NAMESPACE } from '../../../../packages/components/src/wizard/store'; import { ROLLING_CONTENTS } from '../data'; import StatusPill from '../components/status-pill'; import EditInfoModal from '../modals/edit-info'; @@ -49,23 +48,9 @@ const DEFAULT_VIEW: View = { }; export default function All() { - const { setHeaderData } = useDispatch( WIZARD_STORE_NAMESPACE ); const [ data, setData ] = useState< RollingContent[] >( ROLLING_CONTENTS ); const [ view, setView ] = useState< View >( DEFAULT_VIEW ); - useEffect( () => { - setHeaderData( { - sectionName: __( 'Rolling Content', 'newspack-plugin' ), - actions: [ - { - type: 'primary', - label: __( 'Add Rolling Content', 'newspack-plugin' ), - href: 'admin.php?page=newspack-rolling-content-add', - }, - ], - } ); - }, [ setHeaderData ] ); - const fields: Field< RollingContent >[] = useMemo( () => [ { @@ -114,7 +99,7 @@ export default function All() { label: __( 'Edit', 'newspack-plugin' ), isPrimary: true, supportsBulk: false, - RenderModal: ( { items, closeModal } ) => ( + RenderModal: ( { items, closeModal }: { items: RollingContent[]; closeModal: () => void } ) => ( ), }, @@ -123,7 +108,7 @@ export default function All() { label: __( 'Manage Entries', 'newspack-plugin' ), supportsBulk: false, modalSize: 'fill', - RenderModal: ( { items, closeModal } ) => { + RenderModal: ( { items, closeModal }: { items: RollingContent[]; closeModal: () => void } ) => { const parent = items[ 0 ]; return ( , + RenderModal: ( { items, closeModal }: { items: RollingContent[]; closeModal: () => void } ) => ( + + ), }, { id: 'delete', label: __( 'Delete', 'newspack-plugin' ), isDestructive: true, supportsBulk: false, - RenderModal: ( { items, closeModal } ) => ( + RenderModal: ( { items, closeModal }: { items: RollingContent[]; closeModal: () => void } ) => ( filterSortAndPaginate( data, view, fields ), [ data, view, fields ] ); return ( - String( item.id ) } - search - /> + <> +
+

{ __( 'Rolling Content', 'newspack-plugin' ) }

+ +
+ String( item.id ) } + search + /> + ); } From 0fe4bdd6b9813a737eae0bfa46fb7d0836664e83 Mon Sep 17 00:00:00 2001 From: Thomas Guillot Date: Wed, 13 May 2026 14:23:10 +0100 Subject: [PATCH 14/21] refactor(rolling-content): swap status pills for icons and plain text --- .../rollingContent/components/status-pill.tsx | 30 ++++++++----------- .../rollingContent/modals/manage-entries.tsx | 11 +++---- src/wizards/rollingContent/views/all.tsx | 11 +++---- 3 files changed, 24 insertions(+), 28 deletions(-) diff --git a/src/wizards/rollingContent/components/status-pill.tsx b/src/wizards/rollingContent/components/status-pill.tsx index 3ad9d812e9..4d331ca61e 100644 --- a/src/wizards/rollingContent/components/status-pill.tsx +++ b/src/wizards/rollingContent/components/status-pill.tsx @@ -1,34 +1,28 @@ /** - * Rolling Content demo — shared status pill. + * Rolling Content demo — shared status indicator. * - * Used by both the All Rolling Content view and the Manage Entries modal. - * Generic over the status union so each caller passes its own labels and colors. + * Renders a small icon + label for an item's status. Each caller passes its + * own labels and icons keyed by its status union. */ -type Palette = { bg: string; fg: string }; +/** + * WordPress dependencies + */ +import { Icon } from '@wordpress/components'; export default function StatusPill< S extends string >( { status, labels, - colors, + icons, }: { status: S; labels: Record< S, string >; - colors: Record< S, Palette >; + icons: Record< S, JSX.Element >; } ) { - const c = colors[ status ]; return ( - - { labels[ status ] } + + + { labels[ status ] } ); } diff --git a/src/wizards/rollingContent/modals/manage-entries.tsx b/src/wizards/rollingContent/modals/manage-entries.tsx index 3f68973af7..e1d002762e 100644 --- a/src/wizards/rollingContent/modals/manage-entries.tsx +++ b/src/wizards/rollingContent/modals/manage-entries.tsx @@ -14,6 +14,7 @@ import { useState, useMemo } from '@wordpress/element'; import { Button } from '@wordpress/components'; import { filterSortAndPaginate } from '@wordpress/dataviews'; import type { Action, Field, View } from '@wordpress/dataviews'; +import { published, drafts, scheduled } from '@wordpress/icons'; /** * Internal dependencies @@ -30,10 +31,10 @@ const STATUS_LABELS: Record< EntryStatus, string > = { scheduled: __( 'Scheduled', 'newspack-plugin' ), }; -const STATUS_COLORS: Record< EntryStatus, { bg: string; fg: string } > = { - published: { bg: '#d1fae5', fg: '#065f46' }, - draft: { bg: '#e5e7eb', fg: '#374151' }, - scheduled: { bg: '#dbeafe', fg: '#1e40af' }, +const STATUS_ICONS: Record< EntryStatus, JSX.Element > = { + published, + draft: drafts, + scheduled, }; const DEFAULT_VIEW: View = { @@ -100,7 +101,7 @@ export default function ManageEntriesModal( { id: 'status', label: __( 'Status', 'newspack-plugin' ), getValue: ( { item } ) => item.status, - render: ( { item } ) => , + render: ( { item } ) => , elements: ( Object.keys( STATUS_LABELS ) as EntryStatus[] ).map( value => ( { value, label: STATUS_LABELS[ value ], diff --git a/src/wizards/rollingContent/views/all.tsx b/src/wizards/rollingContent/views/all.tsx index 864b08d172..919acaa148 100644 --- a/src/wizards/rollingContent/views/all.tsx +++ b/src/wizards/rollingContent/views/all.tsx @@ -10,6 +10,7 @@ import { useState, useMemo } from '@wordpress/element'; import { Button } from '@wordpress/components'; import { filterSortAndPaginate } from '@wordpress/dataviews'; import type { Action, Field, View } from '@wordpress/dataviews'; +import { published, scheduled, archive } from '@wordpress/icons'; /** * Internal dependencies @@ -28,10 +29,10 @@ const STATUS_LABELS: Record< RollingContentStatus, string > = { scheduled: __( 'Scheduled', 'newspack-plugin' ), }; -const STATUS_COLORS: Record< RollingContentStatus, { bg: string; fg: string } > = { - active: { bg: '#d1fae5', fg: '#065f46' }, - archived: { bg: '#e5e7eb', fg: '#374151' }, - scheduled: { bg: '#dbeafe', fg: '#1e40af' }, +const STATUS_ICONS: Record< RollingContentStatus, JSX.Element > = { + active: published, + archived: archive, + scheduled, }; const DEFAULT_VIEW: View = { @@ -82,7 +83,7 @@ export default function All() { id: 'status', label: __( 'Status', 'newspack-plugin' ), getValue: ( { item } ) => item.status, - render: ( { item } ) => , + render: ( { item } ) => , elements: ( Object.keys( STATUS_LABELS ) as RollingContentStatus[] ).map( value => ( { value, label: STATUS_LABELS[ value ], From 8468161f2c39f31ddbb2316f55ecd6d022ef4739 Mon Sep 17 00:00:00 2001 From: Thomas Guillot Date: Wed, 13 May 2026 14:33:20 +0100 Subject: [PATCH 15/21] fix(rolling-content): restore Newspack chrome, inline actions, conditional menu --- .../wizards/class-rolling-content-demo.php | 65 +++------ .../rollingContent/modals/manage-entries.tsx | 15 +- src/wizards/rollingContent/views/add.tsx | 62 +++++---- src/wizards/rollingContent/views/all.tsx | 129 ++++++++++++------ 4 files changed, 153 insertions(+), 118 deletions(-) diff --git a/includes/wizards/class-rolling-content-demo.php b/includes/wizards/class-rolling-content-demo.php index 646c52f9fa..5879cb750d 100644 --- a/includes/wizards/class-rolling-content-demo.php +++ b/includes/wizards/class-rolling-content-demo.php @@ -79,29 +79,13 @@ public function render_wizard() { /** * Register admin pages. * - * Both pages are ALWAYS registered as hidden so direct URL navigation works. - * When the user is on either page, a visible top-level menu with sub-items is - * also registered so the demo's IA can be observed in the sidebar. + * The visible top-level menu and its sub-items are only registered when the + * user is actually on one of the demo pages. URL access still works because + * `admin_menu` fires before WP validates the slug — `$_GET['page']` is set + * by the time `is_wizard_page()` runs, the menu registers, and WP accepts + * the page. */ public function add_page() { - // Always register both slugs as hidden so direct URL access works. - add_submenu_page( - 'hidden', - $this->get_name(), - $this->get_name(), - $this->capability, - $this->slug, - [ $this, 'render_wizard' ] - ); - add_submenu_page( - 'hidden', - __( 'Add Rolling Content', 'newspack-plugin' ), - __( 'Add Rolling Content', 'newspack-plugin' ), - $this->capability, - self::SLUG_ADD, - [ $this, 'render_wizard' ] - ); - if ( ! $this->is_wizard_page() ) { return; } @@ -140,36 +124,27 @@ public function add_page() { /** * Enqueue scripts and styles. * - * The parent class's slug check is too strict (single slug). We replicate the - * parts we need so both demo pages get the shared `newspack-wizards` chrome. + * Delegate to the parent class so the standard wizard chrome (newspack_urls, + * newspack_aux_data, newspack-wizards registration, etc.) gets set up. + * The parent's enqueue function checks `$_GET['page'] === $this->slug` and + * bails otherwise; for the SLUG_ADD page we temporarily spoof `page` so the + * parent runs, then restore it. */ public function enqueue_scripts_and_styles() { if ( ! $this->is_wizard_page() ) { return; } - Newspack::load_common_assets(); - - // Data carrier script (no source). - wp_register_script( 'newspack_data', '', [], '1.0', false ); - wp_localize_script( - 'newspack_data', - 'newspack_urls', - [ - 'public_path' => Newspack::plugin_url() . '/dist/', - 'site' => get_site_url(), - ] - ); - wp_enqueue_script( 'newspack_data' ); - - // Shared wizards bundle (contains the components map our routes are added to). - wp_register_script( - 'newspack-wizards', - Newspack::plugin_url() . '/dist/wizards.js', - $this->get_script_dependencies(), - NEWSPACK_PLUGIN_VERSION, - true - ); + // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- $_GET['page'] is only stored briefly and restored verbatim. + $original_page = isset( $_GET['page'] ) ? $_GET['page'] : null; + $_GET['page'] = $this->slug; + parent::enqueue_scripts_and_styles(); + if ( null === $original_page ) { + unset( $_GET['page'] ); + } else { + $_GET['page'] = $original_page; + } + wp_enqueue_script( 'newspack-wizards' ); } } diff --git a/src/wizards/rollingContent/modals/manage-entries.tsx b/src/wizards/rollingContent/modals/manage-entries.tsx index e1d002762e..5fdbda2f13 100644 --- a/src/wizards/rollingContent/modals/manage-entries.tsx +++ b/src/wizards/rollingContent/modals/manage-entries.tsx @@ -11,7 +11,7 @@ */ import { __, sprintf } from '@wordpress/i18n'; import { useState, useMemo } from '@wordpress/element'; -import { Button } from '@wordpress/components'; +import { Button, Modal } from '@wordpress/components'; import { filterSortAndPaginate } from '@wordpress/dataviews'; import type { Action, Field, View } from '@wordpress/dataviews'; import { published, drafts, scheduled } from '@wordpress/icons'; @@ -54,7 +54,6 @@ export default function ManageEntriesModal( { parent, entries, onEntriesChange, - // eslint-disable-next-line @typescript-eslint/no-unused-vars onClose, }: { parent: RollingContent; @@ -166,7 +165,15 @@ export default function ManageEntriesModal( { const { data: processedData, paginationInfo } = useMemo( () => filterSortAndPaginate( entries, view, fields ), [ entries, view, fields ] ); return ( - <> +
{ isAddingEntry && setIsAddingEntry( false ) } /> } - + ); } diff --git a/src/wizards/rollingContent/views/add.tsx b/src/wizards/rollingContent/views/add.tsx index 14abf637a1..3a329acc05 100644 --- a/src/wizards/rollingContent/views/add.tsx +++ b/src/wizards/rollingContent/views/add.tsx @@ -6,38 +6,50 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { Button } from '@wordpress/components'; +import { useEffect } from '@wordpress/element'; +import { useDispatch } from '@wordpress/data'; /** * Internal dependencies */ +import { Wizard } from '../../../../packages/components/src'; +import { WIZARD_STORE_NAMESPACE } from '../../../../packages/components/src/wizard/store'; import WizardSection from '../../wizards-section'; +function AddRollingContent() { + const { setHeaderData } = useDispatch( WIZARD_STORE_NAMESPACE ); + + useEffect( () => { + setHeaderData( { + sectionName: __( 'Add Rolling Content', 'newspack-plugin' ), + actions: [ + { + type: 'secondary', + label: __( 'Back to All Rolling Content', 'newspack-plugin' ), + href: 'admin.php?page=newspack-rolling-content', + }, + ], + } ); + }, [ setHeaderData ] ); + + return ( + + <> + + ); +} + export default function Add() { return ( - <> -
-

{ __( 'Add Rolling Content', 'newspack-plugin' ) }

- -
- - <> - - + } ] } + /> ); } diff --git a/src/wizards/rollingContent/views/all.tsx b/src/wizards/rollingContent/views/all.tsx index 919acaa148..102e74bf1b 100644 --- a/src/wizards/rollingContent/views/all.tsx +++ b/src/wizards/rollingContent/views/all.tsx @@ -6,8 +6,9 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { useState, useMemo } from '@wordpress/element'; +import { useState, useMemo, useEffect } from '@wordpress/element'; import { Button } from '@wordpress/components'; +import { useDispatch } from '@wordpress/data'; import { filterSortAndPaginate } from '@wordpress/dataviews'; import type { Action, Field, View } from '@wordpress/dataviews'; import { published, scheduled, archive } from '@wordpress/icons'; @@ -15,7 +16,8 @@ import { published, scheduled, archive } from '@wordpress/icons'; /** * Internal dependencies */ -import { DataViews } from '../../../../packages/components/src'; +import { DataViews, Wizard } from '../../../../packages/components/src'; +import { WIZARD_STORE_NAMESPACE } from '../../../../packages/components/src/wizard/store'; import { ROLLING_CONTENTS } from '../data'; import StatusPill from '../components/status-pill'; import EditInfoModal from '../modals/edit-info'; @@ -41,16 +43,32 @@ const DEFAULT_VIEW: View = { perPage: 20, sort: { field: 'date', direction: 'desc' }, search: '', - fields: [ 'date', 'status' ], + fields: [ 'date', 'entries_count', 'last_updated', 'status', 'inline_actions' ], filters: [], layout: {}, titleField: 'title', mediaField: 'featured_image', }; -export default function All() { +function AllRollingContent() { + const { setHeaderData } = useDispatch( WIZARD_STORE_NAMESPACE ); const [ data, setData ] = useState< RollingContent[] >( ROLLING_CONTENTS ); const [ view, setView ] = useState< View >( DEFAULT_VIEW ); + const [ managingEntriesFor, setManagingEntriesFor ] = useState< RollingContent | null >( null ); + const [ addingEntryFor, setAddingEntryFor ] = useState< RollingContent | null >( null ); + + useEffect( () => { + setHeaderData( { + sectionName: __( 'Rolling Content', 'newspack-plugin' ), + actions: [ + { + type: 'primary', + label: __( 'Add Rolling Content', 'newspack-plugin' ), + href: 'admin.php?page=newspack-rolling-content-add', + }, + ], + } ); + }, [ setHeaderData ] ); const fields: Field< RollingContent >[] = useMemo( () => [ @@ -79,6 +97,33 @@ export default function All() { day: 'numeric', } ), }, + { + id: 'entries_count', + label: __( 'Entries', 'newspack-plugin' ), + getValue: ( { item } ) => item.entries.length, + render: ( { item } ) => item.entries.length, + }, + { + id: 'last_updated', + label: __( 'Last updated', 'newspack-plugin' ), + getValue: ( { item } ) => { + if ( item.entries.length === 0 ) { + return ''; + } + return Math.max( ...item.entries.map( e => new Date( e.date ).getTime() ) ); + }, + render: ( { item } ) => { + if ( item.entries.length === 0 ) { + return '—'; + } + const ms = Math.max( ...item.entries.map( e => new Date( e.date ).getTime() ) ); + return new Date( ms ).toLocaleDateString( undefined, { + year: 'numeric', + month: 'short', + day: 'numeric', + } ); + }, + }, { id: 'status', label: __( 'Status', 'newspack-plugin' ), @@ -89,6 +134,22 @@ export default function All() { label: STATUS_LABELS[ value ], } ) ), }, + { + id: 'inline_actions', + label: '', + enableSorting: false, + enableHiding: false, + render: ( { item } ) => ( +
+ + +
+ ), + }, ], [] ); @@ -104,33 +165,6 @@ export default function All() { ), }, - { - id: 'manage-entries', - label: __( 'Manage Entries', 'newspack-plugin' ), - supportsBulk: false, - modalSize: 'fill', - RenderModal: ( { items, closeModal }: { items: RollingContent[]; closeModal: () => void } ) => { - const parent = items[ 0 ]; - return ( - - setData( prev => prev.map( r => ( r.id === parent.id ? { ...r, entries: nextEntries } : r ) ) ) - } - onClose={ closeModal } - /> - ); - }, - }, - { - id: 'add-entry', - label: __( 'Add New Entry', 'newspack-plugin' ), - supportsBulk: false, - RenderModal: ( { items, closeModal }: { items: RollingContent[]; closeModal: () => void } ) => ( - - ), - }, { id: 'delete', label: __( 'Delete', 'newspack-plugin' ), @@ -153,19 +187,6 @@ export default function All() { return ( <> -
-

{ __( 'Rolling Content', 'newspack-plugin' ) }

- -
String( item.id ) } search /> + { managingEntriesFor && ( + + setData( prev => prev.map( r => ( r.id === managingEntriesFor.id ? { ...r, entries: nextEntries } : r ) ) ) + } + onClose={ () => setManagingEntriesFor( null ) } + /> + ) } + { addingEntryFor && setAddingEntryFor( null ) } /> } ); } + +export default function All() { + return ( + } ] } + /> + ); +} From 69ac184710c102ee8c6935202cb6c2c2c7edf652 Mon Sep 17 00:00:00 2001 From: Thomas Guillot Date: Wed, 13 May 2026 14:35:33 +0100 Subject: [PATCH 16/21] fix(rolling-content): set all view section to fullWidth --- src/wizards/rollingContent/views/all.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wizards/rollingContent/views/all.tsx b/src/wizards/rollingContent/views/all.tsx index 102e74bf1b..a36544638c 100644 --- a/src/wizards/rollingContent/views/all.tsx +++ b/src/wizards/rollingContent/views/all.tsx @@ -217,7 +217,7 @@ export default function All() { return ( } ] } + sections={ [ { path: '/', render: () => , fullWidth: true } ] } /> ); } From ba14ad12f90c8d361282960d1bea5bfb42687aef Mon Sep 17 00:00:00 2001 From: Thomas Guillot Date: Wed, 13 May 2026 14:36:33 +0100 Subject: [PATCH 17/21] fix(rolling-content): merge manage and add buttons into entries column --- src/wizards/rollingContent/views/all.tsx | 30 ++++++++++-------------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/src/wizards/rollingContent/views/all.tsx b/src/wizards/rollingContent/views/all.tsx index a36544638c..16f3daa268 100644 --- a/src/wizards/rollingContent/views/all.tsx +++ b/src/wizards/rollingContent/views/all.tsx @@ -43,7 +43,7 @@ const DEFAULT_VIEW: View = { perPage: 20, sort: { field: 'date', direction: 'desc' }, search: '', - fields: [ 'date', 'entries_count', 'last_updated', 'status', 'inline_actions' ], + fields: [ 'date', 'entries_count', 'last_updated', 'status' ], filters: [], layout: {}, titleField: 'title', @@ -101,7 +101,17 @@ function AllRollingContent() { id: 'entries_count', label: __( 'Entries', 'newspack-plugin' ), getValue: ( { item } ) => item.entries.length, - render: ( { item } ) => item.entries.length, + render: ( { item } ) => ( +
+ { item.entries.length } + + +
+ ), }, { id: 'last_updated', @@ -134,22 +144,6 @@ function AllRollingContent() { label: STATUS_LABELS[ value ], } ) ), }, - { - id: 'inline_actions', - label: '', - enableSorting: false, - enableHiding: false, - render: ( { item } ) => ( -
- - -
- ), - }, ], [] ); From 673c5ff5b1ce35c9ce11ba66ff531e82e7ef5d72 Mon Sep 17 00:00:00 2001 From: Thomas Guillot Date: Wed, 13 May 2026 14:58:07 +0100 Subject: [PATCH 18/21] fix(rolling-content): use $_GET directly and dedupe header text --- .../wizards/class-rolling-content-demo.php | 62 ++++++++++++++----- src/wizards/rollingContent/views/add.tsx | 7 +-- src/wizards/rollingContent/views/all.tsx | 2 +- 3 files changed, 47 insertions(+), 24 deletions(-) diff --git a/includes/wizards/class-rolling-content-demo.php b/includes/wizards/class-rolling-content-demo.php index 5879cb750d..9095009a48 100644 --- a/includes/wizards/class-rolling-content-demo.php +++ b/includes/wizards/class-rolling-content-demo.php @@ -60,7 +60,8 @@ public function get_name() { * @return bool */ public function is_wizard_page() { - $page = filter_input( INPUT_GET, 'page', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; return in_array( $page, [ $this->slug, self::SLUG_ADD ], true ); } @@ -69,7 +70,8 @@ public function is_wizard_page() { * not `$this->slug`. Required because React mounts into `getElementById(pageParam)`. */ public function render_wizard() { - $page = filter_input( INPUT_GET, 'page', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; $id = in_array( $page, [ $this->slug, self::SLUG_ADD ], true ) ? $page : $this->slug; ?>
@@ -124,27 +126,53 @@ public function add_page() { /** * Enqueue scripts and styles. * - * Delegate to the parent class so the standard wizard chrome (newspack_urls, - * newspack_aux_data, newspack-wizards registration, etc.) gets set up. - * The parent's enqueue function checks `$_GET['page'] === $this->slug` and - * bails otherwise; for the SLUG_ADD page we temporarily spoof `page` so the - * parent runs, then restore it. + * Replicates the relevant parts of the parent class's enqueue logic. We can't + * just call `parent::enqueue_scripts_and_styles()` because its slug check uses + * `filter_input(INPUT_GET, 'page')`, which reads from the frozen request state + * and ignores any local `$_GET` mutations — so it would bail on the SLUG_ADD + * page. */ public function enqueue_scripts_and_styles() { if ( ! $this->is_wizard_page() ) { return; } - // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- $_GET['page'] is only stored briefly and restored verbatim. - $original_page = isset( $_GET['page'] ) ? $_GET['page'] : null; - $_GET['page'] = $this->slug; - parent::enqueue_scripts_and_styles(); - if ( null === $original_page ) { - unset( $_GET['page'] ); - } else { - $_GET['page'] = $original_page; - } - + Newspack::load_common_assets(); + + // Data carrier (no source script). + wp_register_script( 'newspack_data', '', [], '1.0', false ); + + $plugin_data = get_plugin_data( NEWSPACK_PLUGIN_FILE ); + $urls = [ + 'dashboard' => Wizards::get_url( 'newspack-dashboard' ), + 'public_path' => Newspack::plugin_url() . '/dist/', + 'bloginfo' => [ 'name' => get_bloginfo( 'name' ) ], + 'plugin_version' => [ 'label' => $plugin_data['Name'] . ' ' . $plugin_data['Version'] ], + 'homepage' => get_edit_post_link( get_option( 'page_on_front', false ) ), + 'site' => get_site_url(), + 'support' => esc_url( 'https://help.newspack.com/' ), + 'support_email' => false, + ]; + + $aux_data = [ + 'is_e2e' => Starter_Content::is_e2e(), + 'is_debug_mode' => Newspack::is_debug_mode(), + 'has_completed_setup' => get_option( NEWSPACK_SETUP_COMPLETE ), + 'site_title' => get_option( 'blogname' ), + 'is_managed' => method_exists( 'Newspack_Manager', 'is_connected_to_manager' ) && \Newspack_Manager::is_connected_to_manager(), + ]; + + wp_localize_script( 'newspack_data', 'newspack_urls', $urls ); + wp_localize_script( 'newspack_data', 'newspack_aux_data', $aux_data ); + wp_enqueue_script( 'newspack_data' ); + + wp_register_script( + 'newspack-wizards', + Newspack::plugin_url() . '/dist/wizards.js', + $this->get_script_dependencies(), + NEWSPACK_PLUGIN_VERSION, + true + ); wp_enqueue_script( 'newspack-wizards' ); } } diff --git a/src/wizards/rollingContent/views/add.tsx b/src/wizards/rollingContent/views/add.tsx index 3a329acc05..67f145cdee 100644 --- a/src/wizards/rollingContent/views/add.tsx +++ b/src/wizards/rollingContent/views/add.tsx @@ -46,10 +46,5 @@ function AddRollingContent() { } export default function Add() { - return ( - } ] } - /> - ); + return } ] } />; } diff --git a/src/wizards/rollingContent/views/all.tsx b/src/wizards/rollingContent/views/all.tsx index 16f3daa268..46009cc7aa 100644 --- a/src/wizards/rollingContent/views/all.tsx +++ b/src/wizards/rollingContent/views/all.tsx @@ -210,7 +210,7 @@ function AllRollingContent() { export default function All() { return ( , fullWidth: true } ] } /> ); From e242b399a20ac5c07d523f70ec72d14131dd6b62 Mon Sep 17 00:00:00 2001 From: Thomas Guillot Date: Wed, 13 May 2026 15:50:08 +0100 Subject: [PATCH 19/21] fix(rolling-content): anchor menu position so it's not buried at the bottom --- includes/wizards/class-rolling-content-demo.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/includes/wizards/class-rolling-content-demo.php b/includes/wizards/class-rolling-content-demo.php index 9095009a48..f8d77f360e 100644 --- a/includes/wizards/class-rolling-content-demo.php +++ b/includes/wizards/class-rolling-content-demo.php @@ -97,13 +97,17 @@ public function add_page() { base64_encode( Newspack_UI_Icons::get_svg( 'collections' ) ) ); + // Anchor the menu near the top of the sidebar (just after Dashboard at pos 2) + // so the demo is easy to find — `add_menu_page` defaults to appending, which + // pushes the demo below every other plugin's menu. add_menu_page( $this->get_name(), $this->get_name(), $this->capability, $this->slug, [ $this, 'render_wizard' ], - $icon + $icon, + '2.5' ); add_submenu_page( $this->slug, From 41f12d5b49033e4816c4165ab8e5cd16a7dd3214 Mon Sep 17 00:00:00 2001 From: Thomas Guillot Date: Wed, 13 May 2026 16:04:11 +0100 Subject: [PATCH 20/21] fix(rolling-content): force-highlight the active menu via parent_file filter --- .../wizards/class-rolling-content-demo.php | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/includes/wizards/class-rolling-content-demo.php b/includes/wizards/class-rolling-content-demo.php index f8d77f360e..8b7f0985e0 100644 --- a/includes/wizards/class-rolling-content-demo.php +++ b/includes/wizards/class-rolling-content-demo.php @@ -92,6 +92,9 @@ public function add_page() { return; } + add_filter( 'parent_file', [ $this, 'set_parent_file' ] ); + add_filter( 'submenu_file', [ $this, 'set_submenu_file' ] ); + $icon = sprintf( 'data:image/svg+xml;base64,%s', base64_encode( Newspack_UI_Icons::get_svg( 'collections' ) ) @@ -127,6 +130,30 @@ public function add_page() { ); } + /** + * Force-highlight the top-level menu when on any rolling-content page. + * Required because WP's automatic highlighting can lose the trail through + * the conditional menu registration. + * + * @param string $parent_file The current parent file. + * @return string + */ + public function set_parent_file( $parent_file ) { + return $this->slug; + } + + /** + * Highlight the correct submenu item for the current page. + * + * @param string $submenu_file The current submenu file. + * @return string + */ + public function set_submenu_file( $submenu_file ) { + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; + return in_array( $page, [ $this->slug, self::SLUG_ADD ], true ) ? $page : $submenu_file; + } + /** * Enqueue scripts and styles. * From abedb05f92ebd82129e69a44dc6576fd5465c58c Mon Sep 17 00:00:00 2001 From: Thomas Guillot Date: Thu, 14 May 2026 17:43:38 +0100 Subject: [PATCH 21/21] fix(rolling-content): address copilot review on modal state and sort types --- src/wizards/rollingContent/views/all.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/wizards/rollingContent/views/all.tsx b/src/wizards/rollingContent/views/all.tsx index 46009cc7aa..a7ef4c81e6 100644 --- a/src/wizards/rollingContent/views/all.tsx +++ b/src/wizards/rollingContent/views/all.tsx @@ -54,8 +54,11 @@ function AllRollingContent() { const { setHeaderData } = useDispatch( WIZARD_STORE_NAMESPACE ); const [ data, setData ] = useState< RollingContent[] >( ROLLING_CONTENTS ); const [ view, setView ] = useState< View >( DEFAULT_VIEW ); - const [ managingEntriesFor, setManagingEntriesFor ] = useState< RollingContent | null >( null ); - const [ addingEntryFor, setAddingEntryFor ] = useState< RollingContent | null >( null ); + const [ managingEntriesForId, setManagingEntriesForId ] = useState< number | null >( null ); + const [ addingEntryForId, setAddingEntryForId ] = useState< number | null >( null ); + + const managingEntriesFor = managingEntriesForId === null ? null : data.find( r => r.id === managingEntriesForId ) ?? null; + const addingEntryFor = addingEntryForId === null ? null : data.find( r => r.id === addingEntryForId ) ?? null; useEffect( () => { setHeaderData( { @@ -104,10 +107,10 @@ function AllRollingContent() { render: ( { item } ) => (
{ item.entries.length } - -
@@ -118,7 +121,7 @@ function AllRollingContent() { label: __( 'Last updated', 'newspack-plugin' ), getValue: ( { item } ) => { if ( item.entries.length === 0 ) { - return ''; + return 0; } return Math.max( ...item.entries.map( e => new Date( e.date ).getTime() ) ); }, @@ -199,10 +202,10 @@ function AllRollingContent() { onEntriesChange={ nextEntries => setData( prev => prev.map( r => ( r.id === managingEntriesFor.id ? { ...r, entries: nextEntries } : r ) ) ) } - onClose={ () => setManagingEntriesFor( null ) } + onClose={ () => setManagingEntriesForId( null ) } /> ) } - { addingEntryFor && setAddingEntryFor( null ) } /> } + { addingEntryFor && setAddingEntryForId( null ) } /> } ); }