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 0000000000000..d2c14d15f341b
--- /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 ;
+}
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 7db14e1d37788..71356ff4cf540 100644
--- a/packages/edit-site/src/components/page-patterns/grid-item.js
+++ b/packages/edit-site/src/components/page-patterns/grid-item.js
@@ -25,7 +25,7 @@ import {
Icon,
header,
footer,
- symbolFilled,
+ symbolFilled as uncategorized,
moreHorizontal,
lockSmall,
} from '@wordpress/icons';
@@ -36,23 +36,31 @@ import { DELETE, BACKSPACE } from '@wordpress/keycodes';
/**
* Internal dependencies
*/
-import { PATTERNS, USER_PATTERNS } from './utils';
+import RenameMenuItem from './rename-menu-item';
+import DuplicateMenuItem from './duplicate-menu-item';
+import { PATTERNS, TEMPLATE_PARTS, USER_PATTERNS } from './utils';
+import { store as editSiteStore } from '../../store';
import { useLink } from '../routes/link';
-const THEME_PATTERN_TOOLTIP = __( 'Theme patterns cannot be edited.' );
+const templatePartIcons = { header, footer, uncategorized };
export default function GridItem( { categoryId, composite, icon, item } ) {
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: item.type === USER_PATTERNS ? item.id : item.name,
+ postId: isUserPattern ? item.id : item.name,
categoryId,
categoryType: item.type,
} );
@@ -68,27 +76,41 @@ export default function GridItem( { categoryId, composite, icon, item } ) {
'is-placeholder': isEmpty,
} );
const previewClassNames = classnames( 'edit-site-patterns__preview', {
- 'is-inactive': item.type === PATTERNS,
+ 'is-inactive': isNonUserPattern,
} );
const deletePattern = async () => {
try {
await __experimentalDeleteReusableBlock( item.id );
- createSuccessNotice( __( 'Pattern successfully deleted.' ), {
- type: 'snackbar',
- } );
+ 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' } );
+ createErrorNotice( errorMessage, {
+ type: 'snackbar',
+ id: 'edit-site-patterns-error',
+ } );
}
};
+ const deleteItem = () =>
+ isTemplatePart ? removeTemplate( item ) : deletePattern();
- const isUserPattern = item.type === USER_PATTERNS;
+ // 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 ( isUserPattern ) {
+
+ 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.' )
@@ -96,19 +118,24 @@ export default function GridItem( { categoryId, composite, icon, item } ) {
} else if ( item.description ) {
ariaDescriptions.push( item.description );
}
- if ( item.type === PATTERNS ) {
- ariaDescriptions.push( THEME_PATTERN_TOOLTIP );
- }
- let itemIcon = icon;
- if ( categoryId === 'header' ) {
- itemIcon = header;
- } else if ( categoryId === 'footer' ) {
- itemIcon = footer;
- } else if ( categoryId === 'uncategorized' ) {
- itemIcon = symbolFilled;
+ if ( isNonUserPattern ) {
+ ariaDescriptions.push( __( 'Theme patterns cannot be edited.' ) );
}
+ const itemIcon = templatePartIcons[ categoryId ]
+ ? templatePartIcons[ categoryId ]
+ : icon;
+
+ 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 (
<>
@@ -118,7 +145,7 @@ export default function GridItem( { categoryId, composite, icon, item } ) {
as="div"
{ ...composite }
onClick={ item.type !== PATTERNS ? onClick : undefined }
- onKeyDown={ isUserPattern ? onKeyDown : undefined }
+ onKeyDown={ isCustomPattern ? onKeyDown : undefined }
aria-label={ item.title }
aria-describedby={
ariaDescriptions.length
@@ -175,58 +202,73 @@ export default function GridItem( { categoryId, composite, icon, item } ) {
) }
>
-
+
) }
- { item.type === USER_PATTERNS && (
-
- { () => (
-
+
+ { ( { onClose } ) => (
+
+ { isCustomPattern && ! hasThemeFile && (
+
+ ) }
+
+ { isCustomPattern && (
-
- ) }
-
- ) }
+ ) }
+
+ ) }
+
{ isDeleteDialogOpen && (
setIsDeleteDialogOpen( false ) }
>
- { __( 'Are you sure you want to delete this pattern?' ) }
+ { confirmPrompt }
) }
>
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 0000000000000..938023a62cefd
--- /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 (
+ <>
+
+ { isModalOpen && (
+ {
+ setIsModalOpen( false );
+ onClose();
+ } }
+ overlayClassName="edit-site-list__rename_modal"
+ >
+
+
+ ) }
+ >
+ );
+}
diff --git a/packages/edit-site/src/components/page-patterns/style.scss b/packages/edit-site/src/components/page-patterns/style.scss
index fdf0aea3431f6..0c7660035fb3e 100644
--- a/packages/edit-site/src/components/page-patterns/style.scss
+++ b/packages/edit-site/src/components/page-patterns/style.scss
@@ -101,6 +101,10 @@
.edit-site-patterns__pattern-lock-icon {
display: inline-flex;
+
+ svg {
+ fill: currentcolor;
+ }
}
}
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 a394aabf572c4..295d1eee8e410 100644
--- a/packages/edit-site/src/components/page-patterns/use-patterns.js
+++ b/packages/edit-site/src/components/page-patterns/use-patterns.js
@@ -31,7 +31,9 @@ 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: templatePart.title.rendered,
type: templatePart.type,
From cfcf5ff64a9cbe7c9bd275d35a187b68c321e803 Mon Sep 17 00:00:00 2001
From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com>
Date: Fri, 7 Jul 2023 19:01:21 +1000
Subject: [PATCH 08/31] Patterns: Update manage pattern links to go to site
editor if available (#52403)
---
packages/edit-post/src/plugins/index.js | 40 ++++++++++---
.../reusable-blocks-manage-button.js | 59 +++++++++++--------
2 files changed, 67 insertions(+), 32 deletions(-)
diff --git a/packages/edit-post/src/plugins/index.js b/packages/edit-post/src/plugins/index.js
index e3bd1b2dd72bd..1cd03debbf7a7 100644
--- a/packages/edit-post/src/plugins/index.js
+++ b/packages/edit-post/src/plugins/index.js
@@ -2,6 +2,9 @@
* WordPress dependencies
*/
import { MenuItem, VisuallyHidden } from '@wordpress/components';
+import { store as coreStore } from '@wordpress/core-data';
+import { store as editorStore } from '@wordpress/editor';
+import { useSelect } from '@wordpress/data';
import { external } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';
import { registerPlugin } from '@wordpress/plugins';
@@ -15,6 +18,34 @@ import KeyboardShortcutsHelpMenuItem from './keyboard-shortcuts-help-menu-item';
import ToolsMoreMenuGroup from '../components/header/tools-more-menu-group';
import WelcomeGuideMenuItem from './welcome-guide-menu-item';
+function ManagePatternsMenuItem() {
+ const url = useSelect( ( select ) => {
+ const { canUser } = select( coreStore );
+ const { getEditorSettings } = select( editorStore );
+
+ const isBlockTheme = getEditorSettings().__unstableIsBlockBasedTheme;
+ const defaultUrl = addQueryArgs( 'edit.php', {
+ post_type: 'wp_block',
+ } );
+ const patternsUrl = addQueryArgs( 'site-editor.php', {
+ path: '/patterns',
+ } );
+
+ // The site editor and templates both check whether the user has
+ // edit_theme_options capabilities. We can leverage that here and not
+ // display the manage patterns link if the user can't access it.
+ return canUser( 'read', 'templates' ) && isBlockTheme
+ ? patternsUrl
+ : defaultUrl;
+ }, [] );
+
+ return (
+
+ );
+}
+
registerPlugin( 'edit-post', {
render() {
return (
@@ -22,14 +53,7 @@ registerPlugin( 'edit-post', {
{ ( { onClose } ) => (
<>
-
+
diff --git a/packages/reusable-blocks/src/components/reusable-blocks-menu-items/reusable-blocks-manage-button.js b/packages/reusable-blocks/src/components/reusable-blocks-menu-items/reusable-blocks-manage-button.js
index 6f33905888511..e3bbef8bf7738 100644
--- a/packages/reusable-blocks/src/components/reusable-blocks-menu-items/reusable-blocks-manage-button.js
+++ b/packages/reusable-blocks/src/components/reusable-blocks-menu-items/reusable-blocks-manage-button.js
@@ -18,28 +18,41 @@ import { store as coreStore } from '@wordpress/core-data';
import { store as reusableBlocksStore } from '../../store';
function ReusableBlocksManageButton( { clientId } ) {
- const { canRemove, isVisible, innerBlockCount } = useSelect(
- ( select ) => {
- const { getBlock, canRemoveBlock, getBlockCount } =
- select( blockEditorStore );
- const { canUser } = select( coreStore );
- const reusableBlock = getBlock( clientId );
+ const { canRemove, isVisible, innerBlockCount, managePatternsUrl } =
+ useSelect(
+ ( select ) => {
+ const { getBlock, canRemoveBlock, getBlockCount, getSettings } =
+ select( blockEditorStore );
+ const { canUser } = select( coreStore );
+ const reusableBlock = getBlock( clientId );
+ const isBlockTheme = getSettings().__unstableIsBlockBasedTheme;
- return {
- canRemove: canRemoveBlock( clientId ),
- isVisible:
- !! reusableBlock &&
- isReusableBlock( reusableBlock ) &&
- !! canUser(
- 'update',
- 'blocks',
- reusableBlock.attributes.ref
- ),
- innerBlockCount: getBlockCount( clientId ),
- };
- },
- [ clientId ]
- );
+ return {
+ canRemove: canRemoveBlock( clientId ),
+ isVisible:
+ !! reusableBlock &&
+ isReusableBlock( reusableBlock ) &&
+ !! canUser(
+ 'update',
+ 'blocks',
+ reusableBlock.attributes.ref
+ ),
+ innerBlockCount: getBlockCount( clientId ),
+ // The site editor and templates both check whether the user
+ // has edit_theme_options capabilities. We can leverage that here
+ // and omit the manage patterns link if the user can't access it.
+ managePatternsUrl:
+ isBlockTheme && canUser( 'read', 'templates' )
+ ? addQueryArgs( 'site-editor.php', {
+ path: '/patterns',
+ } )
+ : addQueryArgs( 'edit.php', {
+ post_type: 'wp_block',
+ } ),
+ };
+ },
+ [ clientId ]
+ );
const { __experimentalConvertBlockToStatic: convertBlockToStatic } =
useDispatch( reusableBlocksStore );
@@ -50,9 +63,7 @@ function ReusableBlocksManageButton( { clientId } ) {
return (
-