Skip to content

Commit a878524

Browse files
simisonsimisonretrofoxretrofox
authored
Dashboard: add command palette commands (#78429)
Co-authored-by: simison <simison@git.wordpress.org> Co-authored-by: retrofox <retrofox@git.wordpress.org> Co-authored-by: Damián Suárez <rdsuarez@gmail.com>
1 parent 8737ce6 commit a878524

12 files changed

Lines changed: 540 additions & 12 deletions

File tree

‎package-lock.json‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎routes/dashboard/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"@wordpress/api-fetch": "file:../../packages/api-fetch",
1414
"@wordpress/base-styles": "file:../../packages/base-styles",
1515
"@wordpress/core-data": "file:../../packages/core-data",
16+
"@wordpress/commands": "file:../../packages/commands",
1617
"@wordpress/components": "file:../../packages/components",
1718
"@wordpress/compose": "file:../../packages/compose",
1819
"@wordpress/data": "file:../../packages/data",

‎routes/dashboard/widget-dashboard/components/actions/actions.tsx‎

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,13 @@ export function Actions(): React.ReactNode {
7272
return () => clearTimeout( exitTimeout );
7373
}, [ editMode, isEditActionsMounted ] );
7474

75-
const { setInserterOpen } = useDashboardUIContext();
76-
77-
const [ isResetDialogOpen, setIsResetDialogOpen ] = useState( false );
78-
const [ isLayoutSettingsOpen, setIsLayoutSettingsOpen ] = useState( false );
75+
const {
76+
setInserterOpen,
77+
layoutSettingsOpen,
78+
setLayoutSettingsOpen,
79+
resetDialogOpen,
80+
setResetDialogOpen,
81+
} = useDashboardUIContext();
7982

8083
const handleEditMode = useCallback( () => {
8184
onEditChange?.( ! editMode );
@@ -94,13 +97,13 @@ export function Actions(): React.ReactNode {
9497
}, [ commit ] );
9598

9699
const openLayoutSettings = useCallback( () => {
97-
setIsLayoutSettingsOpen( true );
98-
}, [] );
100+
setLayoutSettingsOpen( true );
101+
}, [ setLayoutSettingsOpen ] );
99102

