diff --git a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/constants.js b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/constants.js index c309753799a447..465446c475d07f 100644 --- a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/constants.js +++ b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/constants.js @@ -28,3 +28,5 @@ export const MENU_TEMPLATES = 'templates'; export const MENU_TEMPLATES_ALL = 'templates-all'; export const MENU_TEMPLATES_PAGES = 'templates-pages'; export const MENU_TEMPLATES_POSTS = 'templates-posts'; + +export const SEARCH_DEBOUNCE_IN_MS = 75; diff --git a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/content-navigation-item.js b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/content-navigation-item.js new file mode 100644 index 00000000000000..2ab7aec2d0c921 --- /dev/null +++ b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/content-navigation-item.js @@ -0,0 +1,41 @@ +/** + * WordPress dependencies + */ +import { __experimentalNavigationItem as NavigationItem } from '@wordpress/components'; +import { useDispatch } from '@wordpress/data'; +import { useCallback } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { getPathAndQueryString } from '@wordpress/url'; + +const getTitle = ( entity ) => + entity.taxonomy ? entity.name : entity?.title?.rendered; + +export default function ContentNavigationItem( { item } ) { + const { setPage } = useDispatch( 'core/edit-site' ); + + const onActivateItem = useCallback( () => { + const { type, slug, link, id } = item; + setPage( { + type, + slug, + path: getPathAndQueryString( link ), + context: { + postType: type, + postId: id, + }, + } ); + }, [ setPage, item ] ); + + if ( ! item ) { + return null; + } + + return ( + + ); +} diff --git a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/content-categories.js b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/content-categories.js index f2243940aca5b1..31a5249de4f544 100644 --- a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/content-categories.js +++ b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/content-categories.js @@ -1,23 +1,85 @@ /** * WordPress dependencies */ -import { __experimentalNavigationMenu as NavigationMenu } from '@wordpress/components'; +import { + __experimentalNavigationMenu as NavigationMenu, + __experimentalNavigationItem as NavigationItem, +} from '@wordpress/components'; import { __ } from '@wordpress/i18n'; +import { useSelect } from '@wordpress/data'; /** * Internal dependencies */ -import NavigationEntityItems from '../navigation-entity-items'; import { MENU_CONTENT_CATEGORIES, MENU_ROOT } from '../constants'; +import ContentNavigationItem from '../content-navigation-item'; +import SearchResults from '../search-results'; +import useDebouncedSearch from '../use-debounced-search'; export default function ContentCategoriesMenu() { + const { + search, + searchQuery, + onSearch, + isDebouncing, + } = useDebouncedSearch(); + + const { categories, isResolved } = useSelect( + ( select ) => { + const { getEntityRecords, hasFinishedResolution } = select( + 'core' + ); + const getEntityRecordsArgs = [ + 'taxonomy', + 'category', + { + search: searchQuery, + }, + ]; + const hasResolvedPosts = hasFinishedResolution( + 'getEntityRecords', + getEntityRecordsArgs + ); + return { + categories: getEntityRecords( ...getEntityRecordsArgs ), + isResolved: hasResolvedPosts, + }; + }, + [ searchQuery ] + ); + + const shouldShowLoadingForDebouncing = search && isDebouncing; + const showLoading = ! isResolved || shouldShowLoadingForDebouncing; + return ( - + { search && ! isDebouncing && ( + + ) } + + { ! search && + categories?.map( ( category ) => ( + + ) ) } + + { showLoading && ( + + ) } ); } diff --git a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/content-pages.js b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/content-pages.js index bfa08369f338da..f91bfe23bacad6 100644 --- a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/content-pages.js +++ b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/content-pages.js @@ -1,23 +1,85 @@ /** * WordPress dependencies */ -import { __experimentalNavigationMenu as NavigationMenu } from '@wordpress/components'; +import { + __experimentalNavigationMenu as NavigationMenu, + __experimentalNavigationItem as NavigationItem, +} from '@wordpress/components'; import { __ } from '@wordpress/i18n'; +import { useSelect } from '@wordpress/data'; /** * Internal dependencies */ -import NavigationEntityItems from '../navigation-entity-items'; import { MENU_CONTENT_PAGES, MENU_ROOT } from '../constants'; +import ContentNavigationItem from '../content-navigation-item'; +import SearchResults from '../search-results'; +import useDebouncedSearch from '../use-debounced-search'; export default function ContentPagesMenu() { + const { + search, + searchQuery, + onSearch, + isDebouncing, + } = useDebouncedSearch(); + + const { pages, isResolved } = useSelect( + ( select ) => { + const { getEntityRecords, hasFinishedResolution } = select( + 'core' + ); + const getEntityRecordsArgs = [ + 'postType', + 'page', + { + search: searchQuery, + }, + ]; + const hasResolvedPosts = hasFinishedResolution( + 'getEntityRecords', + getEntityRecordsArgs + ); + return { + pages: getEntityRecords( ...getEntityRecordsArgs ), + isResolved: hasResolvedPosts, + }; + }, + [ searchQuery ] + ); + + const shouldShowLoadingForDebouncing = search && isDebouncing; + const showLoading = ! isResolved || shouldShowLoadingForDebouncing; + return ( - + { search && ! isDebouncing && ( + + ) } + + { ! search && + pages?.map( ( page ) => ( + + ) ) } + + { showLoading && ( + + ) } ); } diff --git a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/content-posts.js b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/content-posts.js index 26f9de091a0a3a..23f81bb710d9a1 100644 --- a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/content-posts.js +++ b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/content-posts.js @@ -5,27 +5,58 @@ import { __experimentalNavigationMenu as NavigationMenu, __experimentalNavigationItem as NavigationItem, } from '@wordpress/components'; -import { useDispatch, useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; +import { useCallback } from '@wordpress/element'; +import { useDispatch, useSelect } from '@wordpress/data'; /** * Internal dependencies */ -import NavigationEntityItems from '../navigation-entity-items'; import { MENU_CONTENT_POSTS, MENU_ROOT } from '../constants'; +import ContentNavigationItem from '../content-navigation-item'; +import SearchResults from '../search-results'; +import useDebouncedSearch from '../use-debounced-search'; import { store as editSiteStore } from '../../../../store'; export default function ContentPostsMenu() { - const showOnFront = useSelect( - ( select ) => - select( 'core' ).getEditedEntityRecord( 'root', 'site' ) - .show_on_front, - [] + const { + search, + searchQuery, + onSearch, + isDebouncing, + } = useDebouncedSearch(); + + const { posts, showOnFront, isResolved } = useSelect( + ( select ) => { + const { + getEntityRecords, + getEditedEntityRecord, + hasFinishedResolution, + } = select( 'core' ); + const getEntityRecodsArgs = [ + 'postType', + 'post', + { + search: searchQuery, + }, + ]; + const hasResolvedPosts = hasFinishedResolution( + 'getEntityRecords', + getEntityRecodsArgs + ); + return { + posts: getEntityRecords( ...getEntityRecodsArgs ), + isResolved: hasResolvedPosts, + showOnFront: getEditedEntityRecord( 'root', 'site' ) + .show_on_front, + }; + }, + [ searchQuery ] ); const { setPage } = useDispatch( editSiteStore ); - const onActivateFrontItem = () => { + const onActivateFrontItem = useCallback( () => { setPage( { type: 'page', path: '/', @@ -34,22 +65,51 @@ export default function ContentPostsMenu() { queryContext: { page: 1 }, }, } ); - }; + }, [ setPage ] ); + + const shouldShowLoadingForDebouncing = search && isDebouncing; + const showLoading = ! isResolved || shouldShowLoadingForDebouncing; return ( - { showOnFront === 'posts' && ( - ) } - + + { ! search && ( + <> + { showOnFront === 'posts' && ( + + ) } + + { posts?.map( ( post ) => ( + + ) ) } + + ) } + + { showLoading && ( + + ) } ); } diff --git a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/search-results.js b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/search-results.js index 39e7b60cb1781c..e18f1009430d58 100644 --- a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/search-results.js +++ b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/search-results.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { map } from 'lodash'; +import { map, sortBy, keyBy } from 'lodash'; /** * WordPress dependencies @@ -16,12 +16,24 @@ import { __ } from '@wordpress/i18n'; import { normalizedSearch } from './utils'; import { useSelect } from '@wordpress/data'; import TemplateNavigationItem from './template-navigation-item'; +import ContentNavigationItem from './content-navigation-item'; -export default function SearchResults( { items, search } ) { - const itemType = items?.length > 0 ? items[ 0 ].type : null; +export default function SearchResults( { items, search, disableFilter } ) { + let itemType = null; + if ( items?.length > 0 ) { + if ( items[ 0 ].taxonomy ) { + itemType = 'taxonomy'; + } else { + itemType = items[ 0 ].type; + } + } const itemInfos = useSelect( ( select ) => { + if ( itemType === null || items === null ) { + return []; + } + if ( itemType === 'wp_template' ) { const { __experimentalGetTemplateInfo: getTemplateInfo, @@ -33,6 +45,14 @@ export default function SearchResults( { items, search } ) { } ) ); } + if ( itemType === 'taxonomy' ) { + return items.map( ( item ) => ( { + slug: item.slug, + title: item.name, + description: item.description, + } ) ); + } + return items.map( ( item ) => ( { slug: item.slug, title: item.title?.rendered, @@ -41,16 +61,21 @@ export default function SearchResults( { items, search } ) { }, [ items, itemType ] ); + const itemInfosMap = useMemo( () => keyBy( itemInfos, 'slug' ), [ + itemInfos, + ] ); const itemsFiltered = useMemo( () => { if ( items === null || search.length === 0 ) { return []; } + if ( disableFilter ) { + return items; + } + return items.filter( ( { slug } ) => { - const { title, description } = itemInfos.find( - ( info ) => info.slug === slug - ); + const { title, description } = itemInfosMap[ slug ]; return ( normalizedSearch( slug, search ) || @@ -60,12 +85,30 @@ export default function SearchResults( { items, search } ) { } ); }, [ items, itemInfos, search ] ); + const itemsSorted = useMemo( () => { + if ( ! itemsFiltered ) { + return []; + } + + return sortBy( itemsFiltered, [ + ( { slug } ) => { + const { title } = itemInfosMap[ slug ]; + return ! normalizedSearch( title, search ); + }, + ] ); + }, [ itemsFiltered, search ] ); + + const ItemComponent = + itemType === 'wp_template' || itemType === 'wp_template_part' + ? TemplateNavigationItem + : ContentNavigationItem; + return ( - { map( itemsFiltered, ( item ) => ( - ( + ) ) } diff --git a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/use-debounced-search.js b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/use-debounced-search.js new file mode 100644 index 00000000000000..eb1f75dcd61b78 --- /dev/null +++ b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/use-debounced-search.js @@ -0,0 +1,47 @@ +/** + * External dependencies + */ +import { debounce } from 'lodash'; + +/** + * WordPress dependencies + */ +import { useState, useCallback, useEffect } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { SEARCH_DEBOUNCE_IN_MS } from './constants'; + +export default function useDebouncedSearch() { + // The value used by the NavigationMenu to control the input field. + const [ search, setSearch ] = useState( '' ); + // The value used to actually perform the search query. + const [ searchQuery, setSearchQuery ] = useState( '' ); + const [ isDebouncing, setIsDebouncing ] = useState( false ); + + useEffect( () => { + setIsDebouncing( false ); + }, [ searchQuery ] ); + + const debouncedSetSearchQuery = useCallback( + debounce( setSearchQuery, SEARCH_DEBOUNCE_IN_MS ), + [ setSearchQuery ] + ); + + const onSearch = useCallback( + ( value ) => { + setSearch( value ); + debouncedSetSearchQuery( value ); + setIsDebouncing( true ); + }, + [ setSearch, setIsDebouncing, debouncedSetSearchQuery ] + ); + + return { + search, + searchQuery, + isDebouncing, + onSearch, + }; +}