diff --git a/packages/edit-site/src/components/page-content-focus-manager/constants.js b/packages/edit-site/src/components/page-content-focus-manager/constants.js
deleted file mode 100644
index a81b2fd37563af..00000000000000
--- a/packages/edit-site/src/components/page-content-focus-manager/constants.js
+++ /dev/null
@@ -1,5 +0,0 @@
-export const PAGE_CONTENT_BLOCK_TYPES = [
- 'core/post-title',
- 'core/post-featured-image',
- 'core/post-content',
-];
diff --git a/packages/edit-site/src/components/page-content-focus-manager/disable-non-page-content-blocks.js b/packages/edit-site/src/components/page-content-focus-manager/disable-non-page-content-blocks.js
index 33ea486863d203..7b184ff253c7e6 100644
--- a/packages/edit-site/src/components/page-content-focus-manager/disable-non-page-content-blocks.js
+++ b/packages/edit-site/src/components/page-content-focus-manager/disable-non-page-content-blocks.js
@@ -10,16 +10,22 @@ import { useEffect } from '@wordpress/element';
* Internal dependencies
*/
import { unlock } from '../../lock-unlock';
-import { PAGE_CONTENT_BLOCK_TYPES } from './constants';
const { useBlockEditingMode } = unlock( blockEditorPrivateApis );
+const PAGE_CONTENT_BLOCK_TYPES = [
+ 'core/post-title',
+ 'core/post-featured-image',
+ 'core/post-content',
+];
+
/**
* Component that when rendered, makes it so that the site editor allows only
* page content to be edited.
*/
export default function DisableNonPageContentBlocks() {
useDisableNonPageContentBlocks();
+ return null;
}
/**
@@ -43,8 +49,11 @@ export function useDisableNonPageContentBlocks() {
const withDisableNonPageContentBlocks = createHigherOrderComponent(
( BlockEdit ) => ( props ) => {
- const isContent = PAGE_CONTENT_BLOCK_TYPES.includes( props.name );
- const mode = isContent ? 'contentOnly' : undefined;
+ const isDescendentOfQueryLoop = !! props.context.queryId;
+ const isPageContent =
+ PAGE_CONTENT_BLOCK_TYPES.includes( props.name ) &&
+ ! isDescendentOfQueryLoop;
+ const mode = isPageContent ? 'contentOnly' : undefined;
useBlockEditingMode( mode );
return ;
},
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 ;
+}
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 8795e41eedd4f3..7f40fbce9035cf 100644
--- a/packages/edit-site/src/components/page-patterns/grid-item.js
+++ b/packages/edit-site/src/components/page-patterns/grid-item.js
@@ -24,7 +24,7 @@ import {
Icon,
header,
footer,
- symbolFilled,
+ symbolFilled as uncategorized,
moreHorizontal,
lockSmall,
} from '@wordpress/icons';
@@ -35,23 +35,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,
} );
@@ -67,27 +75,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.' )
@@ -95,19 +117,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 (
<>
@@ -117,7 +144,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
@@ -169,58 +196,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 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 (
+ <>
+
+ { 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 9326a966123198..7a7bf026b9c625 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 cef7b4721193f4..295d1eee8e410f 100644
--- a/packages/edit-site/src/components/page-patterns/use-patterns.js
+++ b/packages/edit-site/src/components/page-patterns/use-patterns.js
@@ -31,21 +31,17 @@ 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,
templatePart,
} );
-const templatePartCategories = [ 'header', 'footer', 'sidebar' ];
-const templatePartHasCategory = ( item, category ) => {
- if ( category === 'uncategorized' ) {
- return ! templatePartCategories.includes( item.templatePart.area );
- }
-
- return item.templatePart.area === category;
-};
+const templatePartHasCategory = ( item, category ) =>
+ item.templatePart.area === category;
const useTemplatePartsAsPatterns = (
categoryId,
diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-content.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-content.js
index d6e7dd23a709fa..dd40bcaef9f707 100644
--- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-content.js
+++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-content.js
@@ -6,22 +6,24 @@ import {
store as blockEditorStore,
privateApis as blockEditorPrivateApis,
} from '@wordpress/block-editor';
+import { useMemo } from '@wordpress/element';
/**
* Internal dependencies
*/
-import { PAGE_CONTENT_BLOCK_TYPES } from '../../page-content-focus-manager/constants';
import { unlock } from '../../../lock-unlock';
const { BlockQuickNavigation } = unlock( blockEditorPrivateApis );
export default function PageContent() {
- const clientIds = useSelect(
+ const clientIdsTree = useSelect(
( select ) =>
- select( blockEditorStore ).__experimentalGetGlobalBlocksByName(
- PAGE_CONTENT_BLOCK_TYPES
- ),
+ unlock( select( blockEditorStore ) ).getEnabledClientIdsTree(),
[]
);
+ const clientIds = useMemo(
+ () => clientIdsTree.map( ( { clientId } ) => clientId ),
+ [ clientIdsTree ]
+ );
return ;
}
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-main/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-main/index.js
index 58b93d61c45a65..152139870fa59f 100644
--- a/packages/edit-site/src/components/sidebar-navigation-screen-main/index.js
+++ b/packages/edit-site/src/components/sidebar-navigation-screen-main/index.js
@@ -20,6 +20,7 @@ import SidebarNavigationItem from '../sidebar-navigation-item';
import { SidebarNavigationItemGlobalStyles } from '../sidebar-navigation-screen-global-styles';
import { unlock } from '../../lock-unlock';
import { store as editSiteStore } from '../../store';
+import TemplatePartHint from './template-part-hint';
export default function SidebarNavigationScreenMain() {
const { location } = useNavigator();
@@ -42,46 +43,49 @@ export default function SidebarNavigationScreenMain() {
'Customize the appearance of your website using the block editor.'
) }
content={
-
-
- { __( 'Navigation' ) }
-
-
- { __( 'Styles' ) }
-
-
- { __( 'Pages' ) }
-
-
- { __( 'Templates' ) }
-
-
- { __( 'Patterns' ) }
-
-
+ <>
+
+
+ { __( 'Navigation' ) }
+
+
+ { __( 'Styles' ) }
+
+
+ { __( 'Pages' ) }
+
+
+ { __( 'Templates' ) }
+
+
+ { __( 'Patterns' ) }
+
+
+
+ >
}
/>
);
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-main/template-part-hint.js b/packages/edit-site/src/components/sidebar-navigation-screen-main/template-part-hint.js
new file mode 100644
index 00000000000000..8fbe74f81bb4d9
--- /dev/null
+++ b/packages/edit-site/src/components/sidebar-navigation-screen-main/template-part-hint.js
@@ -0,0 +1,36 @@
+/**
+ * WordPress dependencies
+ */
+import { Notice } from '@wordpress/components';
+import { useDispatch, useSelect } from '@wordpress/data';
+import { __ } from '@wordpress/i18n';
+import { store as preferencesStore } from '@wordpress/preferences';
+
+const PREFERENCE_NAME = 'isTemplatePartMoveHintVisible';
+
+export default function TemplatePartHint() {
+ const showTemplatePartHint = useSelect(
+ ( select ) =>
+ select( preferencesStore ).get( 'core', PREFERENCE_NAME ) ?? true,
+ []
+ );
+
+ const { set: setPreference } = useDispatch( preferencesStore );
+ if ( ! showTemplatePartHint ) {
+ return null;
+ }
+
+ return (
+ {
+ setPreference( 'core', PREFERENCE_NAME, false );
+ } }
+ >
+ { __(
+ 'Looking for template parts? You can now find them in the new "Patterns" page.'
+ ) }
+
+ );
+}
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 b28aa6687723b7..f200382f963113 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
@@ -7,6 +7,7 @@ import {
Flex,
Icon,
Tooltip,
+ __experimentalHeading as Heading,
} from '@wordpress/components';
import { useViewportMatch } from '@wordpress/compose';
import { useSelect } from '@wordpress/data';
@@ -29,12 +30,79 @@ 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' ) }
+
{ __( 'Synced patterns for use in template building.' ) }
+
+
+ { Object.entries( areas ).map(
+ ( [ area, { label, templateParts } ] ) => (
+
+ )
+ ) }
+
+ >
+ );
+}
+
+function ThemePatternsGroup( { categories, currentCategory, currentType } ) {
+ return (
+ <>
+
+
{ __( 'Theme patterns' ) }
+
+ { __(
+ 'For insertion into documents where they can then be customized.'
+ ) }
+
+
+
+ { categories.map( ( category ) => (
+
+ { category.label }
+
+
+
+
+
+
+ }
+ icon={ file }
+ id={ category.name }
+ type="pattern"
+ isActive={
+ currentCategory === `${ category.name }` &&
+ currentType === 'pattern'
+ }
+ />
+ ) ) }
+
+ >
+ );
+}
export default function SidebarNavigationScreenPatterns() {
const isMobileViewport = useViewportMatch( 'medium', '<' );
@@ -109,76 +177,18 @@ export default function SidebarNavigationScreenPatterns() {
) }
{ hasTemplateParts && (
-
- { Object.entries( templatePartAreas ).map(
- ( [ area, parts ] ) => (
-
- )
- ) }
-
+
) }
{ hasPatterns && (
-
- { patternCategories.map( ( category ) => (
-
- { category.label }
-
-
-
-
-
-
- }
- icon={ file }
- id={ category.name }
- type="pattern"
- isActive={
- currentCategory ===
- `${ category.name }` &&
- currentType === 'pattern'
- }
- />
- ) ) }
-
+
) }
>
) }
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 f0edb96164abca..65790b5e862162 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,28 @@
.edit-site-sidebar-navigation-screen-patterns__group {
- margin-bottom: $grid-unit-30;
+ margin-bottom: $grid-unit-40;
+ padding-bottom: $grid-unit-30;
+ border-bottom: 1px solid $gray-800;
+
+ &: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-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/style.scss b/packages/edit-site/src/components/sidebar-navigation-screen/style.scss
index 1fecf49d712215..26a30da286fc84 100644
--- a/packages/edit-site/src/components/sidebar-navigation-screen/style.scss
+++ b/packages/edit-site/src/components/sidebar-navigation-screen/style.scss
@@ -98,6 +98,20 @@
border-top: 1px solid $gray-800;
}
+.edit-site-sidebar__notice {
+ background: $gray-800;
+ color: $gray-300;
+ margin: $grid-unit-30 0;
+ &.is-dismissible {
+ padding-right: $grid-unit-10;
+ }
+ .components-notice__dismiss:not(:disabled):not([aria-disabled="true"]):focus,
+ .components-notice__dismiss:not(:disabled):not([aria-disabled="true"]):not(.is-secondary):active,
+ .components-notice__dismiss:not(:disabled):not([aria-disabled="true"]):not(.is-secondary):hover {
+ color: $gray-100;
+ }
+}
+
/* In general style overrides are discouraged.
* This is a temporary solution to override the InputControl component's styles.
* The `Theme` component will potentially be the more appropriate approach
diff --git a/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js b/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js
index d82f3a86847da9..ab2487e76f1910 100644
--- a/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js
+++ b/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
-import { get, set } from 'lodash';
+import { get } from 'lodash';
/**
* WordPress dependencies
@@ -119,6 +119,46 @@ function useChangesToPush( name, attributes ) {
);
}
+/**
+ * Sets the value at path of object.
+ * If a portion of path doesn’t exist, it’s created.
+ * Arrays are created for missing index properties while objects are created
+ * for all other missing properties.
+ *
+ * This function intentionally mutates the input object.
+ *
+ * Inspired by _.set().
+ *
+ * @see https://lodash.com/docs/4.17.15#set
+ *
+ * @todo Needs to be deduplicated with its copy in `@wordpress/core-data`.
+ *
+ * @param {Object} object Object to modify
+ * @param {Array} path Path of the property to set.
+ * @param {*} value Value to set.
+ */
+function setNestedValue( object, path, value ) {
+ if ( ! object || typeof object !== 'object' ) {
+ return object;
+ }
+
+ path.reduce( ( acc, key, idx ) => {
+ if ( acc[ key ] === undefined ) {
+ if ( Number.isInteger( path[ idx + 1 ] ) ) {
+ acc[ key ] = [];
+ } else {
+ acc[ key ] = {};
+ }
+ }
+ if ( idx === path.length - 1 ) {
+ acc[ key ] = value;
+ }
+ return acc[ key ];
+ }, object );
+
+ return object;
+}
+
function cloneDeep( object ) {
return ! object ? {} : JSON.parse( JSON.stringify( object ) );
}
@@ -148,8 +188,12 @@ function PushChangesToGlobalStylesControl( {
const newUserConfig = cloneDeep( userConfig );
for ( const { path, value } of changes ) {
- set( newBlockStyles, path, undefined );
- set( newUserConfig, [ 'styles', 'blocks', name, ...path ], value );
+ setNestedValue( newBlockStyles, path, undefined );
+ setNestedValue(
+ newUserConfig,
+ [ 'styles', 'blocks', name, ...path ],
+ value
+ );
}
// @wordpress/core-data doesn't support editing multiple entity types in
diff --git a/packages/react-native-aztec/package.json b/packages/react-native-aztec/package.json
index 371c890feae125..3087d3708a7047 100644
--- a/packages/react-native-aztec/package.json
+++ b/packages/react-native-aztec/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/react-native-aztec",
- "version": "1.98.1",
+ "version": "1.99.0",
"description": "Aztec view for react-native.",
"private": true,
"author": "The WordPress Contributors",
diff --git a/packages/react-native-bridge/package.json b/packages/react-native-bridge/package.json
index 1e4039e8135e1d..8c07de0e29871d 100644
--- a/packages/react-native-bridge/package.json
+++ b/packages/react-native-bridge/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/react-native-bridge",
- "version": "1.98.1",
+ "version": "1.99.0",
"description": "Native bridge library used to integrate the block editor into a native App.",
"private": true,
"author": "The WordPress Contributors",
diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md
index d43144db54722a..5a1dc4d533a686 100644
--- a/packages/react-native-editor/CHANGELOG.md
+++ b/packages/react-native-editor/CHANGELOG.md
@@ -10,8 +10,11 @@ For each user feature we should also add a importance categorization label to i
-->
## Unreleased
+
+## 1.99.0
- [*] Rename "Reusable blocks" to "Synced patterns", aligning with the web editor. [#51704]
- [**] Fix a crash related to Reanimated when closing the editor [#52320]
+- [**] Add media inserter buttons to editor toolbar [#51827]
## 1.98.1
- [*] fix: Display heading level dropdown icons and labels [#52004]
diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-block-insertion-@canary.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-block-insertion-@canary.test.js
index b413f3b6a42cea..05f7c6bfcd0777 100644
--- a/packages/react-native-editor/__device-tests__/gutenberg-editor-block-insertion-@canary.test.js
+++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-block-insertion-@canary.test.js
@@ -104,7 +104,7 @@ describe( 'Gutenberg Editor Slash Inserter tests', () => {
);
expect( await editorPage.assertSlashInserterPresent() ).toBe( true );
- await editorPage.removeBlockAtPosition( blockNames.paragraph );
+ await editorPage.removeBlock();
} );
it( 'should hide the menu after deleting the / character', async () => {
@@ -139,7 +139,7 @@ describe( 'Gutenberg Editor Slash Inserter tests', () => {
// Check if the slash inserter UI no longer exists.
expect( await editorPage.assertSlashInserterPresent() ).toBe( false );
- await editorPage.removeBlockAtPosition( blockNames.paragraph );
+ await editorPage.removeBlock();
} );
it( 'should add an Image block after tying /image and tapping on the Image block button', async () => {
@@ -172,7 +172,7 @@ describe( 'Gutenberg Editor Slash Inserter tests', () => {
expect( await editorPage.assertSlashInserterPresent() ).toBe( false );
// Remove image block.
- await editorPage.removeBlockAtPosition( blockNames.image );
+ await editorPage.removeBlock();
} );
it( 'should insert an embed image block with "/img" + enter', async () => {
@@ -190,6 +190,6 @@ describe( 'Gutenberg Editor Slash Inserter tests', () => {
await editorPage.hasBlockAtPosition( 1, blockNames.embed )
).toBe( true );
- await editorPage.removeBlockAtPosition( blockNames.embed );
+ await editorPage.removeBlock();
} );
} );
diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-device-actions.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-device-actions.test.js
index 3a64ca37508980..e5e7b5c829f8e9 100644
--- a/packages/react-native-editor/__device-tests__/gutenberg-editor-device-actions.test.js
+++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-device-actions.test.js
@@ -108,8 +108,8 @@ describe( 'Gutenberg Editor Paste tests', () => {
const text = await editorPage.getTextForParagraphBlockAtPosition( 2 );
expect( text ).toBe( testData.pastePlainText );
- await editorPage.removeBlockAtPosition( blockNames.paragraph, 2 );
- await editorPage.removeBlockAtPosition( blockNames.paragraph, 1 );
+ await editorPage.removeBlock();
+ await editorPage.removeBlock();
} );
it.skip( 'copies styled text from one paragraph block and pastes in another', async () => {
diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-media-blocks-@canary.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-media-blocks-@canary.test.js
index 6fd68a7a4aff34..e17e39bd8357f9 100644
--- a/packages/react-native-editor/__device-tests__/gutenberg-editor-media-blocks-@canary.test.js
+++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-media-blocks-@canary.test.js
@@ -134,7 +134,7 @@ onlyOniOS( 'Gutenberg Editor Cover Block test', () => {
// Navigate upwards to select parent block
await editorPage.moveBlockSelectionUp();
- await editorPage.removeBlockAtPosition( blockNames.cover );
+ await editorPage.removeBlock();
} );
// Testing this for iOS on a device is valuable to ensure that it properly
@@ -165,6 +165,6 @@ onlyOniOS( 'Gutenberg Editor Cover Block test', () => {
await editorPage.chooseMediaLibrary();
expect( coverBlock ).toBeTruthy();
- await editorPage.removeBlockAtPosition( blockNames.cover );
+ await editorPage.removeBlock();
} );
} );
diff --git a/packages/react-native-editor/__device-tests__/pages/editor-page.js b/packages/react-native-editor/__device-tests__/pages/editor-page.js
index 3b06187482a6f4..f03dae92c4174c 100644
--- a/packages/react-native-editor/__device-tests__/pages/editor-page.js
+++ b/packages/react-native-editor/__device-tests__/pages/editor-page.js
@@ -341,6 +341,46 @@ class EditorPage {
}
}
+ async swipeToolbarToElement( elementSelector, options ) {
+ const { byId, swipeRight } = options || {};
+ const offset = isAndroid() ? 300 : 50;
+ const maxLocatorAttempts = 5;
+ let locatorAttempts = 0;
+ let element;
+
+ const toolbar = await this.getToolbar();
+ const toolbarLocation = await toolbar.getLocation();
+ const toolbarSize = await toolbar.getSize();
+
+ while ( locatorAttempts < maxLocatorAttempts ) {
+ element = byId
+ ? await this.driver.elementsByAccessibilityId( elementSelector )
+ : await this.driver.elementsByXPath( elementSelector );
+ if ( await element[ 0 ]?.isDisplayed() ) {
+ break;
+ }
+
+ swipeFromTo(
+ this.driver,
+ {
+ x: ! swipeRight
+ ? toolbarSize.width - offset
+ : toolbarSize.width / 2,
+ y: toolbarLocation.y + toolbarSize.height / 2,
+ },
+ {
+ x: ! swipeRight
+ ? toolbarSize.width / 2
+ : toolbarSize.width - offset,
+ y: toolbarLocation.y + toolbarSize.height / 2,
+ },
+ 1000
+ );
+ locatorAttempts++;
+ }
+ return element;
+ }
+
async openBlockSettings() {
const settingsButtonElement = isAndroid()
? '//android.widget.Button[@content-desc="Open Settings"]/android.view.ViewGroup'
@@ -356,10 +396,10 @@ class EditorPage {
const blockActionsButtonElement = isAndroid()
? '//android.widget.Button[contains(@content-desc, "Open Block Actions Menu")]'
: '//XCUIElementTypeButton[@name="Open Block Actions Menu"]';
- const blockActionsMenu = await this.waitForElementToBeDisplayedByXPath(
+ const blockActionsMenu = await this.swipeToolbarToElement(
blockActionsButtonElement
);
- await blockActionsMenu.click();
+ await blockActionsMenu[ 0 ].click();
const removeElement = 'Remove block';
const removeBlockButton = await this.waitForElementToBeDisplayedById(
@@ -378,13 +418,16 @@ class EditorPage {
// =========================
async getToolbar() {
- return await this.driver.elementsByAccessibilityId( 'Document tools' );
+ return this.waitForElementToBeDisplayedById( 'Document tools', 4000 );
}
async addNewBlock( blockName, { skipInserterOpen = false } = {} ) {
if ( ! skipInserterOpen ) {
- const addButton = await this.getAddBlockButton();
- await addButton.click();
+ const addButton = await this.swipeToolbarToElement( ADD_BLOCK_ID, {
+ byId: true,
+ swipeRight: true,
+ } );
+ await addButton[ 0 ].click();
}
// Click on block of choice.
@@ -599,10 +642,8 @@ class EditorPage {
const identifier = isAndroid()
? `//android.widget.Button[@content-desc="${ formatting }"]/android.view.ViewGroup`
: `//XCUIElementTypeButton[@name="${ formatting }"]`;
- const toggleElement = await this.waitForElementToBeDisplayedByXPath(
- identifier
- );
- return await toggleElement.click();
+ const toggleElement = await this.swipeToolbarToElement( identifier );
+ return await toggleElement[ 0 ].click();
}
async openLinkToSettings() {
diff --git a/packages/react-native-editor/ios/Podfile.lock b/packages/react-native-editor/ios/Podfile.lock
index e33ff9f46d7890..27d2e6ca929b51 100644
--- a/packages/react-native-editor/ios/Podfile.lock
+++ b/packages/react-native-editor/ios/Podfile.lock
@@ -13,7 +13,7 @@ PODS:
- ReactCommon/turbomodule/core (= 0.69.4)
- fmt (6.2.1)
- glog (0.3.5)
- - Gutenberg (1.98.1):
+ - Gutenberg (1.99.0):
- React-Core (= 0.69.4)
- React-CoreModules (= 0.69.4)
- React-RCTImage (= 0.69.4)
@@ -360,7 +360,7 @@ PODS:
- React-Core
- RNSVG (9.13.6):
- React-Core
- - RNTAztecView (1.98.1):
+ - RNTAztecView (1.99.0):
- React-Core
- WordPress-Aztec-iOS (~> 1.19.8)
- SDWebImage (5.11.1):
@@ -540,7 +540,7 @@ SPEC CHECKSUMS:
FBReactNativeSpec: 2ff441cbe6e58c1778d8a5cf3311831a6a8c0809
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
glog: 3d02b25ca00c2d456734d0bcff864cbc62f6ae1a
- Gutenberg: 36708d354578d1fd904c5c93fa8199b31b4cbb42
+ Gutenberg: 06d0e1bc1dbd7ad23b8f9b587cceba18aa8518da
libwebp: 60305b2e989864154bd9be3d772730f08fc6a59c
RCT-Folly: b9d9fe1fc70114b751c076104e52f3b1b5e5a95a
RCTRequired: bd9d2ab0fda10171fcbcf9ba61a7df4dc15a28f4
@@ -556,13 +556,13 @@ SPEC CHECKSUMS:
React-jsiexecutor: a27badbbdbc0ff781813370736a2d1c7261181d4
React-jsinspector: 8a3d3f5dcd23a91e8c80b1bf0e96902cd1dca999
React-logger: 1088859f145b8f6dd0d3ed051a647ef0e3e80fad
- react-native-blur: 8cd9b4a8007166ad643f4dff914c3fddd2ff5b9a
+ react-native-blur: 3e9c8e8e9f7d17fa1b94e1a0ae9fd816675f5382
react-native-get-random-values: b6fb85e7169b9822976793e467458c151c3e8b69
react-native-safe-area: c9cf765aa2dd96159476a99633e7d462ce5bb94f
- react-native-safe-area-context: e471852c5ed67eea4b10c5d9d43c1cebae3b231d
+ react-native-safe-area-context: f0906bf8bc9835ac9a9d3f97e8bde2a997d8da79
react-native-slider: dff0d8a46f368a8d1bacd8638570d75b9b0be400
- react-native-video: afb806880af4f6612683ab678a793ae41bc39705
- react-native-webview: e3b659a6d614bb37fb12a2de82c91a378c59d84b
+ react-native-video: 6dee623307ed9d04d1be2de87494f9a0fa2041d1
+ react-native-webview: 9f111dfbcfc826084d6c507f569e5e03342ee1c1
React-perflogger: cb386fd44c97ec7f8199c04c12b22066b0f2e1e0
React-RCTActionSheet: f803a85e46cf5b4066c2ac5e122447f918e9c6e5
React-RCTAnimation: 19c80fa950ccce7f4db76a2a7f2cf79baae07fc7
@@ -575,14 +575,14 @@ SPEC CHECKSUMS:
React-RCTVibration: 9adb4a3cbb598d1bbd46a05256f445e4b8c70603
React-runtimeexecutor: 61ee22a8cdf8b6bb2a7fb7b4ba2cc763e5285196
ReactCommon: 8f67bd7e0a6afade0f20718f859dc8c2275f2e83
- RNCClipboard: f49f3de56b40d0f4104680dabadc7a1f063f4fd4
- RNCMaskedView: d367b2a8df3992114e31b32b091a0c00dc800827
+ RNCClipboard: 99fc8ad669a376b756fbc8098ae2fd05c0ed0668
+ RNCMaskedView: c298b644a10c0c142055b3ae24d83879ecb13ccd
RNFastImage: 1f2cab428712a4baaf78d6169eaec7f622556dd7
RNGestureHandler: f5c389f7c9947057ee47d16ca1d7d170289b2c2a
RNReanimated: 5740ec9926f80bccd404bacd3e71108e87c94afa
RNScreens: 953633729a42e23ad0c93574d676b361e3335e8b
RNSVG: 36a7359c428dcb7c6bce1cc546fbfebe069809b0
- RNTAztecView: 3e9b521a52ad407c08235c82ab586bad6bb5f0f7
+ RNTAztecView: 4ccd0ce94481a4026420a7b0725ce86e762f1c16
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
WordPress-Aztec-iOS: 7d11d598f14c82c727c08b56bd35fbeb7dafb504
diff --git a/packages/react-native-editor/package.json b/packages/react-native-editor/package.json
index 1369f8c193b36d..4034ada7699cab 100644
--- a/packages/react-native-editor/package.json
+++ b/packages/react-native-editor/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/react-native-editor",
- "version": "1.98.1",
+ "version": "1.99.0",
"description": "Mobile WordPress gutenberg editor.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
diff --git a/packages/reusable-blocks/src/components/reusable-blocks-menu-items/reusable-block-convert-button.js b/packages/reusable-blocks/src/components/reusable-blocks-menu-items/reusable-block-convert-button.js
index 875adb8fc16e3a..981776880a1374 100644
--- a/packages/reusable-blocks/src/components/reusable-blocks-menu-items/reusable-block-convert-button.js
+++ b/packages/reusable-blocks/src/components/reusable-blocks-menu-items/reusable-block-convert-button.js
@@ -19,7 +19,7 @@ import {
} from '@wordpress/components';
import { symbol } from '@wordpress/icons';
import { useDispatch, useSelect } from '@wordpress/data';
-import { __ } from '@wordpress/i18n';
+import { __, sprintf } from '@wordpress/i18n';
import { store as noticesStore } from '@wordpress/notices';
import { store as coreStore } from '@wordpress/core-data';
@@ -98,15 +98,25 @@ export default function ReusableBlockConvertButton( {
);
createSuccessNotice(
syncType === 'fully'
- ? __( 'Synced Pattern created.' )
- : __( 'Unsynced Pattern created.' ),
+ ? sprintf(
+ // translators: %s: the name the user has given to the pattern.
+ __( 'Synced Pattern created: %s' ),
+ reusableBlockTitle
+ )
+ : sprintf(
+ // translators: %s: the name the user has given to the pattern.
+ __( 'Unsynced Pattern created: %s' ),
+ reusableBlockTitle
+ ),
{
type: 'snackbar',
+ id: 'convert-to-reusable-block-success',
}
);
} catch ( error ) {
createErrorNotice( error.message, {
type: 'snackbar',
+ id: 'convert-to-reusable-block-error',
} );
}
},
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 6f339058885111..e3bbef8bf77388 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 (
-