100103
const moreActionsItems: MoreActionsDropdownItem[] = [
101104
{
102105
label: __( 'Reset to default' ),
103-
onClick: () => setIsResetDialogOpen( true ),
106+
onClick: () => setResetDialogOpen( true ),
104107
disabled: ! onLayoutReset,
105108
},
106109
];
@@ -178,12 +181,12 @@ export function Actions(): React.ReactNode {
178181
<MoreActionsDropdown items={ moreActionsItems } />
179182

180183
<AlertDialog.Root
181-
open={ isResetDialogOpen }
182-
onOpenChange={ setIsResetDialogOpen }
184+
open={ resetDialogOpen }
185+
onOpenChange={ setResetDialogOpen }
183186
onConfirm={ async () => {
184187
await onLayoutReset?.();
185188
onEditChange?.( false );
186-
setIsResetDialogOpen( false );
189+
setResetDialogOpen( false );
187190
} }
188191
>
189192
<AlertDialog.Popup
@@ -198,8 +201,8 @@ export function Actions(): React.ReactNode {
198201

199202
{ canEditGridSettings && (
200203
<LayoutSettings
201-
open={ isLayoutSettingsOpen }
202-
onOpenChange={ setIsLayoutSettingsOpen }
204+
open={ layoutSettingsOpen }
205+
onOpenChange={ setLayoutSettingsOpen }
203206
/>
204207
) }
205208
</Stack>
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/**
2+
* WordPress dependencies
3+
*/
4+
import { useCallback, useMemo } from '@wordpress/element';
5+
import { __ } from '@wordpress/i18n';
6+
import {
7+
useCommands,
8+
privateApis as commandsPrivateApis,
9+
} from '@wordpress/commands';
10+
import {
11+
columns,
12+
grid,
13+
layout as layoutIcon,
14+
plus,
15+
trash,
16+
} from '@wordpress/icons';
17+
18+
/**
19+
* Internal dependencies
20+
*/
21+
import { unlock } from '../../../lock-unlock';
22+
import { useDashboardInternalContext } from '../../context/dashboard-context';
23+
import { useDashboardUIContext } from '../../context/ui-context';
24+
import { getGridModel } from '../../utils/grid-model-change';
25+
import { usePendingWhenEditMode } from './use-pending-when-edit-mode';
26+
27+
const { useCommandContext } = unlock( commandsPrivateApis );
28+
29+
export const DASHBOARD_COMMAND_CONTEXT = 'dashboard';
30+
31+
type CommandCallback = ( options: { close: () => void } ) => void;
32+
33+
/**
34+
* Registers dashboard-specific command palette commands and sets the
35+
* active command context so they surface under Suggestions by default.
36+
*/
37+
export function DashboardCommands() {
38+
const {
39+
editMode,
40+
onEditChange,
41+
onLayoutReset,
42+
gridSettings,
43+
canEditGridSettings,
44+
commitGridModelChange,
45+
} = useDashboardInternalContext();
46+
47+
const { setInserterOpen, setResetDialogOpen } = useDashboardUIContext();
48+
49+
useCommandContext( DASHBOARD_COMMAND_CONTEXT );
50+
51+
const runWhenInEditMode = usePendingWhenEditMode( {
52+
editMode,
53+
onEditChange,
54+
} );
55+
56+
const gridModel = getGridModel( gridSettings );
57+
const isGridLayout = gridModel === 'grid';
58+
const isMasonryLayout = gridModel === 'masonry';
59+
60+
const switchToMasonry = useCallback< CommandCallback >(
61+
( { close } ) => {
62+
close();
63+
commitGridModelChange( 'masonry' );
64+
},
65+
[ commitGridModelChange ]
66+
);
67+
68+
const switchToGrid = useCallback< CommandCallback >(
69+
( { close } ) => {
70+
close();
71+
commitGridModelChange( 'grid' );
72+
},
73+
[ commitGridModelChange ]
74+
);
75+
76+
const resetToDefault = useCallback< CommandCallback >(
77+
( { close } ) => {
78+
close();
79+
setResetDialogOpen( true );
80+
},
81+
[ setResetDialogOpen ]
82+
);
83+
84+
const addWidgets = useCallback< CommandCallback >(
85+
( { close } ) => {
86+
close();
87+
runWhenInEditMode( () => {
88+
setInserterOpen( true );
89+
} );
90+
},
91+
[ runWhenInEditMode, setInserterOpen ]
92+
);
93+
94+
const customize = useCallback< CommandCallback >(
95+
( { close } ) => {
96+
close();
97+
onEditChange?.( true );
98+
},
99+
[ onEditChange ]
100+
);
101+
102+
const commands = useMemo(
103+
() => [
104+
{
105+
name: 'core/dashboard/customize',
106+
label: __( 'Customize dashboard' ),
107+
icon: layoutIcon,
108+
category: 'command',
109+
context: DASHBOARD_COMMAND_CONTEXT,
110+
keywords: [ __( 'edit' ), __( 'widgets' ), __( 'layout' ) ],
111+
disabled: ! onEditChange || editMode,
112+
callback: customize,
113+
},
114+
{
115+
name: 'core/dashboard/add-widgets',
116+
label: __( 'Add dashboard widgets' ),
117+
icon: plus,
118+
category: 'command',
119+
context: DASHBOARD_COMMAND_CONTEXT,
120+
keywords: [ __( 'widgets' ), __( 'inserter' ) ],
121+
disabled: ! onEditChange,
122+
callback: addWidgets,
123+
},
124+
{
125+
name: 'core/dashboard/switch-to-masonry-layout',
126+
label: __( 'Switch dashboard to masonry layout' ),
127+
icon: columns,
128+
category: 'command',
129+
context: DASHBOARD_COMMAND_CONTEXT,
130+
keywords: [
131+
__( 'layout' ),
132+
__( 'layout model' ),
133+
__( 'masonry' ),
134+
],
135+
disabled: ! canEditGridSettings || isMasonryLayout || editMode,
136+
callback: switchToMasonry,
137+
},
138+
{
139+
name: 'core/dashboard/switch-to-grid-layout',
140+
label: __( 'Switch dashboard to grid layout' ),
141+
icon: grid,
142+
category: 'command',
143+
context: DASHBOARD_COMMAND_CONTEXT,
144+
keywords: [
145+
__( 'layout' ),
146+
__( 'layout model' ),
147+
__( 'grid' ),
148+
__( 'standard grid' ),
149+
],
150+
disabled: ! canEditGridSettings || isGridLayout || editMode,
151+
callback: switchToGrid,
152+
},
153+
{
154+
name: 'core/dashboard/reset-to-default',
155+
label: __( 'Reset dashboard widgets to default' ),
156+
icon: trash,
157+
category: 'command',
158+
context: DASHBOARD_COMMAND_CONTEXT,
159+
keywords: [ __( 'reset' ), __( 'default' ) ],
160+
disabled: ! onLayoutReset,
161+
callback: resetToDefault,
162+
},
163+
],
164+
[
165+
onEditChange,
166+
editMode,
167+
customize,
168+
addWidgets,
169+
canEditGridSettings,
170+
isMasonryLayout,
171+
isGridLayout,
172+
switchToMasonry,
173+
switchToGrid,
174+
onLayoutReset,
175+
resetToDefault,
176+
]
177+
);
178+
179+
useCommands( commands );
180+
181+
return null;
182+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export {
2+
DashboardCommands,
3+
DASHBOARD_COMMAND_CONTEXT,
4+
} from './dashboard-commands';
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* WordPress dependencies
3+
*/
4+
import { useCallback, useEffect, useRef } from '@wordpress/element';
5+
6+
type usePendingWhenEditModeProps = {
7+
editMode: boolean;
8+
onEditChange?: ( next: boolean ) => void;
9+
};
10+
11+
type usePendingWhenEditModeResult = ( action: () => void ) => void;
12+
13+
/**
14+
* Runs an action immediately when already in edit mode. Otherwise enables
15+
* edit mode and runs the action after edit mode becomes active.
16+
*
17+
* @param {usePendingWhenEditModeProps} params - The parameters for the function.
18+
* @return {usePendingWhenEditModeResult} The function result.
19+
*/
20+
export function usePendingWhenEditMode( {
21+
editMode,
22+
onEditChange,
23+
}: usePendingWhenEditModeProps ): usePendingWhenEditModeResult {
24+
const pendingRef = useRef< ( () => void ) | null >( null );
25+
const wasEditingRef = useRef( editMode );
26+
27+
useEffect( () => {
28+
if ( ! wasEditingRef.current && editMode && pendingRef.current ) {
29+
const action = pendingRef.current;
30+
pendingRef.current = null;
31+
action();
32+
}
33+
wasEditingRef.current = editMode;
34+
}, [ editMode ] );
35+
36+
return useCallback(
37+
( action: () => void ) => {
38+
if ( editMode ) {
39+
action();
40+
return;
41+
}
42+
43+
pendingRef.current = action;
44+
onEditChange?.( true );
45+
},
46+
[ editMode, onEditChange ]
47+
);
48+
}

‎routes/dashboard/widget-dashboard/context/dashboard-context.tsx‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ import {
1919
/**
2020
* Internal dependencies
2121
*/
22+
import { computeGridModelChange } from '../utils/grid-model-change';
2223
import type {
2324
ResolveWidgetModule,
25+
WidgetGridModel,
2426
WidgetGridSettings,
2527
DashboardWidget,
2628
WidgetType,
@@ -123,6 +125,13 @@ interface InternalDashboardContextValue {
123125
*/
124126
commit: ( options?: { exitEditMode?: boolean } ) => void;
125127

128+
/**
129+
* Switches the layout model, updates staging, and publishes
130+
* immediately — equivalent to changing the model in layout
131+
* settings and clicking Save.
132+
*/
133+
commitGridModelChange: ( targetModel: WidgetGridModel ) => void;
134+
126135
/**
127136
* Reverts both staging slices. By default also exits edit mode; pass
128137
* `{ exitEditMode: false }` when dismissing the layout settings drawer.
@@ -309,6 +318,33 @@ export function WidgetDashboardProvider( {
309318
[ committedLayout, committedGridSettings, onEditChange ]
310319
);
311320

321+
const commitGridModelChange = useCallback(
322+
( targetModel: WidgetGridModel ) => {
323+
const next = computeGridModelChange( {
324+
layout: stagingLayout,
325+
gridSettings: stagingGridSettings,
326+
targetModel,
327+
} );
328+
329+
if ( ! next ) {
330+
return;
331+
}
332+
333+
setStagingLayout( next.layout );
334+
setStagingGridSettings( next.gridSettings );
335+
onLayoutChange( canonicalize( next.layout ) );
336+
onGridSettingsChange?.( next.gridSettings );
337+
onEditChange?.( false );
338+
},
339+
[
340+
stagingLayout,
341+
stagingGridSettings,
342+
onLayoutChange,
343+
onGridSettingsChange,
344+
onEditChange,
345+
]
346+
);
347+
312348
const resetGridSettings = useCallback( () => {
313349
setStagingGridSettings( DEFAULT_GRID );
314350
}, [] );
@@ -337,6 +373,7 @@ export function WidgetDashboardProvider( {
337373
canEditGridSettings,
338374
resetGridSettings,
339375
commit,
376+
commitGridModelChange,
340377
cancel,
341378
hasUncommittedChanges,
342379
editMode,
@@ -351,6 +388,7 @@ export function WidgetDashboardProvider( {
351388
canEditGridSettings,
352389
resetGridSettings,
353390
commit,
391+
commitGridModelChange,
354392
cancel,
355393
hasUncommittedChanges,
356394
editMode,

0 commit comments

Comments
 (0)