Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add contextual commands #50543

Merged
merged 9 commits into from
May 16, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 34 additions & 22 deletions packages/commands/src/components/command-menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,31 @@ export function CommandMenuLoaderWrapper( { hook, search, setLoader, close } ) {
}

export function CommandMenuGroup( { group, search, setLoader, close } ) {
const hasSearch = !! search;
const { commands, loaders } = useSelect(
( select ) => {
const { getCommands, getCommandLoaders } = select( commandsStore );
const {
getCommands,
getCommandLoaders,
getContextualCommands,
getContextualCommandLoaders,
} = select( commandsStore );
return {
commands: getCommands( group ),
loaders: getCommandLoaders( group ),
commands: hasSearch
? getCommands( group )
: getContextualCommands( group ),
loaders: hasSearch
? getCommandLoaders( group )
: getContextualCommandLoaders( group ),
};
},
[ group ]
[ group, hasSearch ]
);

if ( ! commands.length && ! loaders.length ) {
youknowriad marked this conversation as resolved.
Show resolved Hide resolved
return null;
}

return (
<Command.Group>
{ commands.map( ( command ) => (
Expand Down Expand Up @@ -219,24 +233,22 @@ export function CommandMenu() {
placeholder={ __( 'Type a command or search' ) }
/>
</div>
{ search && (
<Command.List>
{ ! isLoading && (
<Command.Empty>
{ __( 'No results found.' ) }
</Command.Empty>
) }
{ groups.map( ( group ) => (
<CommandMenuGroup
key={ group }
group={ group }
search={ search }
setLoader={ setLoader }
close={ closeAndReset }
/>
) ) }
</Command.List>
) }
<Command.List>
{ search && ! isLoading && (
<Command.Empty>
{ __( 'No results found.' ) }
</Command.Empty>
) }
{ groups.map( ( group ) => (
<CommandMenuGroup
key={ group }
group={ group }
search={ search }
setLoader={ setLoader }
close={ closeAndReset }
/>
) ) }
</Command.List>
</Command>
</div>
</Modal>
Expand Down
10 changes: 9 additions & 1 deletion packages/commands/src/components/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,15 @@
[cmdk-root] > [cmdk-list] {
max-height: 400px;
overflow: auto;
padding: $grid-unit;

& > [cmdk-list-sizer] {
padding: $grid-unit;

// Hides the padding in empty contexts.
&:empty {
display: none;
}
}
}

[cmdk-empty] {
Expand Down
32 changes: 32 additions & 0 deletions packages/commands/src/hooks/use-command-context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* WordPress dependencies
*/
import { useEffect, useRef } from '@wordpress/element';
import { useDispatch, useSelect } from '@wordpress/data';

/**
* Internal dependencies
*/
import { store as commandsStore } from '../store';

/**
* Sets the active context of the command center
*
* @param {string} context Context to set.
*/
export default function useCommandContext( context ) {
const { getContext } = useSelect( commandsStore );
const initialContext = useRef( getContext() );
const { setContext } = useDispatch( commandsStore );

useEffect( () => {
setContext( context );
}, [ context, setContext ] );

// This effects ensures that on unmount, we restore the context
// that was set before the component actually mounts.
useEffect( () => {
const initialContextRef = initialContext.current;
return () => setContext( initialContextRef );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain a bit more about this? If I understand correctly this is not used right now and I'm not sure when the edit-site Layout could unmount.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a solution to the problem of third-part contexts. Imagine you have a component that is only rendered in some conditions and that when rendered sets a different third-party context for the command center. The code here ensures that whenever that components unmount, we restore the context that was in the store before that particular component mounts. It's not perfect but it's probably acceptable.

}, [ setContext ] );
}
20 changes: 14 additions & 6 deletions packages/commands/src/hooks/use-command-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,25 @@ import { store as commandsStore } from '../store';
*
* @param {import('../store/actions').WPCommandLoaderConfig} loader command loader config.
*/
export default function useCommandLoader( { name, group, hook } ) {
export default function useCommandLoader( loader ) {
const { registerCommandLoader, unregisterCommandLoader } =
useDispatch( commandsStore );
useEffect( () => {
registerCommandLoader( {
name,
group,
hook,
name: loader.name,
group: loader.group,
hook: loader.hook,
context: loader.context,
} );
return () => {
unregisterCommandLoader( name, group );
unregisterCommandLoader( loader.name, loader.group );
};
}, [ name, group, hook, registerCommandLoader, unregisterCommandLoader ] );
}, [
loader.name,
loader.group,
loader.hook,
loader.context,
registerCommandLoader,
unregisterCommandLoader,
] );
}
2 changes: 2 additions & 0 deletions packages/commands/src/hooks/use-command.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export default function useCommand( command ) {
registerCommand( {
name: command.name,
group: command.group,
context: command.context,
label: command.label,
icon: command.icon,
callback: currentCallback.current,
Expand All @@ -37,6 +38,7 @@ export default function useCommand( command ) {
command.label,
command.group,
command.icon,
command.context,
registerCommand,
unregisterCommand,
] );
Expand Down
2 changes: 2 additions & 0 deletions packages/commands/src/private-apis.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/pri
*/
import { default as useCommand } from './hooks/use-command';
import { default as useCommandLoader } from './hooks/use-command-loader';
import { default as useCommandContext } from './hooks/use-command-context';
import { store } from './store';

export const { lock, unlock } =
Expand All @@ -20,5 +21,6 @@ export const privateApis = {};
lock( privateApis, {
useCommand,
useCommandLoader,
useCommandContext,
store,
} );
38 changes: 25 additions & 13 deletions packages/commands/src/store/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* @property {string} name Command name.
* @property {string} label Command label.
* @property {string=} group Command group.
* @property {string=} context Command context.
* @property {JSX.Element} icon Command icon.
* @property {Function} callback Command callback.
*/
Expand All @@ -21,9 +22,10 @@
*
* @typedef {Object} WPCommandLoaderConfig
*
* @property {string} name Command loader name.
* @property {string=} group Command loader group.
* @property {WPCommandLoaderHook} hook Command loader hook.
* @property {string} name Command loader name.
* @property {string=} group Command loader group.
* @property {string=} context Command loader context.
* @property {WPCommandLoaderHook} hook Command loader hook.
*/

/**
Expand All @@ -33,14 +35,11 @@
*
* @return {Object} action.
*/
export function registerCommand( { name, label, icon, callback, group = '' } ) {
export function registerCommand( config ) {
return {
type: 'REGISTER_COMMAND',
name,
label,
icon,
callback,
group,
...config,
group: config.group ?? '',
};
}

Expand All @@ -67,12 +66,11 @@ export function unregisterCommand( name, group ) {
*
* @return {Object} action.
*/
export function registerCommandLoader( { name, group = '', hook } ) {
export function registerCommandLoader( config ) {
return {
type: 'REGISTER_COMMAND_LOADER',
name,
group,
hook,
...config,
group: config.group ?? '',
};
}

Expand Down Expand Up @@ -113,3 +111,17 @@ export function close() {
type: 'CLOSE',
};
}

/**
* Sets the active context.
*
* @param {string} context Context.
*
* @return {Object} action.
*/
export function setContext( context ) {
return {
type: 'SET_CONTEXT',
context,
};
}
20 changes: 20 additions & 0 deletions packages/commands/src/store/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ function commands( state = {}, action ) {
name: action.name,
label: action.label,
group: action.group,
context: action.context,
callback: action.callback,
icon: action.icon,
},
Expand Down Expand Up @@ -57,6 +58,7 @@ function commandLoaders( state = {}, action ) {
...state[ action.group ],
[ action.name ]: {
name: action.name,
context: action.context,
hook: action.hook,
},
},
Expand Down Expand Up @@ -93,10 +95,28 @@ function isOpen( state = false, action ) {
return state;
}

/**
* Reducer returning the command center's active context.
*
* @param {Object} state Current state.
* @param {Object} action Dispatched action.
*
* @return {boolean} Updated state.
*/
function context( state = 'root', action ) {
switch ( action.type ) {
case 'SET_CONTEXT':
return action.context;
}

return state;
}

const reducer = combineReducers( {
commands,
commandLoaders,
isOpen,
context,
} );

export default reducer;
22 changes: 22 additions & 0 deletions packages/commands/src/store/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,25 @@ export const getCommandLoaders = createSelector(
export function isOpen( state ) {
return state.isOpen;
}

export function getContext( state ) {
return state.context;
}

export const getContextualCommands = createSelector(
( state, group ) => {
return getCommands( state, group ).filter(
( command ) => command.context === state.context
);
},
( state, group ) => [ state.commands[ group ], state.context ]
);

export const getContextualCommandLoaders = createSelector(
( state, group ) => {
return getCommandLoaders( state, group ).filter(
( loader ) => loader.context === state.context
);
},
( state, group ) => [ state.commandLoaders[ group ], state.context ]
);
48 changes: 25 additions & 23 deletions packages/core-commands/src/site-editor-navigation-commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,29 +31,31 @@ const getNavigationCommandLoaderPerPostType = ( postType ) =>
const supportsSearch = ! [ 'wp_template', 'wp_template_part' ].includes(
postType
);
const deps = supportsSearch ? [ search ] : [];
const { records, isLoading } = useSelect( ( select ) => {
const { getEntityRecords } = select( coreStore );
const query = supportsSearch
? {
search: !! search ? search : undefined,
per_page: 10,
orderby: search ? 'relevance' : 'date',
}
: {
per_page: -1,
};
return {
records: getEntityRecords( 'postType', postType, query ),
isLoading: ! select( coreStore ).hasFinishedResolution(
'getEntityRecords',
[ 'postType', postType, query ]
),
// We're using the string literal to check whether we're in the site editor.
/* eslint-disable-next-line @wordpress/data-no-store-string-literals */
isSiteEditor: !! select( 'edit-site' ),
};
}, deps );
const { records, isLoading } = useSelect(
( select ) => {
const { getEntityRecords } = select( coreStore );
const query = supportsSearch
? {
search: !! search ? search : undefined,
per_page: 10,
orderby: search ? 'relevance' : 'date',
}
: {
per_page: -1,
};
return {
records: getEntityRecords( 'postType', postType, query ),
isLoading: ! select( coreStore ).hasFinishedResolution(
'getEntityRecords',
[ 'postType', postType, query ]
),
// We're using the string literal to check whether we're in the site editor.
/* eslint-disable-next-line @wordpress/data-no-store-string-literals */
isSiteEditor: !! select( 'edit-site' ),
};
},
[ supportsSearch, search ]
);

const commands = useMemo( () => {
return ( records ?? [] ).slice( 0, 10 ).map( ( record ) => {
Expand Down
Loading