From 359d1da26c35411446219f6eb2bb0f9e634f3257 Mon Sep 17 00:00:00 2001 From: Glen Davies Date: Thu, 14 Sep 2023 16:32:54 +1200 Subject: [PATCH] Patterns: Add user categories to site editor sidebar navigation screen (#53837) Co-authored-by: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> --- .../page-patterns/duplicate-menu-item.js | 63 ++++++++++++++---- .../src/components/page-patterns/grid-item.js | 15 ++--- .../src/components/page-patterns/header.js | 13 +--- .../components/page-patterns/patterns-list.js | 4 +- .../components/page-patterns/search-items.js | 14 +++- .../components/page-patterns/use-patterns.js | 66 ++++++++++++++----- .../src/components/page-patterns/utils.js | 5 +- .../index.js | 54 +++------------ .../use-my-patterns.js | 24 ------- .../use-pattern-categories.js | 54 +++++++++++++-- 10 files changed, 182 insertions(+), 130 deletions(-) delete mode 100644 packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-my-patterns.js diff --git a/packages/edit-site/src/components/page-patterns/duplicate-menu-item.js b/packages/edit-site/src/components/page-patterns/duplicate-menu-item.js index 994ed168cd186..324b22e460447 100644 --- a/packages/edit-site/src/components/page-patterns/duplicate-menu-item.js +++ b/packages/edit-site/src/components/page-patterns/duplicate-menu-item.js @@ -11,19 +11,14 @@ import { privateApis as routerPrivateApis } from '@wordpress/router'; /** * Internal dependencies */ -import { - TEMPLATE_PARTS, - PATTERNS, - SYNC_TYPES, - USER_PATTERNS, - USER_PATTERN_CATEGORY, -} from './utils'; +import { TEMPLATE_PARTS, PATTERNS, SYNC_TYPES, USER_PATTERNS } from './utils'; import { useExistingTemplateParts, getUniqueTemplatePartTitle, getCleanTemplatePartSlug, } from '../../utils/template-part-create'; import { unlock } from '../../lock-unlock'; +import usePatternCategories from '../sidebar-navigation-screen-patterns/use-pattern-categories'; const { useHistory } = unlock( routerPrivateApis ); @@ -32,11 +27,11 @@ function getPatternMeta( item ) { return { wp_pattern_sync_status: SYNC_TYPES.unsynced }; } - const syncStatus = item.reusableBlock.wp_pattern_sync_status; + const syncStatus = item.patternBlock.wp_pattern_sync_status; const isUnsynced = syncStatus === SYNC_TYPES.unsynced; return { - ...item.reusableBlock.meta, + ...item.patternBlock.meta, wp_pattern_sync_status: isUnsynced ? syncStatus : undefined, }; } @@ -47,12 +42,13 @@ export default function DuplicateMenuItem( { label = __( 'Duplicate' ), onClose, } ) { - const { saveEntityRecord } = useDispatch( coreStore ); + const { saveEntityRecord, invalidateResolution } = useDispatch( coreStore ); const { createErrorNotice, createSuccessNotice } = useDispatch( noticesStore ); const history = useHistory(); const existingTemplateParts = useExistingTemplateParts(); + const { patternCategories } = usePatternCategories(); async function createTemplatePart() { try { @@ -111,6 +107,45 @@ export default function DuplicateMenuItem( { } } + async function findOrCreateTerm( term ) { + try { + const newTerm = await saveEntityRecord( + 'taxonomy', + 'wp_pattern_category', + { + name: term.label, + slug: term.name, + description: term.description, + }, + { + throwOnError: true, + } + ); + invalidateResolution( 'getUserPatternCategories' ); + return newTerm.id; + } catch ( error ) { + if ( error.code !== 'term_exists' ) { + throw error; + } + + return error.data.term_id; + } + } + + async function getCategories( categories ) { + const terms = categories.map( ( category ) => { + const fullCategory = patternCategories.find( + ( cat ) => cat.name === category + ); + if ( fullCategory.id ) { + return fullCategory.id; + } + return findOrCreateTerm( fullCategory ); + } ); + + return Promise.all( terms ); + } + async function createPattern() { try { const isThemePattern = item.type === PATTERNS; @@ -119,6 +154,7 @@ export default function DuplicateMenuItem( { __( '%s (Copy)' ), item.title ); + const categories = await getCategories( item.categories ); const result = await saveEntityRecord( 'postType', @@ -126,10 +162,11 @@ export default function DuplicateMenuItem( { { content: isThemePattern ? item.content - : item.reusableBlock.content, + : item.patternBlock.content, meta: getPatternMeta( item ), status: 'publish', title, + wp_pattern_category: categories, }, { throwOnError: true } ); @@ -147,8 +184,8 @@ export default function DuplicateMenuItem( { ); history.push( { - categoryType: USER_PATTERNS, - categoryId: USER_PATTERN_CATEGORY, + categoryType: PATTERNS, + categoryId, postType: USER_PATTERNS, postId: result?.id, } ); diff --git a/packages/edit-site/src/components/page-patterns/grid-item.js b/packages/edit-site/src/components/page-patterns/grid-item.js index a4797b7af4c7d..780660036b9d3 100644 --- a/packages/edit-site/src/components/page-patterns/grid-item.js +++ b/packages/edit-site/src/components/page-patterns/grid-item.js @@ -119,9 +119,12 @@ function GridItem( { categoryId, item, ...props } ) { ); } - const itemIcon = - templatePartIcons[ categoryId ] || - ( item.syncStatus === SYNC_TYPES.full ? symbol : undefined ); + let itemIcon; + if ( ! isUserPattern && templatePartIcons[ categoryId ] ) { + itemIcon = templatePartIcons[ categoryId ]; + } else { + itemIcon = item.syncStatus === SYNC_TYPES.full ? symbol : undefined; + } const confirmButtonText = hasThemeFile ? __( 'Clear' ) : __( 'Delete' ); const confirmPrompt = hasThemeFile @@ -246,11 +249,7 @@ function GridItem( { categoryId, item, ...props } ) { categoryId={ categoryId } item={ item } onClose={ onClose } - label={ - isNonUserPattern - ? __( 'Copy to My patterns' ) - : __( 'Duplicate' ) - } + label={ __( 'Duplicate' ) } /> { isCustomPattern && ( area.area === categoryId ); diff --git a/packages/edit-site/src/components/page-patterns/patterns-list.js b/packages/edit-site/src/components/page-patterns/patterns-list.js index 01525fc5dccab..57c342ca3caa9 100644 --- a/packages/edit-site/src/components/page-patterns/patterns-list.js +++ b/packages/edit-site/src/components/page-patterns/patterns-list.js @@ -27,7 +27,7 @@ import usePatterns from './use-patterns'; import SidebarButton from '../sidebar-button'; import useDebouncedInput from '../../utils/use-debounced-input'; import { unlock } from '../../lock-unlock'; -import { SYNC_TYPES, USER_PATTERN_CATEGORY, PATTERNS } from './utils'; +import { SYNC_TYPES, PATTERNS } from './utils'; import Pagination from './pagination'; const { useLocation, useHistory } = unlock( routerPrivateApis ); @@ -155,7 +155,7 @@ export default function PatternsList( { categoryId, type } ) { __nextHasNoMarginBottom /> - { categoryId === USER_PATTERN_CATEGORY && ( + { type === PATTERNS && ( item.name || ''; const defaultGetTitle = ( item ) => item.title; @@ -84,7 +89,9 @@ const removeMatchingTerms = ( unmatchedTerms, unprocessedTerms ) => { */ export const searchItems = ( items = [], searchInput = '', config = {} ) => { const normalizedSearchTerms = getNormalizedSearchTerms( searchInput ); - const onlyFilterByCategory = ! normalizedSearchTerms.length; + const onlyFilterByCategory = + config.categoryId !== ALL_PATTERNS_CATEGORY && + ! normalizedSearchTerms.length; const searchRankConfig = { ...config, onlyFilterByCategory }; // If we aren't filtering on search terms, matching on category is satisfactory. @@ -131,7 +138,10 @@ function getItemSearchRank( item, searchTerm, config ) { onlyFilterByCategory, } = config; - let rank = hasCategory( item, categoryId ) ? 1 : 0; + let rank = + categoryId === ALL_PATTERNS_CATEGORY || hasCategory( item, categoryId ) + ? 1 + : 0; // If an item doesn't belong to the current category or we don't have // search terms to filter by, return the initial rank value. diff --git a/packages/edit-site/src/components/page-patterns/use-patterns.js b/packages/edit-site/src/components/page-patterns/use-patterns.js index 4aeb6527bca26..25919d5556710 100644 --- a/packages/edit-site/src/components/page-patterns/use-patterns.js +++ b/packages/edit-site/src/components/page-patterns/use-patterns.js @@ -87,7 +87,7 @@ const selectTemplatePartsAsPatterns = ( return { patterns, isResolving }; }; -const selectThemePatterns = ( select, { categoryId, search = '' } = {} ) => { +const selectThemePatterns = ( select ) => { const { getSettings } = unlock( select( editSiteStore ) ); const settings = getSettings(); const blockPatterns = @@ -96,7 +96,7 @@ const selectThemePatterns = ( select, { categoryId, search = '' } = {} ) => { const restBlockPatterns = select( coreStore ).getBlockPatterns(); - let patterns = [ + const patterns = [ ...( blockPatterns || [] ), ...( restBlockPatterns || [] ), ] @@ -114,6 +114,23 @@ const selectThemePatterns = ( select, { categoryId, search = '' } = {} ) => { } ), } ) ); + return { patterns, isResolving: false }; +}; +const selectPatterns = ( + select, + { categoryId, search = '', syncStatus } = {} +) => { + const { patterns: themePatterns } = selectThemePatterns( select ); + const { patterns: userPatterns } = selectUserPatterns( select ); + + let patterns = [ ...( themePatterns || [] ), ...( userPatterns || [] ) ]; + + if ( syncStatus ) { + patterns = patterns.filter( + ( pattern ) => pattern.syncStatus === syncStatus + ); + } + if ( categoryId ) { patterns = searchItems( patterns, search, { categoryId, @@ -125,32 +142,43 @@ const selectThemePatterns = ( select, { categoryId, search = '' } = {} ) => { hasCategory: ( item ) => ! item.hasOwnProperty( 'categories' ), } ); } - return { patterns, isResolving: false }; }; -const reusableBlockToPattern = ( reusableBlock ) => ( { - blocks: parse( reusableBlock.content.raw, { +const patternBlockToPattern = ( patternBlock, categories ) => ( { + blocks: parse( patternBlock.content.raw, { __unstableSkipMigrationLogs: true, } ), - categories: reusableBlock.wp_pattern, - id: reusableBlock.id, - name: reusableBlock.slug, - syncStatus: reusableBlock.wp_pattern_sync_status || SYNC_TYPES.full, - title: reusableBlock.title.raw, - type: reusableBlock.type, - reusableBlock, + ...( patternBlock.wp_pattern_category.length > 0 && { + categories: patternBlock.wp_pattern_category.map( + ( patternCategoryId ) => + categories && categories.get( patternCategoryId ) + ? categories.get( patternCategoryId ).slug + : patternCategoryId + ), + } ), + id: patternBlock.id, + name: patternBlock.slug, + syncStatus: patternBlock.wp_pattern_sync_status || SYNC_TYPES.full, + title: patternBlock.title.raw, + type: USER_PATTERNS, + patternBlock, } ); const selectUserPatterns = ( select, { search = '', syncStatus } = {} ) => { - const { getEntityRecords, getIsResolving } = select( coreStore ); + const { getEntityRecords, getIsResolving, getUserPatternCategories } = + select( coreStore ); const query = { per_page: -1 }; const records = getEntityRecords( 'postType', USER_PATTERNS, query ); + const categories = getUserPatternCategories(); let patterns = records - ? records.map( ( record ) => reusableBlockToPattern( record ) ) + ? records.map( ( record ) => + patternBlockToPattern( record, categories.patternCategoriesMap ) + ) : EMPTY_PATTERN_LIST; + const isResolving = getIsResolving( 'getEntityRecords', [ 'postType', USER_PATTERNS, @@ -170,13 +198,13 @@ const selectUserPatterns = ( select, { search = '', syncStatus } = {} ) => { hasCategory: () => true, } ); - return { patterns, isResolving }; + return { patterns, isResolving, categories: categories.patternCategories }; }; export const usePatterns = ( categoryType, categoryId, - { search = '', syncStatus } + { search = '', syncStatus } = {} ) => { return useSelect( ( select ) => { @@ -186,7 +214,11 @@ export const usePatterns = ( search, } ); } else if ( categoryType === PATTERNS ) { - return selectThemePatterns( select, { categoryId, search } ); + return selectPatterns( select, { + categoryId, + search, + syncStatus, + } ); } else if ( categoryType === USER_PATTERNS ) { return selectUserPatterns( select, { search, syncStatus } ); } diff --git a/packages/edit-site/src/components/page-patterns/utils.js b/packages/edit-site/src/components/page-patterns/utils.js index bbdff872fe355..ee22f4715c63a 100644 --- a/packages/edit-site/src/components/page-patterns/utils.js +++ b/packages/edit-site/src/components/page-patterns/utils.js @@ -1,6 +1,7 @@ -export const DEFAULT_CATEGORY = 'my-patterns'; -export const DEFAULT_TYPE = 'wp_block'; +export const ALL_PATTERNS_CATEGORY = 'all-patterns'; +export const DEFAULT_CATEGORY = ALL_PATTERNS_CATEGORY; export const PATTERNS = 'pattern'; +export const DEFAULT_TYPE = PATTERNS; export const TEMPLATE_PARTS = 'wp_template_part'; export const USER_PATTERNS = 'wp_block'; export const USER_PATTERN_CATEGORY = 'my-patterns'; diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/index.js index 3b6a6a8110f56..a6e1d9a9008d9 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/index.js @@ -4,16 +4,13 @@ import { __experimentalItemGroup as ItemGroup, __experimentalItem as Item, - Flex, - Icon, - Tooltip, __experimentalHeading as Heading, } from '@wordpress/components'; import { useViewportMatch } from '@wordpress/compose'; import { getTemplatePartIcon } from '@wordpress/editor'; -import { __, sprintf } from '@wordpress/i18n'; +import { __ } from '@wordpress/i18n'; import { getQueryArgs } from '@wordpress/url'; -import { file, starFilled, lockSmall } from '@wordpress/icons'; +import { file } from '@wordpress/icons'; /** * Internal dependencies @@ -25,7 +22,6 @@ import CategoryItem from './category-item'; import { DEFAULT_CATEGORY, DEFAULT_TYPE } from '../page-patterns/utils'; import { useLink } from '../routes/link'; import usePatternCategories from './use-pattern-categories'; -import useMyPatterns from './use-my-patterns'; import useTemplatePartAreas from './use-template-part-areas'; function TemplatePartGroup( { areas, currentArea, currentType } ) { @@ -56,7 +52,11 @@ function TemplatePartGroup( { areas, currentArea, currentType } ) { ); } -function ThemePatternsGroup( { categories, currentCategory, currentType } ) { +function PatternCategoriesGroup( { + categories, + currentCategory, + currentType, +} ) { return ( <> @@ -64,23 +64,7 @@ function ThemePatternsGroup( { categories, currentCategory, currentType } ) { - { category.label } - - - - - - - } + label={ category.label } icon={ file } id={ category.name } type="pattern" @@ -104,7 +88,6 @@ export default function SidebarNavigationScreenPatterns() { const { templatePartAreas, hasTemplateParts, isLoading } = useTemplatePartAreas(); const { patternCategories, hasPatterns } = usePatternCategories(); - const { myPatterns } = useMyPatterns(); const templatePartsLink = useLink( { path: '/wp_template_part/all' } ); const footer = ! isMobileViewport ? ( @@ -144,27 +127,8 @@ export default function SidebarNavigationScreenPatterns() { ) } - - - { hasPatterns && ( - - select( coreStore ).getEntityRecords( 'postType', 'wp_block', { - per_page: -1, - } )?.length ?? 0 - ); - - return { - myPatterns: { - count: myPatternsCount, - name: 'my-patterns', - label: __( 'My patterns' ), - }, - hasPatterns: myPatternsCount > 0, - }; -} diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-pattern-categories.js b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-pattern-categories.js index da4732f5be448..e3ab86e8b0615 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-pattern-categories.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-pattern-categories.js @@ -9,6 +9,8 @@ import { __ } from '@wordpress/i18n'; */ import useDefaultPatternCategories from './use-default-pattern-categories'; import useThemePatterns from './use-theme-patterns'; +import usePatterns from '../page-patterns/use-patterns'; +import { USER_PATTERNS, ALL_PATTERNS_CATEGORY } from '../page-patterns/utils'; export default function usePatternCategories() { const defaultCategories = useDefaultPatternCategories(); @@ -17,6 +19,8 @@ export default function usePatternCategories() { label: __( 'Uncategorized' ), } ); const themePatterns = useThemePatterns(); + const { patterns: userPatterns, categories: userPatternCategories } = + usePatterns( USER_PATTERNS ); const patternCategories = useMemo( () => { const categoryMap = {}; @@ -28,6 +32,11 @@ export default function usePatternCategories() { categoryMap[ category.name ] = { ...category, count: 0 }; } } ); + userPatternCategories.forEach( ( category ) => { + if ( ! categoryMap[ category.name ] ) { + categoryMap[ category.name ] = { ...category, count: 0 }; + } + } ); // Update the category counts to reflect theme registered patterns. themePatterns.forEach( ( pattern ) => { @@ -42,15 +51,48 @@ export default function usePatternCategories() { } } ); - // Filter categories so we only have those containing patterns. - defaultCategories.forEach( ( category ) => { - if ( categoryMap[ category.name ].count ) { - categoriesWithCounts.push( categoryMap[ category.name ] ); + // Update the category counts to reflect user registered patterns. + userPatterns.forEach( ( pattern ) => { + pattern.categories?.forEach( ( category ) => { + if ( categoryMap[ category ] ) { + categoryMap[ category ].count += 1; + } + } ); + // If the pattern has no categories, add it to uncategorized. + if ( ! pattern.categories?.length ) { + categoryMap.uncategorized.count += 1; } } ); - return categoriesWithCounts; - }, [ defaultCategories, themePatterns ] ); + // Filter categories so we only have those containing patterns. + [ ...defaultCategories, ...userPatternCategories ].forEach( + ( category ) => { + if ( + categoryMap[ category.name ].count && + ! categoriesWithCounts.find( + ( cat ) => cat.name === category.name + ) + ) { + categoriesWithCounts.push( categoryMap[ category.name ] ); + } + } + ); + const sortedCategories = categoriesWithCounts.sort( ( a, b ) => + a.label.localeCompare( b.label ) + ); + sortedCategories.unshift( { + name: ALL_PATTERNS_CATEGORY, + label: __( 'All Patterns' ), + description: __( 'A list of all patterns from all sources' ), + count: themePatterns.length + userPatterns.length, + } ); + return sortedCategories; + }, [ + defaultCategories, + themePatterns, + userPatternCategories, + userPatterns, + ] ); return { patternCategories, hasPatterns: !! patternCategories.length }; }