From fe3268dc1957b24ee4a6977967a47d07848e2ee1 Mon Sep 17 00:00:00 2001 From: Carolina Nymark Date: Wed, 28 Jun 2023 08:43:12 +0200 Subject: [PATCH 1/5] Add a new aria label to the category item --- .../category-item.js | 15 +++++++++++++++ .../sidebar-navigation-screen-library/index.js | 4 ++++ 2 files changed, 19 insertions(+) diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-library/category-item.js b/packages/edit-site/src/components/sidebar-navigation-screen-library/category-item.js index 2ea9205b6fca71..e0c94ee30c0acd 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-library/category-item.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-library/category-item.js @@ -4,6 +4,11 @@ import SidebarNavigationItem from '../sidebar-navigation-item'; import { useLink } from '../routes/link'; +/** + * WordPress dependencies + */ +import { sprintf, _n } from '@wordpress/i18n'; + export default function CategoryItem( { count, icon, @@ -11,6 +16,7 @@ export default function CategoryItem( { isActive, label, type, + typeLabel, } ) { const linkInfo = useLink( { @@ -30,12 +36,21 @@ export default function CategoryItem( { return; } + const ariaLabel = sprintf( + /* translators: %1$s is the category name, %2$s is the type (pattern or template part), %3$s is the number of items. */ + _n( '%1$s (%2$s), %3$s item', '%1$s (%2$s), %3$s items', count ), + label, + typeLabel ? typeLabel : type, + count + ); + return ( { count } } aria-current={ isActive ? 'true' : undefined } + aria-label={ ariaLabel } > { label } diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-library/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-library/index.js index d3cfcbece6be0e..b751d55265cf0a 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-library/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-library/index.js @@ -98,6 +98,10 @@ export default function SidebarNavigationScreenLibrary() { } id={ area } type="wp_template_part" + // A human readable label for the type. + typeLabel={ __( + 'template part' + ) } isActive={ currentCategory === area && currentType === From 7af94af4f11e6fecbd593e239a779a0a69d3e97d Mon Sep 17 00:00:00 2001 From: Carolina Nymark Date: Tue, 11 Jul 2023 07:11:34 +0200 Subject: [PATCH 2/5] try to solve merge conflict... --- .../category-item.js | 0 .../index.js | 0 .../style.scss | 0 .../use-default-pattern-categories.js | 0 .../use-pattern-categories.js | 0 .../use-template-part-areas.js | 0 .../use-theme-patterns.js | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename packages/edit-site/src/components/{sidebar-navigation-screen-library => sidebar-navigation-screen-patterns}/category-item.js (100%) rename packages/edit-site/src/components/{sidebar-navigation-screen-library => sidebar-navigation-screen-patterns}/index.js (100%) rename packages/edit-site/src/components/{sidebar-navigation-screen-library => sidebar-navigation-screen-patterns}/style.scss (100%) rename packages/edit-site/src/components/{sidebar-navigation-screen-library => sidebar-navigation-screen-patterns}/use-default-pattern-categories.js (100%) rename packages/edit-site/src/components/{sidebar-navigation-screen-library => sidebar-navigation-screen-patterns}/use-pattern-categories.js (100%) rename packages/edit-site/src/components/{sidebar-navigation-screen-library => sidebar-navigation-screen-patterns}/use-template-part-areas.js (100%) rename packages/edit-site/src/components/{sidebar-navigation-screen-library => sidebar-navigation-screen-patterns}/use-theme-patterns.js (100%) diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-library/category-item.js b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/category-item.js similarity index 100% rename from packages/edit-site/src/components/sidebar-navigation-screen-library/category-item.js rename to packages/edit-site/src/components/sidebar-navigation-screen-patterns/category-item.js diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-library/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/index.js similarity index 100% rename from packages/edit-site/src/components/sidebar-navigation-screen-library/index.js rename to packages/edit-site/src/components/sidebar-navigation-screen-patterns/index.js diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-library/style.scss b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/style.scss similarity index 100% rename from packages/edit-site/src/components/sidebar-navigation-screen-library/style.scss rename to packages/edit-site/src/components/sidebar-navigation-screen-patterns/style.scss diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-library/use-default-pattern-categories.js b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-default-pattern-categories.js similarity index 100% rename from packages/edit-site/src/components/sidebar-navigation-screen-library/use-default-pattern-categories.js rename to packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-default-pattern-categories.js diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-library/use-pattern-categories.js b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-pattern-categories.js similarity index 100% rename from packages/edit-site/src/components/sidebar-navigation-screen-library/use-pattern-categories.js rename to packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-pattern-categories.js diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-library/use-template-part-areas.js b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-template-part-areas.js similarity index 100% rename from packages/edit-site/src/components/sidebar-navigation-screen-library/use-template-part-areas.js rename to packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-template-part-areas.js diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-library/use-theme-patterns.js b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-theme-patterns.js similarity index 100% rename from packages/edit-site/src/components/sidebar-navigation-screen-library/use-theme-patterns.js rename to packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-theme-patterns.js From 6966657d261b918aefce0d986d43a8f5194fde3c Mon Sep 17 00:00:00 2001 From: Carolina Nymark Date: Tue, 11 Jul 2023 08:19:34 +0200 Subject: [PATCH 3/5] try to solve merge conflict --- .../page-patterns/duplicate-menu-item.js | 196 ++++++++++++ .../src/components/page-patterns/grid-item.js | 284 ++++++++++++++++++ .../src/components/page-patterns/grid.js | 52 ++++ .../src/components/page-patterns/header.js | 69 +++++ .../src/components/page-patterns/index.js | 44 +++ .../components/page-patterns/no-patterns.js | 12 + .../components/page-patterns/patterns-list.js | 156 ++++++++++ .../page-patterns/rename-menu-item.js | 115 +++++++ .../components/page-patterns/search-items.js | 171 +++++++++++ .../src/components/page-patterns/style.scss | 169 +++++++++++ .../page-patterns/use-pattern-settings.js | 51 ++++ .../components/page-patterns/use-patterns.js | 170 +++++++++++ .../src/components/page-patterns/utils.js | 21 ++ .../category-item.js | 21 +- .../index.js | 197 +++++++----- .../style.scss | 26 +- .../use-default-pattern-categories.js | 63 ++-- .../use-my-patterns.js | 24 ++ .../use-template-part-areas.js | 40 ++- .../use-theme-patterns.js | 2 +- .../edit-site/src/components/sidebar/index.js | 6 +- packages/edit-site/src/style.scss | 2 +- 22 files changed, 1761 insertions(+), 130 deletions(-) create mode 100644 packages/edit-site/src/components/page-patterns/duplicate-menu-item.js create mode 100644 packages/edit-site/src/components/page-patterns/grid-item.js create mode 100644 packages/edit-site/src/components/page-patterns/grid.js create mode 100644 packages/edit-site/src/components/page-patterns/header.js create mode 100644 packages/edit-site/src/components/page-patterns/index.js create mode 100644 packages/edit-site/src/components/page-patterns/no-patterns.js create mode 100644 packages/edit-site/src/components/page-patterns/patterns-list.js create mode 100644 packages/edit-site/src/components/page-patterns/rename-menu-item.js create mode 100644 packages/edit-site/src/components/page-patterns/search-items.js create mode 100644 packages/edit-site/src/components/page-patterns/style.scss create mode 100644 packages/edit-site/src/components/page-patterns/use-pattern-settings.js create mode 100644 packages/edit-site/src/components/page-patterns/use-patterns.js create mode 100644 packages/edit-site/src/components/page-patterns/utils.js create 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 new file mode 100644 index 00000000000000..d2c14d15f341b0 --- /dev/null +++ b/packages/edit-site/src/components/page-patterns/duplicate-menu-item.js @@ -0,0 +1,196 @@ +/** + * WordPress dependencies + */ +import { MenuItem } from '@wordpress/components'; +import { store as coreStore } from '@wordpress/core-data'; +import { useDispatch } from '@wordpress/data'; +import { __, sprintf } from '@wordpress/i18n'; +import { store as noticesStore } from '@wordpress/notices'; +import { privateApis as routerPrivateApis } from '@wordpress/router'; + +/** + * Internal dependencies + */ +import { + TEMPLATE_PARTS, + PATTERNS, + SYNC_TYPES, + USER_PATTERNS, + USER_PATTERN_CATEGORY, +} from './utils'; +import { + useExistingTemplateParts, + getUniqueTemplatePartTitle, + getCleanTemplatePartSlug, +} from '../../utils/template-part-create'; +import { unlock } from '../../lock-unlock'; + +const { useHistory } = unlock( routerPrivateApis ); + +function getPatternMeta( item ) { + if ( item.type === PATTERNS ) { + return { wp_pattern_sync_status: SYNC_TYPES.unsynced }; + } + + const syncStatus = item.reusableBlock.wp_pattern_sync_status; + const isUnsynced = syncStatus === SYNC_TYPES.unsynced; + + return { + ...item.reusableBlock.meta, + wp_pattern_sync_status: isUnsynced ? syncStatus : undefined, + }; +} + +export default function DuplicateMenuItem( { + categoryId, + item, + label = __( 'Duplicate' ), + onClose, +} ) { + const { saveEntityRecord } = useDispatch( coreStore ); + const { createErrorNotice, createSuccessNotice } = + useDispatch( noticesStore ); + + const history = useHistory(); + const existingTemplateParts = useExistingTemplateParts(); + + async function createTemplatePart() { + try { + const copiedTitle = sprintf( + /* translators: %s: Existing template part title */ + __( '%s (Copy)' ), + item.title + ); + const title = getUniqueTemplatePartTitle( + copiedTitle, + existingTemplateParts + ); + const slug = getCleanTemplatePartSlug( title ); + const { area, content } = item.templatePart; + + const result = await saveEntityRecord( + 'postType', + 'wp_template_part', + { slug, title, content, area }, + { throwOnError: true } + ); + + createSuccessNotice( + sprintf( + // translators: %s: The new template part's title e.g. 'Call to action (copy)'. + __( '"%s" created.' ), + title + ), + { + type: 'snackbar', + id: 'edit-site-patterns-success', + actions: [ + { + label: __( 'Edit' ), + onClick: () => + history.push( { + postType: TEMPLATE_PARTS, + postId: result?.id, + categoryType: TEMPLATE_PARTS, + categoryId, + } ), + }, + ], + } + ); + + onClose(); + } catch ( error ) { + const errorMessage = + error.message && error.code !== 'unknown_error' + ? error.message + : __( + 'An error occurred while creating the template part.' + ); + + createErrorNotice( errorMessage, { + type: 'snackbar', + id: 'edit-site-patterns-error', + } ); + onClose(); + } + } + + async function createPattern() { + try { + const isThemePattern = item.type === PATTERNS; + const title = sprintf( + /* translators: %s: Existing pattern title */ + __( '%s (Copy)' ), + item.title + ); + + const result = await saveEntityRecord( + 'postType', + 'wp_block', + { + content: isThemePattern + ? item.content + : item.reusableBlock.content, + meta: getPatternMeta( item ), + status: 'publish', + title, + }, + { throwOnError: true } + ); + + const actionLabel = isThemePattern + ? __( 'View my patterns' ) + : __( 'Edit' ); + + const newLocation = isThemePattern + ? { + categoryType: USER_PATTERNS, + categoryId: USER_PATTERN_CATEGORY, + path: '/patterns', + } + : { + categoryType: USER_PATTERNS, + categoryId: USER_PATTERN_CATEGORY, + postType: USER_PATTERNS, + postId: result?.id, + }; + + createSuccessNotice( + sprintf( + // translators: %s: The new pattern's title e.g. 'Call to action (copy)'. + __( '"%s" added to my patterns.' ), + title + ), + { + type: 'snackbar', + id: 'edit-site-patterns-success', + actions: [ + { + label: actionLabel, + onClick: () => history.push( newLocation ), + }, + ], + } + ); + + onClose(); + } catch ( error ) { + const errorMessage = + error.message && error.code !== 'unknown_error' + ? error.message + : __( 'An error occurred while creating the pattern.' ); + + createErrorNotice( errorMessage, { + type: 'snackbar', + id: 'edit-site-patterns-error', + } ); + onClose(); + } + } + + const createItem = + item.type === TEMPLATE_PARTS ? createTemplatePart : createPattern; + + return { label }; +} diff --git a/packages/edit-site/src/components/page-patterns/grid-item.js b/packages/edit-site/src/components/page-patterns/grid-item.js new file mode 100644 index 00000000000000..441529e1c0583c --- /dev/null +++ b/packages/edit-site/src/components/page-patterns/grid-item.js @@ -0,0 +1,284 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { BlockPreview } from '@wordpress/block-editor'; +import { + Button, + __experimentalConfirmDialog as ConfirmDialog, + DropdownMenu, + MenuGroup, + MenuItem, + __experimentalHeading as Heading, + __experimentalHStack as HStack, + Tooltip, + Flex, +} from '@wordpress/components'; +import { useDispatch } from '@wordpress/data'; +import { useState, useId, memo } from '@wordpress/element'; +import { __, sprintf } from '@wordpress/i18n'; +import { + Icon, + header, + footer, + symbolFilled as uncategorized, + symbol, + moreHorizontal, + lockSmall, +} from '@wordpress/icons'; +import { store as noticesStore } from '@wordpress/notices'; +import { store as reusableBlocksStore } from '@wordpress/reusable-blocks'; + +/** + * Internal dependencies + */ +import RenameMenuItem from './rename-menu-item'; +import DuplicateMenuItem from './duplicate-menu-item'; +import { PATTERNS, TEMPLATE_PARTS, USER_PATTERNS, SYNC_TYPES } from './utils'; +import { store as editSiteStore } from '../../store'; +import { useLink } from '../routes/link'; + +const templatePartIcons = { header, footer, uncategorized }; + +function GridItem( { categoryId, item, ...props } ) { + const descriptionId = useId(); + const [ isDeleteDialogOpen, setIsDeleteDialogOpen ] = useState( false ); + + const { removeTemplate } = useDispatch( editSiteStore ); + const { __experimentalDeleteReusableBlock } = + useDispatch( reusableBlocksStore ); + const { createErrorNotice, createSuccessNotice } = + useDispatch( noticesStore ); + + const isUserPattern = item.type === USER_PATTERNS; + const isNonUserPattern = item.type === PATTERNS; + const isTemplatePart = item.type === TEMPLATE_PARTS; + + const { onClick } = useLink( { + postType: item.type, + postId: isUserPattern ? item.id : item.name, + categoryId, + categoryType: item.type, + } ); + + const isEmpty = ! item.blocks?.length; + const patternClassNames = classnames( 'edit-site-patterns__pattern', { + 'is-placeholder': isEmpty, + } ); + const previewClassNames = classnames( 'edit-site-patterns__preview', { + 'is-inactive': isNonUserPattern, + } ); + + const deletePattern = async () => { + try { + await __experimentalDeleteReusableBlock( item.id ); + createSuccessNotice( + sprintf( + // translators: %s: The pattern's title e.g. 'Call to action'. + __( '"%s" deleted.' ), + item.title + ), + { type: 'snackbar', id: 'edit-site-patterns-success' } + ); + } catch ( error ) { + const errorMessage = + error.message && error.code !== 'unknown_error' + ? error.message + : __( 'An error occurred while deleting the pattern.' ); + createErrorNotice( errorMessage, { + type: 'snackbar', + id: 'edit-site-patterns-error', + } ); + } + }; + const deleteItem = () => + isTemplatePart ? removeTemplate( item ) : deletePattern(); + + // Only custom patterns or custom template parts can be renamed or deleted. + const isCustomPattern = + isUserPattern || ( isTemplatePart && item.isCustom ); + const hasThemeFile = isTemplatePart && item.templatePart.has_theme_file; + const ariaDescriptions = []; + + if ( isCustomPattern ) { + // User patterns don't have descriptions, but can be edited and deleted, so include some help text. + ariaDescriptions.push( + __( 'Press Enter to edit, or Delete to delete the pattern.' ) + ); + } else if ( item.description ) { + ariaDescriptions.push( item.description ); + } + + if ( isNonUserPattern ) { + ariaDescriptions.push( __( 'Theme patterns cannot be edited.' ) ); + } + + const itemIcon = + templatePartIcons[ categoryId ] || + ( item.syncStatus === SYNC_TYPES.full ? symbol : undefined ); + + const confirmButtonText = hasThemeFile ? __( 'Clear' ) : __( 'Delete' ); + const confirmPrompt = hasThemeFile + ? __( 'Are you sure you want to clear these customizations?' ) + : sprintf( + // translators: %s: The pattern or template part's title e.g. 'Call to action'. + __( 'Are you sure you want to delete "%s"?' ), + item.title + ); + + return ( +
  • + + { ariaDescriptions.map( ( ariaDescription, index ) => ( + + ) ) } + + + { itemIcon && ( + + + + + + ) } + + { item.type === PATTERNS ? ( + item.title + ) : ( + + + + ) } + { item.type === PATTERNS && ( + + + + + + ) } + + + + { ( { onClose } ) => ( + + { isCustomPattern && ! hasThemeFile && ( + + ) } + + { isCustomPattern && ( + + setIsDeleteDialogOpen( true ) + } + > + { hasThemeFile + ? __( 'Clear customizations' ) + : __( 'Delete' ) } + + ) } + + ) } + + + + { isDeleteDialogOpen && ( + setIsDeleteDialogOpen( false ) } + > + { confirmPrompt } + + ) } +
  • + ); +} + +export default memo( GridItem ); diff --git a/packages/edit-site/src/components/page-patterns/grid.js b/packages/edit-site/src/components/page-patterns/grid.js new file mode 100644 index 00000000000000..1902b36982c144 --- /dev/null +++ b/packages/edit-site/src/components/page-patterns/grid.js @@ -0,0 +1,52 @@ +/** + * WordPress dependencies + */ +import { __experimentalText as Text } from '@wordpress/components'; +import { useRef } from '@wordpress/element'; +import { __, sprintf } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import GridItem from './grid-item'; + +const PAGE_SIZE = 100; + +export default function Grid( { categoryId, items, ...props } ) { + const gridRef = useRef(); + + if ( ! items?.length ) { + return null; + } + + const list = items.slice( 0, PAGE_SIZE ); + const restLength = items.length - PAGE_SIZE; + + return ( + <> +
      + { list.map( ( item ) => ( + + ) ) } +
    + { restLength > 0 && ( + + { sprintf( + /* translators: %d: number of patterns */ + __( '+ %d more patterns discoverable by searching' ), + restLength + ) } + + ) } + + ); +} diff --git a/packages/edit-site/src/components/page-patterns/header.js b/packages/edit-site/src/components/page-patterns/header.js new file mode 100644 index 00000000000000..1237b85d6c9787 --- /dev/null +++ b/packages/edit-site/src/components/page-patterns/header.js @@ -0,0 +1,69 @@ +/** + * WordPress dependencies + */ +import { + __experimentalVStack as VStack, + __experimentalHeading as Heading, + __experimentalText as Text, +} from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { store as editorStore } from '@wordpress/editor'; +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import usePatternCategories from '../sidebar-navigation-screen-patterns/use-pattern-categories'; +import { + USER_PATTERN_CATEGORY, + USER_PATTERNS, + TEMPLATE_PARTS, + PATTERNS, +} from './utils'; + +export default function PatternsHeader( { + categoryId, + type, + titleId, + descriptionId, +} ) { + const { patternCategories } = usePatternCategories(); + const templatePartAreas = useSelect( + ( select ) => + select( editorStore ).__experimentalGetDefaultTemplatePartAreas(), + [] + ); + + let title, description; + if ( categoryId === USER_PATTERN_CATEGORY && type === USER_PATTERNS ) { + title = __( 'My Patterns' ); + description = ''; + } else if ( type === TEMPLATE_PARTS ) { + const templatePartArea = templatePartAreas.find( + ( area ) => area.area === categoryId + ); + title = templatePartArea?.label; + description = templatePartArea?.description; + } else if ( type === PATTERNS ) { + const patternCategory = patternCategories.find( + ( category ) => category.name === categoryId + ); + title = patternCategory?.label; + description = patternCategory?.description; + } + + if ( ! title ) return null; + + return ( + + + { title } + + { description ? ( + + { description } + + ) : null } + + ); +} diff --git a/packages/edit-site/src/components/page-patterns/index.js b/packages/edit-site/src/components/page-patterns/index.js new file mode 100644 index 00000000000000..d90fc748442444 --- /dev/null +++ b/packages/edit-site/src/components/page-patterns/index.js @@ -0,0 +1,44 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; +import { getQueryArgs } from '@wordpress/url'; + +/** + * Internal dependencies + */ +import { DEFAULT_CATEGORY, DEFAULT_TYPE } from './utils'; +import Page from '../page'; +import PatternsList from './patterns-list'; +import usePatternSettings from './use-pattern-settings'; +import { unlock } from '../../lock-unlock'; + +const { ExperimentalBlockEditorProvider } = unlock( blockEditorPrivateApis ); + +export default function PagePatterns() { + const { categoryType, categoryId } = getQueryArgs( window.location.href ); + const type = categoryType || DEFAULT_TYPE; + const category = categoryId || DEFAULT_CATEGORY; + const settings = usePatternSettings(); + + // Wrap everything in a block editor provider. + // This ensures 'styles' that are needed for the previews are synced + // from the site editor store to the block editor store. + return ( + + + + + + ); +} diff --git a/packages/edit-site/src/components/page-patterns/no-patterns.js b/packages/edit-site/src/components/page-patterns/no-patterns.js new file mode 100644 index 00000000000000..b4805f57018c7c --- /dev/null +++ b/packages/edit-site/src/components/page-patterns/no-patterns.js @@ -0,0 +1,12 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +export default function NoPatterns() { + return ( +
    + { __( 'No patterns found.' ) } +
    + ); +} diff --git a/packages/edit-site/src/components/page-patterns/patterns-list.js b/packages/edit-site/src/components/page-patterns/patterns-list.js new file mode 100644 index 00000000000000..7bf2a9d5065841 --- /dev/null +++ b/packages/edit-site/src/components/page-patterns/patterns-list.js @@ -0,0 +1,156 @@ +/** + * WordPress dependencies + */ +import { useState, useDeferredValue, useId } from '@wordpress/element'; +import { + SearchControl, + __experimentalVStack as VStack, + Flex, + FlexBlock, + __experimentalToggleGroupControl as ToggleGroupControl, + __experimentalToggleGroupControlOption as ToggleGroupControlOption, + __experimentalHeading as Heading, + __experimentalText as Text, +} from '@wordpress/components'; +import { __, isRTL } from '@wordpress/i18n'; +import { chevronLeft, chevronRight } from '@wordpress/icons'; +import { privateApis as routerPrivateApis } from '@wordpress/router'; +import { useViewportMatch, useAsyncList } from '@wordpress/compose'; + +/** + * Internal dependencies + */ +import PatternsHeader from './header'; +import Grid from './grid'; +import NoPatterns from './no-patterns'; +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 } from './utils'; + +const { useLocation, useHistory } = unlock( routerPrivateApis ); + +const SYNC_FILTERS = { + all: __( 'All' ), + [ SYNC_TYPES.full ]: __( 'Synced' ), + [ SYNC_TYPES.unsynced ]: __( 'Standard' ), +}; + +const SYNC_DESCRIPTIONS = { + all: '', + [ SYNC_TYPES.full ]: __( + 'Patterns that are kept in sync across the site.' + ), + [ SYNC_TYPES.unsynced ]: __( + 'Patterns that can be changed freely without affecting the site.' + ), +}; + +export default function PatternsList( { categoryId, type } ) { + const location = useLocation(); + const history = useHistory(); + const isMobileViewport = useViewportMatch( 'medium', '<' ); + const [ filterValue, setFilterValue, delayedFilterValue ] = + useDebouncedInput( '' ); + const deferredFilterValue = useDeferredValue( delayedFilterValue ); + + const [ syncFilter, setSyncFilter ] = useState( 'all' ); + const deferredSyncedFilter = useDeferredValue( syncFilter ); + const { patterns, isResolving } = usePatterns( type, categoryId, { + search: deferredFilterValue, + syncStatus: + deferredSyncedFilter === 'all' ? undefined : deferredSyncedFilter, + } ); + + const id = useId(); + const titleId = `${ id }-title`; + const descriptionId = `${ id }-description`; + + const hasPatterns = patterns.length; + const title = SYNC_FILTERS[ syncFilter ]; + const description = SYNC_DESCRIPTIONS[ syncFilter ]; + const shownPatterns = useAsyncList( patterns ); + + return ( + + + + + { isMobileViewport && ( + { + // Go back in history if we came from the Patterns page. + // Otherwise push a stack onto the history. + if ( location.state?.backPath === '/patterns' ) { + history.back(); + } else { + history.push( { path: '/patterns' } ); + } + } } + /> + ) } + + setFilterValue( value ) } + placeholder={ __( 'Search patterns' ) } + label={ __( 'Search patterns' ) } + value={ filterValue } + __nextHasNoMarginBottom + /> + + { categoryId === USER_PATTERN_CATEGORY && ( + setSyncFilter( value ) } + __nextHasNoMarginBottom + > + { Object.entries( SYNC_FILTERS ).map( + ( [ key, label ] ) => ( + + ) + ) } + + ) } + + { syncFilter !== 'all' && ( + + + { title } + + { description ? ( + + { description } + + ) : null } + + ) } + { hasPatterns && ( + + ) } + { ! isResolving && ! hasPatterns && } + + ); +} diff --git a/packages/edit-site/src/components/page-patterns/rename-menu-item.js b/packages/edit-site/src/components/page-patterns/rename-menu-item.js new file mode 100644 index 00000000000000..938023a62cefd3 --- /dev/null +++ b/packages/edit-site/src/components/page-patterns/rename-menu-item.js @@ -0,0 +1,115 @@ +/** + * WordPress dependencies + */ +import { + Button, + MenuItem, + Modal, + TextControl, + __experimentalHStack as HStack, + __experimentalVStack as VStack, +} from '@wordpress/components'; +import { store as coreStore } from '@wordpress/core-data'; +import { useDispatch } from '@wordpress/data'; +import { useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { store as noticesStore } from '@wordpress/notices'; + +/** + * Internal dependencies + */ +import { TEMPLATE_PARTS } from './utils'; + +export default function RenameMenuItem( { item, onClose } ) { + const [ title, setTitle ] = useState( () => item.title ); + const [ isModalOpen, setIsModalOpen ] = useState( false ); + + const { editEntityRecord, saveEditedEntityRecord } = + useDispatch( coreStore ); + const { createSuccessNotice, createErrorNotice } = + useDispatch( noticesStore ); + + if ( item.type === TEMPLATE_PARTS && ! item.isCustom ) { + return null; + } + + async function onRename( event ) { + event.preventDefault(); + + try { + await editEntityRecord( 'postType', item.type, item.id, { title } ); + + // Update state before saving rerenders the list. + setTitle( '' ); + setIsModalOpen( false ); + onClose(); + + // Persist edited entity. + await saveEditedEntityRecord( 'postType', item.type, item.id, { + throwOnError: true, + } ); + + createSuccessNotice( __( 'Entity renamed.' ), { + type: 'snackbar', + } ); + } catch ( error ) { + const errorMessage = + error.message && error.code !== 'unknown_error' + ? error.message + : __( 'An error occurred while renaming the entity.' ); + + createErrorNotice( errorMessage, { type: 'snackbar' } ); + } + } + + return ( + <> + { + setIsModalOpen( true ); + setTitle( item.title ); + } } + > + { __( 'Rename' ) } + + { isModalOpen && ( + { + setIsModalOpen( false ); + onClose(); + } } + overlayClassName="edit-site-list__rename_modal" + > +
    + + + + + + + + + +
    +
    + ) } + + ); +} diff --git a/packages/edit-site/src/components/page-patterns/search-items.js b/packages/edit-site/src/components/page-patterns/search-items.js new file mode 100644 index 00000000000000..9026e7f39f4bf9 --- /dev/null +++ b/packages/edit-site/src/components/page-patterns/search-items.js @@ -0,0 +1,171 @@ +/** + * External dependencies + */ +import removeAccents from 'remove-accents'; +import { noCase } from 'change-case'; + +// Default search helpers. +const defaultGetName = ( item ) => item.name || ''; +const defaultGetTitle = ( item ) => item.title; +const defaultGetDescription = ( item ) => item.description || ''; +const defaultGetKeywords = ( item ) => item.keywords || []; +const defaultHasCategory = () => false; + +/** + * Extracts words from an input string. + * + * @param {string} input The input string. + * + * @return {Array} Words, extracted from the input string. + */ +function extractWords( input = '' ) { + return noCase( input, { + splitRegexp: [ + /([\p{Ll}\p{Lo}\p{N}])([\p{Lu}\p{Lt}])/gu, // One lowercase or digit, followed by one uppercase. + /([\p{Lu}\p{Lt}])([\p{Lu}\p{Lt}][\p{Ll}\p{Lo}])/gu, // One uppercase followed by one uppercase and one lowercase. + ], + stripRegexp: /(\p{C}|\p{P}|\p{S})+/giu, // Anything that's not a punctuation, symbol or control/format character. + } ) + .split( ' ' ) + .filter( Boolean ); +} + +/** + * Sanitizes the search input string. + * + * @param {string} input The search input to normalize. + * + * @return {string} The normalized search input. + */ +function normalizeSearchInput( input = '' ) { + // Disregard diacritics. + // Input: "média" + input = removeAccents( input ); + + // Accommodate leading slash, matching autocomplete expectations. + // Input: "/media" + input = input.replace( /^\//, '' ); + + // Lowercase. + // Input: "MEDIA" + input = input.toLowerCase(); + + return input; +} + +/** + * Converts the search term into a list of normalized terms. + * + * @param {string} input The search term to normalize. + * + * @return {string[]} The normalized list of search terms. + */ +export const getNormalizedSearchTerms = ( input = '' ) => { + return extractWords( normalizeSearchInput( input ) ); +}; + +const removeMatchingTerms = ( unmatchedTerms, unprocessedTerms ) => { + return unmatchedTerms.filter( + ( term ) => + ! getNormalizedSearchTerms( unprocessedTerms ).some( + ( unprocessedTerm ) => unprocessedTerm.includes( term ) + ) + ); +}; + +/** + * Filters an item list given a search term. + * + * @param {Array} items Item list + * @param {string} searchInput Search input. + * @param {Object} config Search Config. + * + * @return {Array} Filtered item list. + */ +export const searchItems = ( items = [], searchInput = '', config = {} ) => { + const normalizedSearchTerms = getNormalizedSearchTerms( searchInput ); + const onlyFilterByCategory = ! normalizedSearchTerms.length; + const searchRankConfig = { ...config, onlyFilterByCategory }; + + // If we aren't filtering on search terms, matching on category is satisfactory. + // If we are, then we need more than a category match. + const threshold = onlyFilterByCategory ? 0 : 1; + + const rankedItems = items + .map( ( item ) => { + return [ + item, + getItemSearchRank( item, searchInput, searchRankConfig ), + ]; + } ) + .filter( ( [ , rank ] ) => rank > threshold ); + + // If we didn't have terms to search on, there's no point sorting. + if ( normalizedSearchTerms.length === 0 ) { + return rankedItems.map( ( [ item ] ) => item ); + } + + rankedItems.sort( ( [ , rank1 ], [ , rank2 ] ) => rank2 - rank1 ); + return rankedItems.map( ( [ item ] ) => item ); +}; + +/** + * Get the search rank for a given item and a specific search term. + * The better the match, the higher the rank. + * If the rank equals 0, it should be excluded from the results. + * + * @param {Object} item Item to filter. + * @param {string} searchTerm Search term. + * @param {Object} config Search Config. + * + * @return {number} Search Rank. + */ +function getItemSearchRank( item, searchTerm, config ) { + const { + categoryId, + getName = defaultGetName, + getTitle = defaultGetTitle, + getDescription = defaultGetDescription, + getKeywords = defaultGetKeywords, + hasCategory = defaultHasCategory, + onlyFilterByCategory, + } = config; + + let rank = 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. + if ( ! rank || onlyFilterByCategory ) { + return rank; + } + + const name = getName( item ); + const title = getTitle( item ); + const description = getDescription( item ); + const keywords = getKeywords( item ); + + const normalizedSearchInput = normalizeSearchInput( searchTerm ); + const normalizedTitle = normalizeSearchInput( title ); + + // Prefers exact matches + // Then prefers if the beginning of the title matches the search term + // name, keywords, description matches come later. + if ( normalizedSearchInput === normalizedTitle ) { + rank += 30; + } else if ( normalizedTitle.startsWith( normalizedSearchInput ) ) { + rank += 20; + } else { + const terms = [ name, title, description, ...keywords ].join( ' ' ); + const normalizedSearchTerms = extractWords( normalizedSearchInput ); + const unmatchedTerms = removeMatchingTerms( + normalizedSearchTerms, + terms + ); + + if ( unmatchedTerms.length === 0 ) { + rank += 10; + } + } + + return rank; +} diff --git a/packages/edit-site/src/components/page-patterns/style.scss b/packages/edit-site/src/components/page-patterns/style.scss new file mode 100644 index 00000000000000..79731999f46efa --- /dev/null +++ b/packages/edit-site/src/components/page-patterns/style.scss @@ -0,0 +1,169 @@ +.edit-site-patterns { + background: rgba(0, 0, 0, 0.15); + margin: $header-height 0 0; + .components-base-control { + width: 100%; + @include break-medium { + width: auto; + } + } + + .components-text { + color: $gray-600; + } + + .components-heading { + color: $gray-200; + } + + @include break-medium { + margin: 0; + } + + .edit-site-patterns__search-block { + min-width: fit-content; + flex-grow: 1; + } + + // The increased specificity here is to overcome component styles + // without relying on internal component class names. + .edit-site-patterns__search { + input[type="search"] { + height: $button-size-next-default-40px; + background: $gray-800; + color: $gray-200; + + &:focus { + background: $gray-800; + } + } + + svg { + fill: $gray-600; + } + } + + .edit-site-patterns__sync-status-filter { + background: $gray-800; + border: none; + height: $button-size-next-default-40px; + min-width: max-content; + width: 100%; + max-width: 100%; + + @include break-medium { + width: 300px; + } + } + .edit-site-patterns__sync-status-filter-option:active { + background: $gray-700; + color: $gray-100; + } +} + +.edit-site-patterns__section-header { + .screen-reader-shortcut:focus { + top: 0; + } +} + +.edit-site-patterns__grid { + display: grid; + grid-template-columns: 1fr; + gap: $grid-unit-40; + // Small top padding required to avoid cutting off the visible outline + // when hovering items. + padding-top: $border-width-focus-fallback; + margin-bottom: $grid-unit-40; + @include break-large { + grid-template-columns: 1fr 1fr; + } + .edit-site-patterns__pattern { + break-inside: avoid-column; + display: flex; + flex-direction: column; + .edit-site-patterns__preview { + box-shadow: none; + border: none; + padding: 0; + background-color: unset; + box-sizing: border-box; + border-radius: 4px; + cursor: pointer; + overflow: hidden; + + &:focus { + box-shadow: inset 0 0 0 0 $white, 0 0 0 2px var(--wp-admin-theme-color); + // Windows High Contrast mode will show this outline, but not the box-shadow. + outline: 2px solid transparent; + } + + &.is-inactive { + cursor: default; + } + &.is-inactive:focus { + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) $gray-800; + opacity: 0.8; + } + } + + .edit-site-patterns__footer, + .edit-site-patterns__button { + color: $gray-600; + } + + &.is-placeholder .edit-site-patterns__preview { + min-height: $grid-unit-80; + color: $gray-600; + border: 1px dashed $gray-800; + display: flex; + align-items: center; + justify-content: center; + + &:focus { + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + } + } + } + + .edit-site-patterns__preview { + flex: 0 1 auto; + margin-bottom: $grid-unit-20; + } +} + +.edit-site-patterns__load-more { + align-self: center; +} + +.edit-site-patterns__pattern-title { + color: $gray-200; + + .is-link { + text-decoration: none; + color: $gray-200; + + &:hover, + &:focus { + color: $white; + } + } + + .edit-site-patterns__pattern-icon { + border-radius: $grid-unit-05; + background: var(--wp-block-synced-color); + fill: $white; + } + + .edit-site-patterns__pattern-lock-icon { + display: inline-flex; + + svg { + fill: currentcolor; + } + } +} + +.edit-site-patterns__no-results { + color: $gray-600; +} diff --git a/packages/edit-site/src/components/page-patterns/use-pattern-settings.js b/packages/edit-site/src/components/page-patterns/use-pattern-settings.js new file mode 100644 index 00000000000000..28a16b1d7ed7db --- /dev/null +++ b/packages/edit-site/src/components/page-patterns/use-pattern-settings.js @@ -0,0 +1,51 @@ +/** + * WordPress dependencies + */ +import { store as coreStore } from '@wordpress/core-data'; +import { useSelect } from '@wordpress/data'; +import { useMemo } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { unlock } from '../../lock-unlock'; +import { store as editSiteStore } from '../../store'; +import { filterOutDuplicatesByName } from './utils'; + +export default function usePatternSettings() { + const storedSettings = useSelect( ( select ) => { + const { getSettings } = unlock( select( editSiteStore ) ); + return getSettings(); + }, [] ); + + const settingsBlockPatterns = + storedSettings.__experimentalAdditionalBlockPatterns ?? // WP 6.0 + storedSettings.__experimentalBlockPatterns; // WP 5.9 + + const restBlockPatterns = useSelect( + ( select ) => select( coreStore ).getBlockPatterns(), + [] + ); + + const blockPatterns = useMemo( + () => + [ + ...( settingsBlockPatterns || [] ), + ...( restBlockPatterns || [] ), + ].filter( filterOutDuplicatesByName ), + [ settingsBlockPatterns, restBlockPatterns ] + ); + + const settings = useMemo( () => { + const { __experimentalAdditionalBlockPatterns, ...restStoredSettings } = + storedSettings; + + return { + ...restStoredSettings, + __experimentalBlockPatterns: blockPatterns, + __unstableIsPreviewMode: true, + }; + }, [ storedSettings, blockPatterns ] ); + + return settings; +} diff --git a/packages/edit-site/src/components/page-patterns/use-patterns.js b/packages/edit-site/src/components/page-patterns/use-patterns.js new file mode 100644 index 00000000000000..ea2b8ac976fea4 --- /dev/null +++ b/packages/edit-site/src/components/page-patterns/use-patterns.js @@ -0,0 +1,170 @@ +/** + * WordPress dependencies + */ +import { parse } from '@wordpress/blocks'; +import { useSelect } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; +import { decodeEntities } from '@wordpress/html-entities'; + +/** + * Internal dependencies + */ +import { + CORE_PATTERN_SOURCES, + PATTERNS, + SYNC_TYPES, + TEMPLATE_PARTS, + USER_PATTERNS, + filterOutDuplicatesByName, +} from './utils'; +import { unlock } from '../../lock-unlock'; +import { searchItems } from './search-items'; +import { store as editSiteStore } from '../../store'; + +const EMPTY_PATTERN_LIST = []; + +const createTemplatePartId = ( theme, slug ) => + theme && slug ? theme + '//' + slug : null; + +const templatePartToPattern = ( templatePart ) => ( { + blocks: parse( templatePart.content.raw ), + categories: [ templatePart.area ], + description: templatePart.description || '', + isCustom: templatePart.source === 'custom', + keywords: templatePart.keywords || [], + id: createTemplatePartId( templatePart.theme, templatePart.slug ), + name: createTemplatePartId( templatePart.theme, templatePart.slug ), + title: decodeEntities( templatePart.title.rendered ), + type: templatePart.type, + templatePart, +} ); + +const templatePartHasCategory = ( item, category ) => + item.templatePart.area === category; + +const selectTemplatePartsAsPatterns = ( + select, + { categoryId, search = '' } = {} +) => { + const { getEntityRecords, getIsResolving } = select( coreStore ); + const query = { per_page: -1 }; + const rawTemplateParts = + getEntityRecords( 'postType', TEMPLATE_PARTS, query ) ?? + EMPTY_PATTERN_LIST; + const templateParts = rawTemplateParts.map( ( templatePart ) => + templatePartToPattern( templatePart ) + ); + + const isResolving = getIsResolving( 'getEntityRecords', [ + 'postType', + 'wp_template_part', + query, + ] ); + + const patterns = searchItems( templateParts, search, { + categoryId, + hasCategory: templatePartHasCategory, + } ); + + return { patterns, isResolving }; +}; + +const selectThemePatterns = ( select, { categoryId, search = '' } = {} ) => { + const { getSettings } = unlock( select( editSiteStore ) ); + const settings = getSettings(); + const blockPatterns = + settings.__experimentalAdditionalBlockPatterns ?? + settings.__experimentalBlockPatterns; + + const restBlockPatterns = select( coreStore ).getBlockPatterns(); + + let patterns = [ + ...( blockPatterns || [] ), + ...( restBlockPatterns || [] ), + ] + .filter( + ( pattern ) => ! CORE_PATTERN_SOURCES.includes( pattern.source ) + ) + .filter( filterOutDuplicatesByName ) + .map( ( pattern ) => ( { + ...pattern, + keywords: pattern.keywords || [], + type: 'pattern', + blocks: parse( pattern.content ), + } ) ); + + patterns = searchItems( patterns, search, { + categoryId, + hasCategory: ( item, currentCategory ) => + item.categories?.includes( currentCategory ), + } ); + + return { patterns, isResolving: false }; +}; + +const reusableBlockToPattern = ( reusableBlock ) => ( { + blocks: parse( reusableBlock.content.raw ), + 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, +} ); + +const selectUserPatterns = ( select, { search = '', syncStatus } = {} ) => { + const { getEntityRecords, getIsResolving } = select( coreStore ); + + const query = { per_page: -1 }; + const records = getEntityRecords( 'postType', USER_PATTERNS, query ); + + let patterns = records + ? records.map( ( record ) => reusableBlockToPattern( record ) ) + : EMPTY_PATTERN_LIST; + const isResolving = getIsResolving( 'getEntityRecords', [ + 'postType', + USER_PATTERNS, + query, + ] ); + + if ( syncStatus ) { + patterns = patterns.filter( + ( pattern ) => pattern.syncStatus === syncStatus + ); + } + + patterns = searchItems( patterns, search, { + // We exit user pattern retrieval early if we aren't in the + // catch-all category for user created patterns, so it has + // to be in the category. + hasCategory: () => true, + } ); + + return { patterns, isResolving }; +}; + +export const usePatterns = ( + categoryType, + categoryId, + { search = '', syncStatus } +) => { + return useSelect( + ( select ) => { + if ( categoryType === TEMPLATE_PARTS ) { + return selectTemplatePartsAsPatterns( select, { + categoryId, + search, + } ); + } else if ( categoryType === PATTERNS ) { + return selectThemePatterns( select, { categoryId, search } ); + } else if ( categoryType === USER_PATTERNS ) { + return selectUserPatterns( select, { search, syncStatus } ); + } + return { patterns: EMPTY_PATTERN_LIST, isResolving: false }; + }, + [ categoryId, categoryType, search, syncStatus ] + ); +}; + +export default usePatterns; diff --git a/packages/edit-site/src/components/page-patterns/utils.js b/packages/edit-site/src/components/page-patterns/utils.js new file mode 100644 index 00000000000000..bbdff872fe355a --- /dev/null +++ b/packages/edit-site/src/components/page-patterns/utils.js @@ -0,0 +1,21 @@ +export const DEFAULT_CATEGORY = 'my-patterns'; +export const DEFAULT_TYPE = 'wp_block'; +export const PATTERNS = 'pattern'; +export const TEMPLATE_PARTS = 'wp_template_part'; +export const USER_PATTERNS = 'wp_block'; +export const USER_PATTERN_CATEGORY = 'my-patterns'; + +export const CORE_PATTERN_SOURCES = [ + 'core', + 'pattern-directory/core', + 'pattern-directory/featured', + 'pattern-directory/theme', +]; + +export const SYNC_TYPES = { + full: 'fully', + unsynced: 'unsynced', +}; + +export const filterOutDuplicatesByName = ( currentItem, index, items ) => + index === items.findIndex( ( item ) => currentItem.name === item.name ); diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/category-item.js b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/category-item.js index e0c94ee30c0acd..6e5096bee3fbe8 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/category-item.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/category-item.js @@ -4,11 +4,6 @@ import SidebarNavigationItem from '../sidebar-navigation-item'; import { useLink } from '../routes/link'; -/** - * WordPress dependencies - */ -import { sprintf, _n } from '@wordpress/i18n'; - export default function CategoryItem( { count, icon, @@ -16,19 +11,18 @@ export default function CategoryItem( { isActive, label, type, - typeLabel, } ) { const linkInfo = useLink( { - path: '/library', + path: '/patterns', categoryType: type, categoryId: id, }, { // Keep a record of where we came from in state so we can - // use the browser's back button to go back to the library. + // use the browser's back button to go back to Patterns. // See the implementation of the back button in patterns-list. - backPath: '/library', + backPath: '/patterns', } ); @@ -36,21 +30,12 @@ export default function CategoryItem( { return; } - const ariaLabel = sprintf( - /* translators: %1$s is the category name, %2$s is the type (pattern or template part), %3$s is the number of items. */ - _n( '%1$s (%2$s), %3$s item', '%1$s (%2$s), %3$s items', count ), - label, - typeLabel ? typeLabel : type, - count - ); - return ( { count } } aria-current={ isActive ? 'true' : undefined } - aria-label={ ariaLabel } > { label } 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 b751d55265cf0a..d3fc15358027b3 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,13 +4,17 @@ import { __experimentalItemGroup as ItemGroup, __experimentalItem as Item, + Flex, + Icon, + Tooltip, + __experimentalHeading as Heading, } from '@wordpress/components'; import { useViewportMatch } from '@wordpress/compose'; import { useSelect } from '@wordpress/data'; import { getTemplatePartIcon } from '@wordpress/editor'; import { __ } from '@wordpress/i18n'; import { getQueryArgs } from '@wordpress/url'; -import { file } from '@wordpress/icons'; +import { file, starFilled, lockSmall } from '@wordpress/icons'; /** * Internal dependencies @@ -19,19 +23,82 @@ import AddNewPattern from '../add-new-pattern'; import SidebarNavigationItem from '../sidebar-navigation-item'; import SidebarNavigationScreen from '../sidebar-navigation-screen'; import CategoryItem from './category-item'; -import { DEFAULT_CATEGORY, DEFAULT_TYPE } from '../page-library/utils'; +import { DEFAULT_CATEGORY, DEFAULT_TYPE } from '../page-patterns/utils'; import { store as editSiteStore } from '../../store'; +import { useLink } from '../routes/link'; import usePatternCategories from './use-pattern-categories'; +import useMyPatterns from './use-my-patterns'; import useTemplatePartAreas from './use-template-part-areas'; -const templatePartAreaLabels = { - header: __( 'Headers' ), - footer: __( 'Footers' ), - sidebar: __( 'Sidebar' ), - uncategorized: __( 'Uncategorized' ), -}; +function TemplatePartGroup( { areas, currentArea, currentType } ) { + return ( + <> +
    + { __( 'Template parts' ) } +
    + + { Object.entries( areas ).map( + ( [ area, { label, templateParts } ] ) => ( + + ) + ) } + + + ); +} + +function ThemePatternsGroup( { categories, currentCategory, currentType } ) { + return ( + <> +
    + { __( 'Theme patterns' ) } +
    + + { categories.map( ( category ) => ( + + { category.label } + + + + + + + } + icon={ file } + id={ category.name } + type="pattern" + isActive={ + currentCategory === `${ category.name }` && + currentType === 'pattern' + } + /> + ) ) } + + + ); +} -export default function SidebarNavigationScreenLibrary() { +export default function SidebarNavigationScreenPatterns() { const isMobileViewport = useViewportMatch( 'medium', '<' ); const { categoryType, categoryId } = getQueryArgs( window.location.href ); const currentCategory = categoryId || DEFAULT_CATEGORY; @@ -40,40 +107,45 @@ export default function SidebarNavigationScreenLibrary() { const { templatePartAreas, hasTemplateParts, isLoading } = useTemplatePartAreas(); const { patternCategories, hasPatterns } = usePatternCategories(); + const { myPatterns, hasPatterns: hasMyPatterns } = useMyPatterns(); const isTemplatePartsMode = useSelect( ( select ) => { const settings = select( editSiteStore ).getSettings(); return !! settings.supportsTemplatePartsMode; }, [] ); + const templatePartsLink = useLink( { path: '/wp_template_part/all' } ); + const footer = ! isMobileViewport ? ( + + + { __( 'Manage all template parts' ) } + + + { __( 'Manage all of my patterns' ) } + + + ) : undefined; + return ( } - footer={ - - { ! isMobileViewport && ( - - { __( 'Manage all custom patterns' ) } - - ) } - - } + footer={ footer } content={ <> - { isLoading && __( 'Loading library' ) } + { isLoading && __( 'Loading patterns' ) } { ! isLoading && ( <> { ! hasTemplateParts && ! hasPatterns && ( - + { __( 'No template parts or patterns found' @@ -81,55 +153,36 @@ export default function SidebarNavigationScreenLibrary() { ) } - { hasTemplateParts && ( - - { Object.entries( templatePartAreas ).map( - ( [ area, parts ] ) => ( - - ) - ) } + { hasMyPatterns && ( + + ) } + { hasTemplateParts && ( + + ) } { hasPatterns && ( - - { patternCategories.map( ( category ) => ( - - ) ) } - + ) } ) } diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/style.scss b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/style.scss index efb8b7537588db..6a6fbc009e0aa4 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/style.scss +++ b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/style.scss @@ -1,3 +1,25 @@ -.edit-site-sidebar-navigation-screen-library__group { - margin-bottom: $grid-unit-30; +.edit-site-sidebar-navigation-screen-patterns__group { + margin-bottom: $grid-unit-40; + &:last-of-type, + &:first-of-type { + border-bottom: 0; + padding-bottom: 0; + margin-bottom: 0; + } + + &:first-of-type { + margin-bottom: $grid-unit-40; + } +} + +.edit-site-sidebar-navigation-screen-patterns__group-header { + p { + color: $gray-600; + } + + h2 { + font-size: 11px; + font-weight: 500; + text-transform: uppercase; + } } diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-default-pattern-categories.js b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-default-pattern-categories.js index 014d0e2e65b0ce..96491018d07722 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-default-pattern-categories.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-default-pattern-categories.js @@ -1,32 +1,47 @@ /** * WordPress dependencies */ -import { store as coreStore } from '@wordpress/core-data'; -import { useSelect } from '@wordpress/data'; +import { useMemo } from '@wordpress/element'; /** * Internal dependencies */ -import { unlock } from '../../lock-unlock'; -import { store as editSiteStore } from '../../store'; - -export default function useDefaultPatternCategories() { - const blockPatternCategories = useSelect( ( select ) => { - const { getSettings } = unlock( select( editSiteStore ) ); - const settings = getSettings(); - - return ( - settings.__experimentalAdditionalBlockPatternCategories ?? - settings.__experimentalBlockPatternCategories - ); - } ); - - const restBlockPatternCategories = useSelect( ( select ) => - select( coreStore ).getBlockPatternCategories() - ); - - return [ - ...( blockPatternCategories || [] ), - ...( restBlockPatternCategories || [] ), - ]; +import useDefaultPatternCategories from './use-default-pattern-categories'; +import useThemePatterns from './use-theme-patterns'; + +export default function usePatternCategories() { + const defaultCategories = useDefaultPatternCategories(); + const themePatterns = useThemePatterns(); + + const patternCategories = useMemo( () => { + const categoryMap = {}; + const categoriesWithCounts = []; + + // Create a map for easier counting of patterns in categories. + defaultCategories.forEach( ( category ) => { + if ( ! categoryMap[ category.name ] ) { + categoryMap[ category.name ] = { ...category, count: 0 }; + } + } ); + + // Update the category counts to reflect theme registered patterns. + themePatterns.forEach( ( pattern ) => { + pattern.categories?.forEach( ( category ) => { + if ( categoryMap[ category ] ) { + categoryMap[ category ].count += 1; + } + } ); + } ); + + // Filter categories so we only have those containing patterns. + defaultCategories.forEach( ( category ) => { + if ( categoryMap[ category.name ].count ) { + categoriesWithCounts.push( categoryMap[ category.name ] ); + } + } ); + + return categoriesWithCounts; + }, [ defaultCategories, themePatterns ] ); + + return { patternCategories, hasPatterns: !! patternCategories.length }; } diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-my-patterns.js b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-my-patterns.js new file mode 100644 index 00000000000000..37f0b0f8a4e063 --- /dev/null +++ b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-my-patterns.js @@ -0,0 +1,24 @@ +/** + * WordPress dependencies + */ +import { store as coreStore } from '@wordpress/core-data'; +import { useSelect } from '@wordpress/data'; +import { __ } from '@wordpress/i18n'; + +export default function useMyPatterns() { + const myPatternsCount = useSelect( + ( select ) => + 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-template-part-areas.js b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-template-part-areas.js index aa258344d132da..bc538c5e7a85fa 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-template-part-areas.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-template-part-areas.js @@ -2,19 +2,41 @@ * WordPress dependencies */ import { useEntityRecords } from '@wordpress/core-data'; +import { useSelect } from '@wordpress/data'; +import { store as editorStore } from '@wordpress/editor'; -const getTemplatePartAreas = ( items ) => { +const useTemplatePartsGroupedByArea = ( items ) => { const allItems = items || []; - const groupedByArea = allItems.reduce( - ( accumulator, item ) => { - const key = accumulator[ item.area ] ? item.area : 'uncategorized'; - accumulator[ key ].push( item ); - return accumulator; - }, - { header: [], footer: [], sidebar: [], uncategorized: [] } + const templatePartAreas = useSelect( + ( select ) => + select( editorStore ).__experimentalGetDefaultTemplatePartAreas(), + [] ); + // Create map of template areas ensuring that default areas are displayed before + // any custom registered template part areas. + const knownAreas = { + header: {}, + footer: {}, + sidebar: {}, + uncategorized: {}, + }; + + templatePartAreas.forEach( + ( templatePartArea ) => + ( knownAreas[ templatePartArea.area ] = { + ...templatePartArea, + templateParts: [], + } ) + ); + + const groupedByArea = allItems.reduce( ( accumulator, item ) => { + const key = accumulator[ item.area ] ? item.area : 'uncategorized'; + accumulator[ key ].templateParts.push( item ); + return accumulator; + }, knownAreas ); + return groupedByArea; }; @@ -28,6 +50,6 @@ export default function useTemplatePartAreas() { return { hasTemplateParts: templateParts ? !! templateParts.length : false, isLoading, - templatePartAreas: getTemplatePartAreas( templateParts ), + templatePartAreas: useTemplatePartsGroupedByArea( templateParts ), }; } diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-theme-patterns.js b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-theme-patterns.js index d0534eca2846e1..b4a0b570c3c8dd 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-theme-patterns.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-theme-patterns.js @@ -11,7 +11,7 @@ import { useMemo } from '@wordpress/element'; import { CORE_PATTERN_SOURCES, filterOutDuplicatesByName, -} from '../page-library/utils'; +} from '../page-patterns/utils'; import { unlock } from '../../lock-unlock'; import { store as editSiteStore } from '../../store'; diff --git a/packages/edit-site/src/components/sidebar/index.js b/packages/edit-site/src/components/sidebar/index.js index eef37a44339956..84989e6da86f6e 100644 --- a/packages/edit-site/src/components/sidebar/index.js +++ b/packages/edit-site/src/components/sidebar/index.js @@ -12,9 +12,9 @@ import { privateApis as routerPrivateApis } from '@wordpress/router'; * Internal dependencies */ import SidebarNavigationScreenMain from '../sidebar-navigation-screen-main'; -import SidebarNavigationScreenLibrary from '../sidebar-navigation-screen-library'; import SidebarNavigationScreenTemplates from '../sidebar-navigation-screen-templates'; import SidebarNavigationScreenTemplate from '../sidebar-navigation-screen-template'; +import SidebarNavigationScreenPatterns from '../sidebar-navigation-screen-patterns'; import SidebarNavigationScreenPattern from '../sidebar-navigation-screen-pattern'; import useSyncPathWithURL, { getPathFromURL, @@ -56,8 +56,8 @@ function SidebarScreens() { - - + + diff --git a/packages/edit-site/src/style.scss b/packages/edit-site/src/style.scss index 324f1ab4bc0eca..c135bbc053060a 100644 --- a/packages/edit-site/src/style.scss +++ b/packages/edit-site/src/style.scss @@ -33,7 +33,7 @@ @import "./components/sidebar-navigation-screen/style.scss"; @import "./components/sidebar-navigation-screen-details-footer/style.scss"; @import "./components/sidebar-navigation-screen-global-styles/style.scss"; -@import "./components/sidebar-navigation-screen-library/style.scss"; +@import "./components/sidebar-navigation-screen-patterns/style.scss"; @import "./components/sidebar-navigation-screen-navigation-menu/style.scss"; @import "./components/sidebar-navigation-screen-page/style.scss"; @import "components/sidebar-navigation-screen-details-panel/style.scss"; From 456c1a357886c9fedc722a5f3ffbd947d5be0222 Mon Sep 17 00:00:00 2001 From: Carolina Nymark Date: Tue, 11 Jul 2023 08:23:58 +0200 Subject: [PATCH 4/5] try to solve merge conflicts --- .../use-default-pattern-categories.js | 63 +++++++------------ .../use-pattern-categories.js | 19 +----- 2 files changed, 25 insertions(+), 57 deletions(-) diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-default-pattern-categories.js b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-default-pattern-categories.js index 96491018d07722..014d0e2e65b0ce 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-default-pattern-categories.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-default-pattern-categories.js @@ -1,47 +1,32 @@ /** * WordPress dependencies */ -import { useMemo } from '@wordpress/element'; +import { store as coreStore } from '@wordpress/core-data'; +import { useSelect } from '@wordpress/data'; /** * Internal dependencies */ -import useDefaultPatternCategories from './use-default-pattern-categories'; -import useThemePatterns from './use-theme-patterns'; - -export default function usePatternCategories() { - const defaultCategories = useDefaultPatternCategories(); - const themePatterns = useThemePatterns(); - - const patternCategories = useMemo( () => { - const categoryMap = {}; - const categoriesWithCounts = []; - - // Create a map for easier counting of patterns in categories. - defaultCategories.forEach( ( category ) => { - if ( ! categoryMap[ category.name ] ) { - categoryMap[ category.name ] = { ...category, count: 0 }; - } - } ); - - // Update the category counts to reflect theme registered patterns. - themePatterns.forEach( ( pattern ) => { - pattern.categories?.forEach( ( category ) => { - if ( categoryMap[ category ] ) { - categoryMap[ category ].count += 1; - } - } ); - } ); - - // Filter categories so we only have those containing patterns. - defaultCategories.forEach( ( category ) => { - if ( categoryMap[ category.name ].count ) { - categoriesWithCounts.push( categoryMap[ category.name ] ); - } - } ); - - return categoriesWithCounts; - }, [ defaultCategories, themePatterns ] ); - - return { patternCategories, hasPatterns: !! patternCategories.length }; +import { unlock } from '../../lock-unlock'; +import { store as editSiteStore } from '../../store'; + +export default function useDefaultPatternCategories() { + const blockPatternCategories = useSelect( ( select ) => { + const { getSettings } = unlock( select( editSiteStore ) ); + const settings = getSettings(); + + return ( + settings.__experimentalAdditionalBlockPatternCategories ?? + settings.__experimentalBlockPatternCategories + ); + } ); + + const restBlockPatternCategories = useSelect( ( select ) => + select( coreStore ).getBlockPatternCategories() + ); + + return [ + ...( blockPatternCategories || [] ), + ...( restBlockPatternCategories || [] ), + ]; } 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 a787f8c04c6390..96491018d07722 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 @@ -1,10 +1,7 @@ /** * WordPress dependencies */ -import { store as coreStore } from '@wordpress/core-data'; -import { useSelect } from '@wordpress/data'; import { useMemo } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; /** * Internal dependencies @@ -15,11 +12,6 @@ import useThemePatterns from './use-theme-patterns'; export default function usePatternCategories() { const defaultCategories = useDefaultPatternCategories(); const themePatterns = useThemePatterns(); - const userPatterns = useSelect( ( select ) => - select( coreStore ).getEntityRecords( 'postType', 'wp_block', { - per_page: -1, - } ) - ); const patternCategories = useMemo( () => { const categoryMap = {}; @@ -48,17 +40,8 @@ export default function usePatternCategories() { } } ); - // Add "Your Patterns" category for user patterns if there are any. - if ( userPatterns?.length ) { - categoriesWithCounts.push( { - count: userPatterns.length || 0, - name: 'custom-patterns', - label: __( 'Custom patterns' ), - } ); - } - return categoriesWithCounts; - }, [ defaultCategories, themePatterns, userPatterns ] ); + }, [ defaultCategories, themePatterns ] ); return { patternCategories, hasPatterns: !! patternCategories.length }; } From 9f17e02ea4208826c85970a9ec160adc34e7eb95 Mon Sep 17 00:00:00 2001 From: Carolina Nymark Date: Tue, 11 Jul 2023 10:14:25 +0200 Subject: [PATCH 5/5] Update category-item.js --- .../category-item.js | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/category-item.js b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/category-item.js index 6e5096bee3fbe8..cefa40fa16f330 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/category-item.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/category-item.js @@ -1,3 +1,9 @@ +/** + * WordPress dependencies + */ +import { useInstanceId } from '@wordpress/compose'; +import { VisuallyHidden } from '@wordpress/components'; + /** * Internal dependencies */ @@ -12,6 +18,7 @@ export default function CategoryItem( { label, type, } ) { + const instanceId = useInstanceId( CategoryItem ); const linkInfo = useLink( { path: '/patterns', @@ -31,13 +38,19 @@ export default function CategoryItem( { } return ( - { count } } - aria-current={ isActive ? 'true' : undefined } - > - { label } - + <> + + + { count } + + ); }