From 6bd8578eab3cc8cdd074dc2c86d0c4b3901249f5 Mon Sep 17 00:00:00 2001 From: Glen Davies Date: Mon, 9 Oct 2023 13:35:35 +1300 Subject: [PATCH] Patterns: Add category selector to pattern creation modal (#55024) --------- Co-authored-by: Kai Hao --- .../src/components/category-selector.js | 70 ++++++++----------- .../src/components/create-pattern-modal.js | 51 ++++++++++++-- packages/patterns/src/components/style.scss | 30 +++++++- 3 files changed, 103 insertions(+), 48 deletions(-) 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; }