From b7440a99bb14a6e309bd1f9d46d0d7f9e578d13b Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 7 Apr 2023 15:38:30 +0100 Subject: [PATCH] Allow nested command loaders within the command center --- .../commands/src/components/command-menu.js | 178 +++++++++++++----- .../commands/src/hooks/use-command-loader.js | 20 +- packages/commands/src/store/actions.js | 18 +- packages/commands/src/store/reducer.js | 3 + packages/commands/src/store/selectors.js | 13 ++ .../hooks/commands/use-navigation-commands.js | 8 + 6 files changed, 185 insertions(+), 55 deletions(-) diff --git a/packages/commands/src/components/command-menu.js b/packages/commands/src/components/command-menu.js index 6ced4ced90424a..58c3e668c45f0e 100644 --- a/packages/commands/src/components/command-menu.js +++ b/packages/commands/src/components/command-menu.js @@ -9,18 +9,19 @@ import { Command } from 'cmdk'; import { useSelect } from '@wordpress/data'; import { useState, useEffect, useRef, useCallback } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -import { Modal, TextHighlight } from '@wordpress/components'; +import { Modal, TextHighlight, Button } from '@wordpress/components'; +import { Icon, chevronLeft } from '@wordpress/icons'; /** * Internal dependencies */ import { store as commandsStore } from '../store'; -function CommandMenuLoader( { name, search, hook, setLoader, close } ) { +function CommandMenuLoader( { name, search, hook, setIsLoading, close } ) { const { isLoading, commands = [] } = hook( { search } ) ?? {}; useEffect( () => { - setLoader( name, isLoading ); - }, [ setLoader, name, isLoading ] ); + setIsLoading( name, isLoading ); + }, [ setIsLoading, name, isLoading ] ); return ( <> @@ -48,33 +49,45 @@ function CommandMenuLoader( { name, search, hook, setLoader, close } ) { ); } -export function CommandMenuLoaderWrapper( { hook, search, setLoader, close } ) { +export function CommandMenuLoaderWrapper( { + loader, + search, + setIsLoading, + close, +} ) { // The "hook" prop is actually a custom React hook // so to avoid breaking the rules of hooks // the CommandMenuLoaderWrapper component need to be // remounted on each hook prop change // We use the key state to make sure we do that properly. - const currentLoader = useRef( hook ); + const currentLoader = useRef( loader.hook ); const [ key, setKey ] = useState( 0 ); useEffect( () => { - if ( currentLoader.current !== hook ) { - currentLoader.current = hook; + if ( currentLoader.current !== loader.hook ) { + currentLoader.current = loader.hook; setKey( ( prevKey ) => prevKey + 1 ); } - }, [ hook ] ); + }, [ loader.hook ] ); return ( ); } -export function CommandMenuGroup( { group, search, setLoader, close } ) { +export function CommandMenuGroup( { + group, + search, + setIsLoading, + close, + selectLoader, +} ) { const { commands, loaders } = useSelect( ( select ) => { const { getCommands, getCommandLoaders } = select( commandsStore ); @@ -91,7 +104,7 @@ export function CommandMenuGroup( { group, search, setLoader, close } ) { { commands.map( ( command ) => ( command.callback( { close } ) } > @@ -102,29 +115,69 @@ export function CommandMenuGroup( { group, search, setLoader, close } ) { ) ) } - { loaders.map( ( loader ) => ( - - ) ) } + { loaders.map( ( loader ) => + loader.isNested && ! search ? ( + selectLoader( loader.name ) } + > + { loader.placeholder } + + ) : ( + + ) + ) } ); } -export function CommandMenu() { - const [ search, setSearch ] = useState( '' ); - const [ open, setOpen ] = useState( false ); +function RootCommandMenu( { search, close, setIsLoading, selectLoader } ) { const { groups } = useSelect( ( select ) => { const { getGroups } = select( commandsStore ); return { groups: getGroups(), }; }, [] ); - const [ loaders, setLoaders ] = useState( {} ); + + return ( + + { groups.map( ( group ) => ( + + ) ) } + + ); +} + +export function CommandMenu() { + const [ selectedLoader, selectLoader ] = useState( null ); + const [ search, setSearch ] = useState( '' ); + const [ open, setOpen ] = useState( false ); + const [ loadings, setLoadings ] = useState( {} ); + const { loader } = useSelect( + ( select ) => { + const { getCommandLoader } = select( commandsStore ); + return { + loader: selectedLoader + ? getCommandLoader( selectedLoader ) + : null, + }; + }, + [ selectedLoader ] + ); // Toggle the menu when Meta-K is pressed useEffect( () => { @@ -139,23 +192,28 @@ export function CommandMenu() { return () => document.removeEventListener( 'keydown', toggleOnMetaK ); }, [] ); - const setLoader = useCallback( + const goBack = useCallback( () => { + selectLoader( null ); + }, [ selectLoader ] ); + const setIsLoading = useCallback( ( name, value ) => - setLoaders( ( current ) => ( { + setLoadings( ( current ) => ( { ...current, [ name ]: value, } ) ), [] ); - const close = () => { + const close = useCallback( () => { setSearch( '' ); setOpen( false ); - }; + selectLoader( null ); + }, [ setSearch, setOpen, selectLoader ] ); if ( ! open ) { return false; } - const isLoading = Object.values( loaders ).some( Boolean ); + + const isLoading = Object.values( loadings ).some( Boolean ); return (
+ { !! loader && ( + + ) } { + if ( + event.key === 'Backspace' && + search === '' + ) { + goBack(); + } + } } />
- - { ! isLoading && ( - - { __( 'No results found.' ) } - - ) } - { groups.map( ( group ) => ( - - ) ) } - + { ! isLoading && ( + + { __( 'No results found.' ) } + + ) } + { ! loader && ( + + ) } + { loader && ( + + ) }
diff --git a/packages/commands/src/hooks/use-command-loader.js b/packages/commands/src/hooks/use-command-loader.js index ac81873c01a2ea..6aea2c59d4b682 100644 --- a/packages/commands/src/hooks/use-command-loader.js +++ b/packages/commands/src/hooks/use-command-loader.js @@ -14,7 +14,13 @@ 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( { + name, + group, + hook, + isNested, + placeholder, +} ) { const { registerCommandLoader, unregisterCommandLoader } = useDispatch( commandsStore ); useEffect( () => { @@ -22,9 +28,19 @@ export default function useCommandLoader( { name, group, hook } ) { name, group, hook, + isNested, + placeholder, } ); return () => { unregisterCommandLoader( name, group ); }; - }, [ name, group, hook, registerCommandLoader, unregisterCommandLoader ] ); + }, [ + name, + group, + hook, + isNested, + placeholder, + registerCommandLoader, + unregisterCommandLoader, + ] ); } diff --git a/packages/commands/src/store/actions.js b/packages/commands/src/store/actions.js index a2dfe526700a2b..31977b3326f2d0 100644 --- a/packages/commands/src/store/actions.js +++ b/packages/commands/src/store/actions.js @@ -20,9 +20,11 @@ * * @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 {WPCommandLoaderHook} hook Command loader hook. + * @property {boolean=} isNested Whether the command loader is nested. + * @property {string=} placeholder Command loader placeholder. */ /** @@ -65,12 +67,20 @@ export function unregisterCommand( name, group ) { * * @return {Object} action. */ -export function registerCommandLoader( { name, group = '', hook } ) { +export function registerCommandLoader( { + name, + group = '', + hook, + isNested = false, + placeholder, +} ) { return { type: 'REGISTER_COMMAND_LOADER', name, group, hook, + isNested, + placeholder, }; } diff --git a/packages/commands/src/store/reducer.js b/packages/commands/src/store/reducer.js index 6b4a65b9132e0d..0216e5f9662fb5 100644 --- a/packages/commands/src/store/reducer.js +++ b/packages/commands/src/store/reducer.js @@ -57,6 +57,9 @@ function commandLoaders( state = {}, action ) { [ action.name ]: { name: action.name, hook: action.hook, + group: action.group, + isNested: action.isNested, + placeholder: action.placeholder, }, }, }; diff --git a/packages/commands/src/store/selectors.js b/packages/commands/src/store/selectors.js index b8ad97fb0d4963..ac6f8e51fa76c3 100644 --- a/packages/commands/src/store/selectors.js +++ b/packages/commands/src/store/selectors.js @@ -27,3 +27,16 @@ export const getCommandLoaders = createSelector( ( state, group ) => Object.values( state.commandLoaders[ group ] ?? {} ), ( state, group ) => [ state.commandLoaders[ group ] ] ); + +export function getCommandLoader( state, loaderName ) { + const group = Object.keys( state.commandLoaders ).find( + ( currentGroup ) => + !! state.commandLoaders[ currentGroup ][ loaderName ] + ); + + if ( ! group ) { + return null; + } + + return state.commandLoaders[ group ][ loaderName ]; +} diff --git a/packages/edit-site/src/hooks/commands/use-navigation-commands.js b/packages/edit-site/src/hooks/commands/use-navigation-commands.js index 995d16d5013966..38186c77b7fc30 100644 --- a/packages/edit-site/src/hooks/commands/use-navigation-commands.js +++ b/packages/edit-site/src/hooks/commands/use-navigation-commands.js @@ -84,20 +84,28 @@ export function useNavigationCommands() { name: 'core/edit-site/navigate-pages', group: __( 'Pages' ), hook: usePageNavigationCommandLoader, + isNested: true, + placeholder: __( 'Search pages…' ), } ); useCommandLoader( { name: 'core/edit-site/navigate-posts', group: __( 'Posts' ), hook: usePostNavigationCommandLoader, + isNested: true, + placeholder: __( 'Search posts…' ), } ); useCommandLoader( { name: 'core/edit-site/navigate-templates', group: __( 'Templates' ), hook: useTemplateNavigationCommandLoader, + isNested: true, + placeholder: __( 'Search templates…' ), } ); useCommandLoader( { name: 'core/edit-site/navigate-template-parts', group: __( 'Template Parts' ), hook: useTemplatePartNavigationCommandLoader, + isNested: true, + placeholder: __( 'Search template parts…' ), } ); }