From cfe0fb5f2f03d0ed2eb4232846e0f4a246723c17 Mon Sep 17 00:00:00 2001 From: Mikael Korpela Date: Fri, 22 May 2026 12:14:31 +0300 Subject: [PATCH 1/2] Add dashboard news widget. Extracted from update/dashboard-widget-render-suspense for standalone review. --- lib/experimental/dashboard-widgets/load.php | 18 +++ widgets/news/list.tsx | 83 +++++++++++ widgets/news/render.tsx | 153 ++++++++++++++++++++ widgets/news/style.module.css | 30 ++++ widgets/news/widget.json | 6 + widgets/news/widget.ts | 6 + 6 files changed, 296 insertions(+) create mode 100644 widgets/news/list.tsx create mode 100644 widgets/news/render.tsx create mode 100644 widgets/news/style.module.css create mode 100644 widgets/news/widget.json create mode 100644 widgets/news/widget.ts diff --git a/lib/experimental/dashboard-widgets/load.php b/lib/experimental/dashboard-widgets/load.php index 635c18af15dbcd..3079bb7f7b006e 100644 --- a/lib/experimental/dashboard-widgets/load.php +++ b/lib/experimental/dashboard-widgets/load.php @@ -6,6 +6,8 @@ */ add_action( 'admin_menu', 'gutenberg_register_dashboard_widgets_menu' ); +add_action( 'dashboard_init', 'gutenberg_dashboard_widgets_register_news_widget' ); +add_action( 'dashboard-wp-admin_init', 'gutenberg_dashboard_widgets_register_news_widget' ); /** * Registers the Dashboard Widgets menu item. @@ -21,3 +23,19 @@ function gutenberg_register_dashboard_widgets_menu() { 1 ); } + +/** + * Wires the `news` widget as a dynamic dep of the dashboard module so it + * lands in the import map and React.lazy in the dashboard stage can resolve it. + */ +function gutenberg_dashboard_widgets_register_news_widget() { + $widget_module = 'wp/widgets/news/render'; + + if ( function_exists( 'gutenberg_register_dashboard_route' ) ) { + gutenberg_register_dashboard_route( '/__widget_news', $widget_module ); + } + + if ( function_exists( 'gutenberg_register_dashboard_wp_admin_route' ) ) { + gutenberg_register_dashboard_wp_admin_route( '/__widget_news', $widget_module ); + } +} diff --git a/widgets/news/list.tsx b/widgets/news/list.tsx new file mode 100644 index 00000000000000..349994e8130d6f --- /dev/null +++ b/widgets/news/list.tsx @@ -0,0 +1,83 @@ +/** + * WordPress dependencies + */ +import type { ReactNode } from 'react'; +import { Link, Stack, Text } from '@wordpress/ui'; + +/** + * Internal dependencies + */ +import styles from './style.module.css'; + +export type ListItem = { + id: string; + title: string; + url: string; + meta?: string[]; + icon?: ReactNode; +}; + +export default function List( { + items, + empty, +}: { + items: ListItem[]; + empty?: ReactNode; +} ) { + if ( items.length === 0 ) { + return empty ?? null; + } + + return ( + + { items.map( ( item ) => ( + + { item.icon && ( +
+ { item.icon } +
+ ) } + + + { item.url ? ( + + { item.title } + + ) : ( + item.title + ) } + + { item.meta && item.meta.length > 0 && ( + + { item.meta.map( ( metaItem, index ) => ( + + { index > 0 && '· ' } + { metaItem } + + ) ) } + + ) } + +
+ ) ) } +
+ ); +} diff --git a/widgets/news/render.tsx b/widgets/news/render.tsx new file mode 100644 index 00000000000000..8f3100a79983b7 --- /dev/null +++ b/widgets/news/render.tsx @@ -0,0 +1,153 @@ +/** + * WordPress dependencies + */ +import { useState, useEffect } from '@wordpress/element'; +import { dateI18n } from '@wordpress/date'; +import { __, _x } from '@wordpress/i18n'; +import { globe, postList, wordpress } from '@wordpress/icons'; +import { Spinner } from '@wordpress/components'; +/* eslint-disable @wordpress/use-recommended-components */ +import { Card, EmptyState, Icon, Link, Stack } from '@wordpress/ui'; +/* eslint-enable @wordpress/use-recommended-components */ + +/** + * Internal dependencies + */ +import List, { type ListItem } from './list'; + +interface NewsPost { + id: number; + title: { rendered: string }; + link: string; + date: string; +} + +interface NewsFeed { + key: string; + label: string; + siteUrl: string; + posts: NewsPost[]; +} + +const NEWS_FEEDS = [ + { + key: 'news', + label: __( 'WordPress Blog' ), + siteUrl: _x( + 'https://wordpress.org/news/', + 'Events and News dashboard widget' + ), + apiUrl: 'https://wordpress.org/news/wp-json/wp/v2/posts?per_page=3&_fields=id,title,link,date', + }, + { + key: 'planet', + label: __( 'Other WordPress News' ), + siteUrl: _x( + 'https://planet.wordpress.org/', + 'Events and News dashboard widget' + ), + apiUrl: 'https://planet.wordpress.org/wp-json/wp/v2/posts?per_page=3&_fields=id,title,link,date', + }, +]; + +function decodeEntities( html: string ): string { + const txt = document.createElement( 'textarea' ); + txt.innerHTML = html; + return txt.value; +} + +export default function WordPressNews() { + const [ newsFeeds, setNewsFeeds ] = useState< NewsFeed[] >( [] ); + const [ newsLoading, setNewsLoading ] = useState( true ); + + useEffect( () => { + Promise.all( + NEWS_FEEDS.map( async ( feed ) => { + try { + const posts: NewsPost[] = await fetch( feed.apiUrl ).then( + ( r ) => r.json() + ); + return { + key: feed.key, + label: feed.label, + siteUrl: feed.siteUrl, + posts, + }; + } catch { + return { + key: feed.key, + label: feed.label, + siteUrl: feed.siteUrl, + posts: [], + }; + } + } ) + ) + .then( setNewsFeeds ) + .finally( () => setNewsLoading( false ) ); + }, [] ); + + const combinedItems: ListItem[] = newsFeeds + .flatMap( ( feed ) => + feed.posts.map( ( post ) => ( { + feedKey: feed.key, + feedLabel: feed.label, + id: post.id, + title: decodeEntities( post.title.rendered ), + url: post.link, + date: post.date, + } ) ) + ) + .sort( + ( a, b ) => + new Date( b.date ).getTime() - new Date( a.date ).getTime() + ) + .map( ( post ) => ( { + id: `${ post.feedKey }-${ post.id }`, + title: post.title, + url: post.url, + icon: + post.feedKey === 'news' ? ( + + ) : ( + + ), + meta: [ + post.feedLabel, + dateI18n( __( 'M j, Y g:i a' ), post.date ), + ], + } ) ); + + const emptyState = ( + + + + + { __( 'Quiet for now — the next headline is on its way.' ) } + + + + ); + + return ( + + + { newsLoading && ( + + + + ) } + + + { __( 'See all' ) } + + + + ); +} diff --git a/widgets/news/style.module.css b/widgets/news/style.module.css new file mode 100644 index 00000000000000..7ee257aff6e127 --- /dev/null +++ b/widgets/news/style.module.css @@ -0,0 +1,30 @@ + +.footer { + padding: 12px 0 0; +} + +.listItem { + inline-size: 100%; +} + +.listItemIcon { + inline-size: 36px; + block-size: 36px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 8px; + background: var(--wpds-color-bg-track-neutral-weak); + color: var(--wpds-color-fg-content-neutral-weak); + flex-shrink: 0; +} + +.listItemContent { + flex: 1; + min-inline-size: 0; +} + +.statusText { + color: var(--wpds-color-fg-content-neutral-weak); + margin: 0; +} diff --git a/widgets/news/widget.json b/widgets/news/widget.json new file mode 100644 index 00000000000000..7d2fe10062bd5e --- /dev/null +++ b/widgets/news/widget.json @@ -0,0 +1,6 @@ +{ + "name": "core/news", + "title": "WordPress news", + "description": "Displays the latest news from WordPress.org and Planet WordPress.", + "category": "dashboard" +} diff --git a/widgets/news/widget.ts b/widgets/news/widget.ts new file mode 100644 index 00000000000000..0acec4ef377d64 --- /dev/null +++ b/widgets/news/widget.ts @@ -0,0 +1,6 @@ +import { __ } from '@wordpress/i18n'; + +export default { + name: 'core/news', + title: __( 'WordPress news' ), +}; From 2529b5f6f26da22c3a48b5c096d22da06190e345 Mon Sep 17 00:00:00 2001 From: Mikael Korpela Date: Fri, 22 May 2026 12:20:57 +0300 Subject: [PATCH 2/2] Remove manual load.php widget registration. Widgets are discovered from widgets/ via the build registry, same as hello-world. --- lib/experimental/dashboard-widgets/load.php | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/lib/experimental/dashboard-widgets/load.php b/lib/experimental/dashboard-widgets/load.php index 3079bb7f7b006e..635c18af15dbcd 100644 --- a/lib/experimental/dashboard-widgets/load.php +++ b/lib/experimental/dashboard-widgets/load.php @@ -6,8 +6,6 @@ */ add_action( 'admin_menu', 'gutenberg_register_dashboard_widgets_menu' ); -add_action( 'dashboard_init', 'gutenberg_dashboard_widgets_register_news_widget' ); -add_action( 'dashboard-wp-admin_init', 'gutenberg_dashboard_widgets_register_news_widget' ); /** * Registers the Dashboard Widgets menu item. @@ -23,19 +21,3 @@ function gutenberg_register_dashboard_widgets_menu() { 1 ); } - -/** - * Wires the `news` widget as a dynamic dep of the dashboard module so it - * lands in the import map and React.lazy in the dashboard stage can resolve it. - */ -function gutenberg_dashboard_widgets_register_news_widget() { - $widget_module = 'wp/widgets/news/render'; - - if ( function_exists( 'gutenberg_register_dashboard_route' ) ) { - gutenberg_register_dashboard_route( '/__widget_news', $widget_module ); - } - - if ( function_exists( 'gutenberg_register_dashboard_wp_admin_route' ) ) { - gutenberg_register_dashboard_wp_admin_route( '/__widget_news', $widget_module ); - } -}