diff --git a/packages/patterns/src/components/category-selector.js b/packages/patterns/src/components/category-selector.js
index 397d851d3886b..7f00350e278ec 100644
--- a/packages/patterns/src/components/category-selector.js
+++ b/packages/patterns/src/components/category-selector.js
@@ -4,8 +4,6 @@
import { __ } from '@wordpress/i18n';
import { useMemo, useState } from '@wordpress/element';
import { FormTokenField } from '@wordpress/components';
-import { useSelect } from '@wordpress/data';
-import { store as coreStore } from '@wordpress/core-data';
import { useDebounce } from '@wordpress/compose';
import { decodeEntities } from '@wordpress/html-entities';
@@ -13,40 +11,29 @@ const unescapeString = ( arg ) => {
return decodeEntities( arg );
};
-const EMPTY_ARRAY = [];
-const MAX_TERMS_SUGGESTIONS = 20;
-const DEFAULT_QUERY = {
- per_page: MAX_TERMS_SUGGESTIONS,
- _fields: 'id,name',
- context: 'view',
-};
export const CATEGORY_SLUG = 'wp_pattern_category';
-export default function CategorySelector( { values, onChange } ) {
+export default function CategorySelector( {
+ categoryTerms,
+ onChange,
+ categoryMap,
+} ) {
const [ search, setSearch ] = useState( '' );
const debouncedSearch = useDebounce( setSearch, 500 );
- const { searchResults } = useSelect(
- ( select ) => {
- const { getEntityRecords } = select( coreStore );
-
- return {
- searchResults: !! search
- ? getEntityRecords( 'taxonomy', CATEGORY_SLUG, {
- ...DEFAULT_QUERY,
- search,
- } )
- : EMPTY_ARRAY,
- };
- },
- [ search ]
- );
-
const suggestions = useMemo( () => {
- return ( searchResults ?? [] ).map( ( term ) =>
- unescapeString( term.name )
- );
- }, [ searchResults ] );
+ return Array.from( categoryMap.values() )
+ .map( ( category ) => unescapeString( category.label ) )
+ .filter( ( category ) => {
+ if ( search !== '' ) {
+ return category
+ .toLowerCase()
+ .includes( search.toLowerCase() );
+ }
+ return true;
+ } )
+ .sort( ( a, b ) => a.localeCompare( b ) );
+ }, [ search, categoryMap ] );
function handleChange( termNames ) {
const uniqueTerms = termNames.reduce( ( terms, newTerm ) => {
@@ -64,17 +51,16 @@ export default function CategorySelector( { values, onChange } ) {
}
return (
- <>
-
- >
+
);
}
diff --git a/packages/patterns/src/components/create-pattern-modal.js b/packages/patterns/src/components/create-pattern-modal.js
index 531936da5e5c2..37dd725ef9226 100644
--- a/packages/patterns/src/components/create-pattern-modal.js
+++ b/packages/patterns/src/components/create-pattern-modal.js
@@ -10,8 +10,8 @@ import {
ToggleControl,
} from '@wordpress/components';
import { __ } from '@wordpress/i18n';
-import { useState } from '@wordpress/element';
-import { useDispatch } from '@wordpress/data';
+import { useState, useMemo } from '@wordpress/element';
+import { useDispatch, useSelect } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';
import { store as coreStore } from '@wordpress/core-data';
@@ -42,6 +42,42 @@ export default function CreatePatternModal( {
const { saveEntityRecord, invalidateResolution } = useDispatch( coreStore );
const { createErrorNotice } = useDispatch( noticesStore );
+ const { corePatternCategories, userPatternCategories } = useSelect(
+ ( select ) => {
+ const { getUserPatternCategories, getBlockPatternCategories } =
+ select( coreStore );
+
+ return {
+ corePatternCategories: getBlockPatternCategories(),
+ userPatternCategories: getUserPatternCategories(),
+ };
+ }
+ );
+
+ const categoryMap = useMemo( () => {
+ // Merge the user and core pattern categories and remove any duplicates.
+ const uniqueCategories = new Map();
+ [ ...userPatternCategories, ...corePatternCategories ].forEach(
+ ( category ) => {
+ if (
+ ! uniqueCategories.has( category.label ) &&
+ // There are two core categories with `Post` label so explicitly remove the one with
+ // the `query` slug to avoid any confusion.
+ category.name !== 'query'
+ ) {
+ // We need to store the name separately as this is used as the slug in the
+ // taxonomy and may vary from the label.
+ uniqueCategories.set( category.label, {
+ label: category.label,
+ value: category.label,
+ name: category.name,
+ } );
+ }
+ }
+ );
+ return uniqueCategories;
+ }, [ userPatternCategories, corePatternCategories ] );
+
async function onCreate( patternTitle, sync ) {
if ( ! title || isSaving ) {
return;
@@ -84,10 +120,16 @@ export default function CreatePatternModal( {
*/
async function findOrCreateTerm( term ) {
try {
+ // We need to match any existing term to the correct slug to prevent duplicates, eg.
+ // the core `Headers` category uses the singular `header` as the slug.
+ const existingTerm = categoryMap.get( term );
+ const termData = existingTerm
+ ? { name: existingTerm.label, slug: existingTerm.name }
+ : { name: term };
const newTerm = await saveEntityRecord(
'taxonomy',
CATEGORY_SLUG,
- { name: term },
+ termData,
{ throwOnError: true }
);
invalidateResolution( 'getUserPatternCategories' );
@@ -126,8 +168,9 @@ export default function CreatePatternModal( {
className="patterns-create-modal__name-input"
/>
[role="document"] {
+ width: 350px;
+ }
+
.patterns-menu-items__convert-modal-categories {
- max-width: 300px;
+ width: 100%;
+ position: relative;
+ min-height: 40px;
+ }
+ .components-form-token-field__suggestions-list {
+ position: absolute;
+ box-sizing: border-box;
+ z-index: 1;
+ background-color: $white;
+ // Account for the border width of the token field.
+ width: calc(100% + 2px);
+ left: -1px;
+ min-width: initial;
+ border: 1px solid var(--wp-admin-theme-color);
+ border-top: none;
+ box-shadow: 0 0 0 0.5px var(--wp-admin-theme-color);
+ border-bottom-left-radius: 2px;
+ border-bottom-right-radius: 2px;
}
}
.patterns-create-modal__name-input input[type="text"] {
- min-height: 34px;
+ // Match the minimal height of the category selector.
+ min-height: 40px;
+ // Override the default 1px margin-x.
+ margin: 0;
}