Dashboard: Add experimental WidgetDashboard rendering engine#77770
Dashboard: Add experimental WidgetDashboard rendering engine#77770
WidgetDashboard rendering engine#77770Conversation
Stateless WidgetDashboard with compound .Widgets and .Widget, minimal attributes-only render contract, lazy module resolver, and unit tests. Intermediate home before extraction to a @wordpress/dashboard package.
split metadata + runtime, adopt DataViews Field, rename renderModule
duplicate of grid's own resize observation; move to @wordpress/grid tracked in RSM-1222
WidgetBadge had no consumers; WidgetErrorConfig was over-specified for what the engine actually reports — error shape inlined on the onWidgetError signature
groups every engine module — root, compounds, context, factory, types, styles, stories, tests — under routes/dashboard/widgets-dashboard/ so the folder mirrors the future @wordpress/dashboard package layout. drops the stale route-level barrel since stage.tsx imports from the subdirectory directly.
drop @param root0.X boilerplate from component jsdocs and trust the props interfaces — modeled after @wordpress/ui. forwardRef wraps DOM-rendering components; impl-pattern covers the rest.
pick columns / minColumnWidth / rowHeight / spacing from @wordpress/grid instead of redefining locally — same pattern used for placement. converts WidgetDashboardProps to a type alias so the discriminated columns ↔ minColumnWidth union from grid distributes correctly.
no caller in the engine; reintroduce alongside the first concrete consumer (extension hooks, persistence helpers, analytics) so the shape can be designed against a real use case
demonstrates that WidgetDashboard is a container — compound parts (Empty, Widgets) can be interleaved with any consumer markup like headers and footers
more descriptive at the call site; the name signals 'configuration of the grid model' rather than the grid itself
note that WidgetName / WidgetTypeMetadata / WidgetType will collapse to re-exports from @wordpress/widget-types once that package lands.
describe widget-dashboard props, attribute schema, and Field<any> on their own terms instead of as a copy of the dataviews pattern.
also moves the factory into utils/ and adds a GridTilePlacement alias.
compounds (Empty, Widget, Widgets, WidgetRender) move under components/; provider+hook pairs move under context/. Root component, types, and public barrel stay at the top.
Empty falls back to a built-in EmptyState when no children are passed. The root component renders Empty + Widgets when the consumer omits children, so an empty layout still shows a sensible UI.
each compound owns its CSS next to its TSX. adds an empty.module.css seed for the empty placeholder so it can be tuned independently.
each compound (NoWidgetsState, Widget, Widgets, WidgetRender) now lives in its own folder with the tsx, css module, and a barrel index.ts. WidgetDashboard.Empty becomes WidgetDashboard.NoWidgetsState to make the compound's purpose explicit.
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
Size Change: 0 B Total Size: 7.87 MB ℹ️ View Unchanged
|
|
Flaky tests detected in 013f00f. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/25321678454
|
| <Stack justify="center" align="center" className={ styles.root }> | ||
| { children ?? ( | ||
| <EmptyState.Root> | ||
| <EmptyState.Icon icon={ widget } /> |
There was a problem hiding this comment.
Actually copy and icons might be something we want to allow configure later on when in a package, similarly to how DataViews allows passing empty state.
There was a problem hiding this comment.
This is already supported.
WidgetDashboard.NoWidgetsState accepts children as an override and falls back to a built-in EmptyState (icon + copy) when none are passed:
// Default placeholder (home icon + built-in copy)
<WidgetDashboard.NoWidgetsState />
// Full override
<WidgetDashboard.NoWidgetsState>
<EmptyState.Root>
<EmptyState.Icon icon={ myIcon } />
<EmptyState.Title>…</EmptyState.Title>
<EmptyState.Description>…</EmptyState.Description>
</EmptyState.Root>
</WidgetDashboard.NoWidgetsState>The NoWidgets and Composition stories cover both shapes.
Addressed here 30167a1 Screen.Recording.2026-04-29.at.12.06.47.PM.mov |
| <div | ||
| ref={ ref } | ||
| className={ styles.widget } | ||
| { ...( editMode ? { inert: '' } : {} ) } |
There was a problem hiding this comment.
Yeah, great feature



Part of #77616 — specifically the rendering engine track at #77626.
What?
Adds a stateless rendering engine for widget dashboards under
routes/dashboard/widget-dashboard/. The engine renders an editable grid of widget instances and exposes a compound component API (WidgetDashboard,.Widgets,.Widget,.NoWidgetsState).The dashboard route, previously a placeholder, now mounts the engine.
Why?
Establishes the rendering surface for the experimental customizable dashboard work. The engine is intentionally scoped inside the route for now, and is ready to graduate to its own
@wordpress/dashboardpackage once the API stabilizes and there are real consumers outside the route.How?
layoutandeditMode; every mutation firesonLayoutChangewith the fully updated array. The engine never queries a widget-types store — types flow in via thewidgetTypesprop.<NoWidgetsState />+<Widgets />. Passchildrento compose custom layouts (header, footer, custom empty state, etc.).{ attributes, setAttributes }. Removal, badges, error UI, and loading are surface concerns owned by the engine — not the widget.EmptyStatefrom@wordpress/ui(icon + title + description). Override by passing children toWidgetDashboard.NoWidgetsState.@wordpress/gridfor drag/resize..module.cssco-located with the TSX.Notes
WidgetName,WidgetTypeMetadata, andWidgetTypeare declared locally intypes.ts. They will collapse into a re-export from@wordpress/widget-typesonce that package lands.EmptyStatefrom@wordpress/uiis imported with aneslint-disable-next-line @wordpress/use-recommended-componentscomment in two files. Promotion of the component to the rule's allowlist is tracked in ESLint Plugin: RecommendEmptyStatefrom@wordpress/ui#77765; once that lands the disables can come out.Testing
npm run test:unit -- routes/dashboard/widget-dashboard— 10 tests pass.Dashboard / WidgetDashboardincludesDefault,EditMode,Empty,Responsive, andCompositionstories.npm startrunning, visit the dashboard route — the empty placeholder renders. Drag/resize works onceeditModeis on.Follow-ups
@wordpress/widget-typesonce that package lands.eslint-disable-next-line @wordpress/use-recommended-componentsmarkers once ESLint Plugin: RecommendEmptyStatefrom@wordpress/ui#77765 merges.Actions,Inserter, per-instance header + remove button.