Dashboard: REST endpoint for the default layout#78066
Conversation
|
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. |
passes the dashboard name as a second arg to the
gutenberg_dashboard_default_layout filter and registers
GET /wp/v2/dashboards/{name}/default-layout to expose the
filter chain output to clients on demand.
parametrizes the hook by dashboard name and backs the new
resetLayout entry by the GET /wp/v2/dashboards/{name}/default-layout
endpoint via apiFetch. The fetched default replaces the
layout through the preferences store, mirroring how
setLayout commits user changes.
threads the new layout-reset callback through the widget- dashboard surface props, the internal context, and the provider so child components can trigger a reset. The dashboard route forwards the hook's resetLayout to it.
introduces a more-actions dropdown next to the edit-mode buttons. Reset opens an irreversible AlertDialog whose confirm clears the customized layout, applies the default, and exits edit mode. Built on the private Menu primitive with render-prop delegation to design-system Button and IconButton.
9acdeb3 to
c246f97
Compare
|
Size Change: 0 B Total Size: 7.92 MB ℹ️ View Unchanged
|
There was a problem hiding this comment.
Pull request overview
Adds end-to-end “Reset to default” support for the experimental wp-admin Dashboard surface by exposing the server-resolved default layout over REST, wiring a client reset action into the layout hook, and surfacing the action via an edit-mode overflow menu with confirmation.
Changes:
- PHP: Extend
gutenberg_dashboard_default_layoutfilter to receive a dashboard name, and addGET /wp/v2/dashboards/{name}/default-layoutto return a fresh resolved default layout. - JS hook: Update
useDashboardLayout( dashboardName )to provide an asyncresetLayoutthat fetches and persists the default layout via the preferences store. - UI: Add a more-actions dropdown + irreversible
AlertDialogconfirmation to trigger reset and exit edit mode.
Reviewed changes
Copilot reviewed 14 out of 15 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| routes/dashboard/widget-dashboard/widget-dashboard.tsx | Threads new onLayoutReset prop into the provider. |
| routes/dashboard/widget-dashboard/types.ts | Adds onLayoutReset to WidgetDashboardProps. |
| routes/dashboard/widget-dashboard/test/actions.test.tsx | Minor test change (removes eslint disable). |
| routes/dashboard/widget-dashboard/context/dashboard-context.tsx | Adds onLayoutReset to internal context/provider props. |
| routes/dashboard/widget-dashboard/components/more-actions-dropdown/more-actions-dropdown.tsx | Introduces overflow menu component built on private Menu. |
| routes/dashboard/widget-dashboard/components/more-actions-dropdown/index.ts | Exports the new dropdown component/types. |
| routes/dashboard/widget-dashboard/components/actions/actions.tsx | Adds overflow menu item + confirmation dialog to reset layout. |
| routes/dashboard/stage.tsx | Supplies dashboard name to hook and forwards resetLayout into WidgetDashboard. |
| routes/dashboard/package.json | Adds dependencies needed for REST fetch + private API opt-in. |
| routes/dashboard/lock-unlock.ts | Opts the dashboard route into private APIs under @wordpress/routes. |
| routes/dashboard/hooks/use-dashboard-layout/use-dashboard-layout.ts | Implements async reset via REST + preferences store write. |
| routes/dashboard/hooks/test/use-dashboard-layout.test.ts | Adds/updates unit test to cover reset via REST. |
| package-lock.json | Updates lockfile for new route dependencies. |
| lib/experimental/dashboard-widgets/default-layout-seed.php | Seeds default layout only for gutenberg_dashboard and accepts dashboard name arg. |
| lib/experimental/dashboard-widgets/dashboard-layout.php | Adds dashboard name constant, passes it into filter, and registers REST endpoint. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| interface InternalDashboardContextValue { | ||
| widgetTypes: WidgetType[]; | ||
| layout: DashboardWidget[]; | ||
| onLayoutChange: ( layout: DashboardWidget[] ) => void; | ||
| onLayoutReset: () => void; | ||
| editMode: boolean; | ||
| onEditChange?: ( next: boolean ) => void; | ||
| resolveWidgetModule: ResolveWidgetModule; | ||
| gridSettings: WidgetGridSettings; | ||
| } | ||
|
|
||
| const Context = createContext< InternalDashboardContextValue | null >( null ); | ||
|
|
||
| /** | ||
| * Compound-internal hook — exposes the full provider state. Not part of the | ||
| * public API; lives in the same module so compound components can reach the | ||
| * state directly. | ||
| */ | ||
| export function useDashboardInternalContext(): InternalDashboardContextValue { | ||
| const ctx = useContext( Context ); | ||
| if ( ! ctx ) { | ||
| throw new Error( | ||
| 'Dashboard compound used outside a WidgetDashboard subtree.' | ||
| ); | ||
| } | ||
| return ctx; | ||
| } | ||
|
|
||
| interface ProviderProps { | ||
| widgetTypes: WidgetType[]; | ||
| layout: DashboardWidget[]; | ||
| onLayoutChange: ( layout: DashboardWidget[] ) => void; | ||
| onLayoutReset: () => void; | ||
| editMode?: boolean; |
| @@ -52,6 +53,7 @@ export const WidgetDashboard = Object.assign( | |||
| <WidgetDashboardProvider | |||
| layout={ layout } | |||
| onLayoutChange={ onLayoutChange } | |||
| onLayoutReset={ onLayoutReset } | |||
| widgetTypes={ widgetTypes } | |||
| @@ -81,6 +96,27 @@ export function Actions(): React.ReactNode { | |||
| > | |||
| { __( 'Done' ) } | |||
| </Button> | |||
|
|
|||
| <MoreActionsDropdown items={ moreActionsItems } /> | |||
|
|
|||
| <AlertDialog.Root | |||
| open={ isResetDialogOpen } | |||
| onOpenChange={ setIsResetDialogOpen } | |||
| onConfirm={ () => { | |||
| onLayoutReset?.(); | |||
| onEditChange?.( false ); | |||
| setIsResetDialogOpen( false ); | |||
| } } | |||
| > | |||
| <AlertDialog.Popup | |||
| intent="irreversible" | |||
| title={ __( 'Reset dashboard to default?' ) } | |||
| description={ __( | |||
| 'Your customized layout will be replaced with the default. This cannot be undone.' | |||
| ) } | |||
| confirmButtonText={ __( 'Reset' ) } | |||
| /> | |||
| </AlertDialog.Root> | |||
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
|
Would it make sense to keep the three-dot menu visible all the time, and allow resetting without entering customisation mode? One click less. |
Fair question. In fact, that's what we did in a similar implementation. Updating... |
Co-authored-by: Mikael Korpela <mikael@ihminen.org>
…:WordPress/gutenberg into update/dashboard-default-layout-endpoint
|
Flaky tests detected in 1e86745. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/25509304265
|
What?
Adds the wiring needed to "reset to default" from the dashboard surface, in three steps:
gutenberg_dashboard_default_layoutnow receives a second argument with the dashboard name, and a new routeGET /wp/v2/dashboards/{name}/default-layoutreturns a fresh evaluation of the filter chain.useDashboardLayout( dashboardName )returns aresetLayoutaction that fetches the registered default and writes it back through the preferences store.AlertDialogconfirmation. On confirm, the customized layout is replaced by the default and edit mode exits.Part of #77616 #78035
Why?
Resetting to the default in-session needs the JS layer to know what the default is. The default is defined server-side, on the
gutenberg_dashboard_default_layoutfilter, so a plugin/theme can layer onto it. There are three reasonable ways to surface that to the client; this PR picks the third.Inline script preload. The simplest route would be to print the resolved default into the page on every load (
wp_add_inline_scriptor similar), and have the client read from a global. We rejected this because the default is only consumed by a rare user action; preloading it on every page load means we ship payload that almost no one uses, on a load path that already has bootstrap pressure.Reset via the preferences store. The cleanest semantic is "purge the committed value and let the server re-resolve." That requires (a) a granular
purge( scope, key )action in@wordpress/preferences-persistencethat clears the in-memory cache +localStoragesnapshot + the persisted user-meta entry, and (b) a re-hydration action in@wordpress/preferencesthat re-pulls the meta after the purge so the store reflects the new default in-session. Both are reusable primitives that would benefit any preference-driven feature, not just dashboards. We considered them and chose to defer: they touch two core packages, change the preferences store's controlled-state model, and needed more discussion than this PR's scope. They remain the long-term direction.Dedicated REST endpoint. This is the path taken here. It needs no core changes outside the dashboard's own experimental directory, has zero footprint on every page load, and incurs a single round-trip only when the user actually triggers the reset. The trade-off is semantic: after the reset, the default is committed as a snapshot for that user instead of remaining "live" against future filter changes. For the current default (a single bundled widget) this is acceptable; if/when the default becomes more dynamic, the preferences-purge path above is the natural next step.
How?
Backend (
lib/experimental/dashboard-widgets/dashboard-layout.php):GUTENBERG_DASHBOARD_NAMEconstant (gutenberg_dashboard), shaped as<plugin>_<page>to mirror the underscore form produced by the wp-build pipeline ({{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}inpackages/wp-build/templates/page-wp-admin.php.template).apply_filters( 'gutenberg_dashboard_default_layout', ... ).GET /wp/v2/dashboards/(?P<name>[a-z][a-z0-9]*(?:_[a-z0-9]+)+)/default-layout. The regex enforces the<plugin>_<page>shape.current_user_can( 'read' ), matching the dashboard menu page.Seed (
lib/experimental/dashboard-widgets/default-layout-seed.php):core/hello-worldcallback now switches on$dashboard_nameand only contributes when targetinggutenberg_dashboard. Other dashboards layered onto the same filter are untouched.Hook (
routes/dashboard/hooks/use-dashboard-layout/use-dashboard-layout.ts):useDashboardLayout( dashboardName: DashboardName ), whereDashboardName = ${ string }_${ string }enforces the same shape as the endpoint regex at the type level.resetLayoutreturnsPromise<void>: itapiFetches/wp/v2/dashboards/${ dashboardName }/default-layoutand writes the response throughpreferencesStore.set. The result lands in the same scope/key the surface reads from, so the existinguseSelectpicks it up without any other plumbing.Surface props (
routes/dashboard/widget-dashboard/):WidgetDashboardPropsexposesonLayoutReset: () => void. The provider threads it into the internal context so any descendant can trigger the reset.resetLayoutto it.UI (
routes/dashboard/widget-dashboard/components/):MoreActionsDropdowncomponent built on the privateMenuprimitive from@wordpress/components. It accepts a declarativeitemsarray (label,onClick,disabled?).renderprop: the trigger is rendered asIconButton, each item is rendered asButton. Accessibility wiring (focus, keyboard nav, aria) stays with the menu primitives; the visual presentation comes from the DS.Actionsrenders the dropdown only in edit mode, and only includes the "Reset to default" item whenonLayoutResetis wired. Confirmation is anAlertDialog.Rootwithintent="irreversible". TheonConfirmcallback fires the reset, exits edit mode, and closes the dialog.lock-unlock.tsopts the dashboard route into private APIs under the@wordpress/routesmodule identifier.Testing
AlertDialogopens with the right copy.core/hello-world).npm run test:unit -- routes/dashboard/hooks/test/use-dashboard-layout.test.ts. The test suite mocksapiFetchand assertsresetLayoutrestores the registered default end-to-end.Screen.Recording.2026-05-07.at.1.13.59.PM.mov