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,
+ };
+}