diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md
index dec943d4bb790..0a6519576da3a 100644
--- a/docs/reference-guides/core-blocks.md
+++ b/docs/reference-guides/core-blocks.md
@@ -36,7 +36,7 @@ Add a user’s avatar. ([Source](https://github.com/WordPress/gutenberg/tree/tru
- **Supports:** align, color (~~background~~, ~~text~~), spacing (margin, padding), ~~alignWide~~, ~~html~~
- **Attributes:** isLink, linkTarget, size, userId
-## Reusable block
+## Pattern
Create and save content to reuse across your site. Update the block, and the changes apply everywhere it’s used. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/block))
@@ -473,7 +473,7 @@ Start with the basic building block of all narrative. ([Source](https://github.c
- **Supports:** __unstablePasteTextInline, anchor, color (background, gradients, link, text), spacing (margin, padding), typography (fontSize, lineHeight), ~~className~~
- **Attributes:** align, content, direction, dropCap, placeholder
-## Pattern
+## Pattern placeholder
Show a block pattern. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/pattern))
diff --git a/docs/reference-guides/data/data-core-block-editor.md b/docs/reference-guides/data/data-core-block-editor.md
index 8b0f24e60fb7b..20d6ebfe5291a 100644
--- a/docs/reference-guides/data/data-core-block-editor.md
+++ b/docs/reference-guides/data/data-core-block-editor.md
@@ -538,6 +538,7 @@ _Parameters_
- _state_ `Object`: Editor state.
- _rootClientId_ `?string`: Optional root client ID of block list.
+- _syncStatus_ `?string`: Optional sync status to filter pattern blocks by.
_Returns_
diff --git a/lib/compat/wordpress-6.3/blocks.php b/lib/compat/wordpress-6.3/blocks.php
index 5f017997f52b5..6902a258c5c3d 100644
--- a/lib/compat/wordpress-6.3/blocks.php
+++ b/lib/compat/wordpress-6.3/blocks.php
@@ -26,3 +26,113 @@ function gutenberg_add_selectors_property_to_block_type_settings( $settings, $me
return $settings;
}
add_filter( 'block_type_metadata_settings', 'gutenberg_add_selectors_property_to_block_type_settings', 10, 2 );
+
+/**
+ * Renames Reusable block CPT to Pattern.
+ *
+ * Note: This should be removed when the minimum required WP version is >= 6.3.
+ *
+ * @see https://github.com/WordPress/gutenberg/pull/51144
+ *
+ * @param array $args Register post type args.
+ * @param string $post_type The post type string.
+ *
+ * @return array Register post type args.
+ */
+function gutenberg_rename_reusable_block_cpt_to_pattern( $args, $post_type ) {
+ if ( 'wp_block' === $post_type ) {
+ $args['labels']['name'] = _x( 'Patterns', 'post type general name' );
+ $args['labels']['singular_name'] = _x( 'Pattern', 'post type singular name' );
+ $args['labels']['add_new_item'] = __( 'Add new Pattern' );
+ $args['labels']['new_item'] = __( 'New Pattern' );
+ $args['labels']['edit_item'] = __( 'Edit Pattern' );
+ $args['labels']['view_item'] = __( 'View Pattern' );
+ $args['labels']['all_items'] = __( 'All Patterns' );
+ $args['labels']['search_items'] = __( 'Search Patterns' );
+ $args['labels']['not_found'] = __( 'No Patterns found.' );
+ $args['labels']['not_found_in_trash'] = __( 'No Patterns found in Trash.' );
+ $args['labels']['filter_items_list'] = __( 'Filter Patterns list' );
+ $args['labels']['items_list_navigation'] = __( 'Patterns list navigation' );
+ $args['labels']['items_list'] = __( 'Patterns list' );
+ $args['labels']['item_published'] = __( 'Pattern published.' );
+ $args['labels']['item_published_privately'] = __( 'Pattern published privately.' );
+ $args['labels']['item_reverted_to_draft'] = __( 'Pattern reverted to draft.' );
+ $args['labels']['item_scheduled'] = __( 'Pattern scheduled.' );
+ $args['labels']['item_updated'] = __( 'Pattern updated.' );
+ }
+
+ return $args;
+}
+
+add_filter( 'register_post_type_args', 'gutenberg_rename_reusable_block_cpt_to_pattern', 10, 2 );
+
+/**
+ * Adds custom fields support to the wp_block post type so an unsynced option can be added.
+ *
+ * Note: This should be removed when the minimum required WP version is >= 6.3.
+ *
+ * @see https://github.com/WordPress/gutenberg/pull/51144
+ *
+ * @param array $args Register post type args.
+ * @param string $post_type The post type string.
+ *
+ * @return array Register post type args.
+ */
+function gutenberg_add_custom_fields_to_wp_block( $args, $post_type ) {
+ if ( 'wp_block' === $post_type ) {
+ array_push( $args['supports'], 'custom-fields' );
+ }
+
+ return $args;
+}
+add_filter( 'register_post_type_args', 'gutenberg_add_custom_fields_to_wp_block', 10, 2 );
+
+/**
+ * Adds sync_status meta fields to the wp_block post type so an unsynced option can be added.
+ *
+ * Note: This should be removed when the minimum required WP version is >= 6.3.
+ *
+ * @see https://github.com/WordPress/gutenberg/pull/51144
+ *
+ * @return void
+ */
+function gutenberg_wp_block_register_post_meta() {
+ $post_type = 'wp_block';
+ register_post_meta(
+ $post_type,
+ 'sync_status',
+ array(
+ 'auth_callback' => function() {
+ return current_user_can( 'edit_posts' );
+ },
+ 'sanitize_callback' => 'gutenberg_wp_block_sanitize_post_meta',
+ 'single' => true,
+ 'type' => 'string',
+ 'show_in_rest' => array(
+ 'schema' => array(
+ 'type' => 'string',
+ 'properties' => array(
+ 'sync_status' => array(
+ 'type' => 'string',
+ ),
+ ),
+ ),
+ ),
+ )
+ );
+}
+/**
+ * Sanitizes the array of wp_block post meta sync_status string.
+ *
+ * Note: This should be removed when the minimum required WP version is >= 6.3.
+ *
+ * @see https://github.com/WordPress/gutenberg/pull/51144
+ *
+ * @param array $meta_value String to sanitize.
+ *
+ * @return array Sanitized string.
+ */
+function gutenberg_wp_block_sanitize_post_meta( $meta_value ) {
+ return sanitize_text_field( $meta_value );
+}
+add_action( 'init', 'gutenberg_wp_block_register_post_meta' );
diff --git a/packages/block-editor/src/components/inserter/block-patterns-tab.js b/packages/block-editor/src/components/inserter/block-patterns-tab.js
index 49a5939107bb1..578791e880269 100644
--- a/packages/block-editor/src/components/inserter/block-patterns-tab.js
+++ b/packages/block-editor/src/components/inserter/block-patterns-tab.js
@@ -18,6 +18,7 @@ import {
Button,
} from '@wordpress/components';
import { Icon, chevronRight, chevronLeft } from '@wordpress/icons';
+import { parse } from '@wordpress/blocks';
import { focus } from '@wordpress/dom';
/**
@@ -27,6 +28,7 @@ import usePatternsState from './hooks/use-patterns-state';
import BlockPatternList from '../block-patterns-list';
import PatternsExplorerModal from './block-patterns-explorer/explorer';
import MobileTabNavigation from './mobile-tab-navigation';
+import useBlockTypesState from './hooks/use-block-types-state';
const noop = () => {};
@@ -49,6 +51,18 @@ function usePatternsCategories( rootClientId ) {
rootClientId
);
+ const [ unsyncedPatterns ] = useBlockTypesState(
+ rootClientId,
+ undefined,
+ 'unsynced'
+ );
+
+ const filteredUnsyncedPatterns = useMemo( () => {
+ return unsyncedPatterns.filter(
+ ( { category: unsyncedPatternCategory } ) =>
+ unsyncedPatternCategory === 'reusable'
+ );
+ }, [ unsyncedPatterns ] );
const hasRegisteredCategory = useCallback(
( pattern ) => {
if ( ! pattern.categories || ! pattern.categories.length ) {
@@ -93,9 +107,20 @@ function usePatternsCategories( rootClientId ) {
label: _x( 'Uncategorized' ),
} );
}
+ if ( filteredUnsyncedPatterns.length > 0 ) {
+ categories.push( {
+ name: 'reusable',
+ label: _x( 'Custom patterns' ),
+ } );
+ }
return categories;
- }, [ allPatterns, allCategories ] );
+ }, [
+ allCategories,
+ allPatterns,
+ filteredUnsyncedPatterns.length,
+ hasRegisteredCategory,
+ ] );
return populatedCategories;
}
@@ -144,6 +169,24 @@ export function BlockPatternsCategoryPanel( {
onInsert,
rootClientId
);
+ const [ unsyncedPatterns ] = useBlockTypesState(
+ rootClientId,
+ onInsert,
+ 'unsynced'
+ );
+ const filteredUnsyncedPatterns = useMemo( () => {
+ return unsyncedPatterns
+ .filter(
+ ( { category: unsyncedPatternCategory } ) =>
+ unsyncedPatternCategory === 'reusable'
+ )
+ .map( ( syncedPattern ) => ( {
+ ...syncedPattern,
+ blocks: parse( syncedPattern.content, {
+ __unstableSkipMigrationLogs: true,
+ } ),
+ } ) );
+ }, [ unsyncedPatterns ] );
const availableCategories = usePatternsCategories( rootClientId );
const currentCategoryPatterns = useMemo(
@@ -167,13 +210,19 @@ export function BlockPatternsCategoryPanel( {
} ),
[ allPatterns, category ]
);
-
- const currentShownPatterns = useAsyncList( currentCategoryPatterns );
+ const patterns =
+ category.name === 'reusable'
+ ? filteredUnsyncedPatterns
+ : currentCategoryPatterns;
+ const currentShownPatterns = useAsyncList( patterns );
// Hide block pattern preview on unmount.
useEffect( () => () => onHover( null ), [] );
- if ( ! currentCategoryPatterns.length ) {
+ if (
+ ! currentCategoryPatterns.length &&
+ ! filteredUnsyncedPatterns.length
+ ) {
return null;
}
@@ -185,7 +234,7 @@ export function BlockPatternsCategoryPanel( {
{ category.description }
{
+const useBlockTypesState = ( rootClientId, onInsert, syncStatus ) => {
const { categories, collections, items } = useSelect(
( select ) => {
const { getInserterItems } = select( blockEditorStore );
@@ -30,10 +31,10 @@ const useBlockTypesState = ( rootClientId, onInsert ) => {
return {
categories: getCategories(),
collections: getCollections(),
- items: getInserterItems( rootClientId ),
+ items: getInserterItems( rootClientId, syncStatus ),
};
},
- [ rootClientId ]
+ [ rootClientId, syncStatus ]
);
const onSelectItem = useCallback(
diff --git a/packages/block-editor/src/components/inserter/reusable-blocks-tab.js b/packages/block-editor/src/components/inserter/reusable-blocks-tab.js
index 9505dd77f3b94..65930fa9fcd4a 100644
--- a/packages/block-editor/src/components/inserter/reusable-blocks-tab.js
+++ b/packages/block-editor/src/components/inserter/reusable-blocks-tab.js
@@ -29,12 +29,12 @@ function ReusableBlocksList( { onHover, onInsert, rootClientId } ) {
}
return (
-
+
);
@@ -67,7 +67,7 @@ export function ReusableBlocksTab( { rootClientId, onInsert, onHover } ) {
post_type: 'wp_block',
} ) }
>
- { __( 'Manage Reusable blocks' ) }
+ { __( 'Manage custom patterns' ) }
>
diff --git a/packages/block-editor/src/components/inserter/tabs.js b/packages/block-editor/src/components/inserter/tabs.js
index 6f8377892059b..1ff8b529707a4 100644
--- a/packages/block-editor/src/components/inserter/tabs.js
+++ b/packages/block-editor/src/components/inserter/tabs.js
@@ -13,13 +13,13 @@ const blocksTab = {
};
const patternsTab = {
name: 'patterns',
- /* translators: Patterns tab title in the block inserter. */
+ /* translators: Theme and Directory Patterns tab title in the block inserter. */
title: __( 'Patterns' ),
};
const reusableBlocksTab = {
name: 'reusable',
- /* translators: Reusable blocks tab title in the block inserter. */
- title: __( 'Reusable' ),
+ /* translators: Locally created Patterns tab title in the block inserter. */
+ title: __( 'Synced patterns' ),
icon: reusableBlockIcon,
};
const mediaTab = {
diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js
index 1616a2fde8b1b..6a62f50c2a2f7 100644
--- a/packages/block-editor/src/store/selectors.js
+++ b/packages/block-editor/src/store/selectors.js
@@ -1945,6 +1945,7 @@ const buildBlockTypeItem =
*
* @param {Object} state Editor state.
* @param {?string} rootClientId Optional root client ID of block list.
+ * @param {?string} syncStatus Optional sync status to filter pattern blocks by.
*
* @return {WPEditorInserterItem[]} Items that appear in inserter.
*
@@ -1961,7 +1962,7 @@ const buildBlockTypeItem =
* @property {number} frecency Heuristic that combines frequency and recency.
*/
export const getInserterItems = createSelector(
- ( state, rootClientId = null ) => {
+ ( state, rootClientId = null, syncStatus ) => {
const buildBlockTypeInserterItem = buildBlockTypeItem( state, {
buildScope: 'inserter',
} );
@@ -2026,6 +2027,7 @@ export const getInserterItems = createSelector(
isDisabled: false,
utility: 1, // Deprecated.
frecency,
+ content: reusableBlock.content.raw,
};
};
@@ -2040,7 +2042,14 @@ export const getInserterItems = createSelector(
'core/block',
rootClientId
)
- ? getReusableBlocks( state ).map( buildReusableBlockInserterItem )
+ ? getReusableBlocks( state )
+ .filter(
+ ( reusableBlock ) =>
+ syncStatus === reusableBlock.meta?.sync_status ||
+ ( ! syncStatus &&
+ reusableBlock.meta?.sync_status === '' )
+ )
+ .map( buildReusableBlockInserterItem )
: [];
const items = blockTypeInserterItems.reduce( ( accumulator, item ) => {
diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js
index dca9b847bc5a2..270bceaad447f 100644
--- a/packages/block-editor/src/store/test/selectors.js
+++ b/packages/block-editor/src/store/test/selectors.js
@@ -3326,36 +3326,36 @@ describe( 'selectors', () => {
( item ) => item.id === 'core/test-block-a'
);
expect( testBlockAItem ).toEqual( {
+ category: 'design',
+ description: undefined,
+ example: undefined,
+ frecency: 0,
+ icon: { src: 'test' },
id: 'core/test-block-a',
- name: 'core/test-block-a',
initialAttributes: {},
- title: 'Test Block A',
- icon: {
- src: 'test',
- },
- category: 'design',
- keywords: [ 'testing' ],
- variations: [],
isDisabled: false,
+ keywords: [ 'testing' ],
+ name: 'core/test-block-a',
+ title: 'Test Block A',
utility: 1,
- frecency: 0,
+ variations: [],
} );
const reusableBlockItem = items.find(
( item ) => item.id === 'core/block/1'
);
expect( reusableBlockItem ).toEqual( {
+ category: 'reusable',
+ content: '',
+ frecency: 0,
+ icon: { src: 'test' },
id: 'core/block/1',
- name: 'core/block',
initialAttributes: { ref: 1 },
- title: 'Reusable Block 1',
- icon: {
- src: 'test',
- },
- category: 'reusable',
- keywords: [],
isDisabled: false,
+ keywords: [],
+ name: 'core/block',
+ syncStatus: undefined,
+ title: 'Reusable Block 1',
utility: 1,
- frecency: 0,
} );
} );
diff --git a/packages/block-library/src/block/block.json b/packages/block-library/src/block/block.json
index 78fed058a5bd1..5846e7ead0c9b 100644
--- a/packages/block-library/src/block/block.json
+++ b/packages/block-library/src/block/block.json
@@ -2,7 +2,7 @@
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "core/block",
- "title": "Reusable block",
+ "title": "Pattern",
"category": "reusable",
"description": "Create and save content to reuse across your site. Update the block, and the changes apply everywhere it’s used.",
"textdomain": "default",
diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js
index fcba45450ea5e..cc1ec16aeedc2 100644
--- a/packages/block-library/src/block/edit.js
+++ b/packages/block-library/src/block/edit.js
@@ -59,6 +59,7 @@ export default function ReusableBlockEdit( { attributes: { ref }, clientId } ) {
'wp_block',
{ id: ref }
);
+
const [ title, setTitle ] = useEntityProp(
'postType',
'wp_block',
diff --git a/packages/block-library/src/block/test/__snapshots__/transforms.native.js.snap b/packages/block-library/src/block/test/__snapshots__/transforms.native.js.snap
index 7489c0b04954a..3c4d791eb9f75 100644
--- a/packages/block-library/src/block/test/__snapshots__/transforms.native.js.snap
+++ b/packages/block-library/src/block/test/__snapshots__/transforms.native.js.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Reusable block block transforms to Columns block 1`] = `
+exports[`Pattern block transforms to Columns block 1`] = `
"
@@ -8,7 +8,7 @@ exports[`Reusable block block transforms to Columns block 1`] = `
"
`;
-exports[`Reusable block block transforms to Group block 1`] = `
+exports[`Pattern block transforms to Group block 1`] = `
"
"
diff --git a/packages/block-library/src/block/test/edit.native.js b/packages/block-library/src/block/test/edit.native.js
index 4652f8ba20f38..ae9d3d7c03e3e 100644
--- a/packages/block-library/src/block/test/edit.native.js
+++ b/packages/block-library/src/block/test/edit.native.js
@@ -114,7 +114,7 @@ describe( 'Reusable block', () => {
// Get the reusable block.
const [ reusableBlock ] = await screen.findAllByLabelText(
- /Reusable block Block\. Row 1/
+ /Pattern Block\. Row 1/
);
expect( reusableBlock ).toBeDefined();
@@ -131,7 +131,7 @@ describe( 'Reusable block', () => {
} );
const [ reusableBlock ] = await screen.findAllByLabelText(
- /Reusable block Block\. Row 1/
+ /Pattern Block\. Row 1/
);
const blockDeleted = within( reusableBlock ).getByText(
@@ -164,7 +164,7 @@ describe( 'Reusable block', () => {
} );
const [ reusableBlock ] = await screen.findByLabelText(
- /Reusable block Block\. Row 1/
+ /Pattern Block\. Row 1/
);
const innerBlockListWrapper = await within(
diff --git a/packages/block-library/src/block/test/transforms.native.js b/packages/block-library/src/block/test/transforms.native.js
index 9771b40743a42..95104ac613399 100644
--- a/packages/block-library/src/block/test/transforms.native.js
+++ b/packages/block-library/src/block/test/transforms.native.js
@@ -9,7 +9,7 @@ import {
getBlockTransformOptions,
} from 'test/helpers';
-const block = 'Reusable block';
+const block = 'Pattern';
const initialHtml = `
`;
diff --git a/packages/block-library/src/pattern/block.json b/packages/block-library/src/pattern/block.json
index 16428e2969ca2..1fc319b8fb33d 100644
--- a/packages/block-library/src/pattern/block.json
+++ b/packages/block-library/src/pattern/block.json
@@ -2,7 +2,7 @@
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "core/pattern",
- "title": "Pattern",
+ "title": "Pattern placeholder",
"category": "theme",
"description": "Show a block pattern.",
"supports": {
diff --git a/packages/e2e-test-utils/src/create-reusable-block.js b/packages/e2e-test-utils/src/create-reusable-block.js
index ec35e07390847..266e0525d34bd 100644
--- a/packages/e2e-test-utils/src/create-reusable-block.js
+++ b/packages/e2e-test-utils/src/create-reusable-block.js
@@ -15,22 +15,30 @@ import { canvas } from './canvas';
export const createReusableBlock = async ( content, title ) => {
const reusableBlockNameInputSelector =
'.reusable-blocks-menu-items__convert-modal .components-text-control__input';
+ const syncToggleSelector =
+ '.reusable-blocks-menu-items__convert-modal .components-form-toggle__input';
+ const syncToggleSelectorChecked =
+ '.reusable-blocks-menu-items__convert-modal .components-form-toggle.is-checked';
// Insert a paragraph block
await insertBlock( 'Paragraph' );
await page.keyboard.type( content );
await clickBlockToolbarButton( 'Options' );
- await clickMenuItem( 'Create Reusable block' );
+ await clickMenuItem( 'Create a Pattern' );
const nameInput = await page.waitForSelector(
reusableBlockNameInputSelector
);
await nameInput.click();
await page.keyboard.type( title );
+
+ const syncToggle = await page.waitForSelector( syncToggleSelector );
+ syncToggle.click();
+ await page.waitForSelector( syncToggleSelectorChecked );
await page.keyboard.press( 'Enter' );
// Wait for creation to finish
await page.waitForXPath(
- '//*[contains(@class, "components-snackbar")]/*[text()="Reusable block created."]'
+ '//*[contains(@class, "components-snackbar")]/*[text()="Synced Pattern created."]'
);
// Check that we have a reusable block on the page
diff --git a/packages/e2e-test-utils/src/inserter.js b/packages/e2e-test-utils/src/inserter.js
index 58f6d01a66514..cdd1c534d928d 100644
--- a/packages/e2e-test-utils/src/inserter.js
+++ b/packages/e2e-test-utils/src/inserter.js
@@ -106,8 +106,8 @@ export async function selectGlobalInserterTab( label ) {
case 'Media':
labelSelector = `. = "${ label }"`;
break;
- case 'Reusable':
- // Reusable tab label is an icon, hence the different selector.
+ case 'Synced patterns':
+ // Synced patterns tab label is an icon, hence the different selector.
labelSelector = `@aria-label = "${ label }"`;
break;
}
@@ -180,7 +180,7 @@ export async function searchGlobalInserter( category, searchTerm ) {
switch ( category ) {
case 'Blocks':
case 'Patterns':
- case 'Reusable': {
+ case 'Synced patterns': {
waitForInsertElement = async () => {
return await page.waitForXPath(
`//*[@role='option' and contains(., '${ searchTerm }')]`
@@ -220,7 +220,7 @@ export async function searchGlobalInserter( category, searchTerm ) {
* If the entity is not instantly available in the open inserter, a search will
* be performed. If the search returns no results, an error will be thrown.
*
- * Available categories: Blocks, Patterns, Reusable and Block Directory.
+ * Available categories: Blocks, Patterns, Synced patterns and Block Directory.
*
* @param {string} category The category to insert from.
* @param {string} searchTerm The term by which to find the entity to insert.
@@ -231,8 +231,8 @@ export async function insertFromGlobalInserter( category, searchTerm ) {
let insertButton;
- if ( [ 'Blocks', 'Reusable' ].includes( category ) ) {
- // If it's a block, see it it's insertable without searching...
+ if ( [ 'Blocks', 'Synced patterns' ].includes( category ) ) {
+ // If it's a block, see if it's insertable without searching...
try {
insertButton = (
await page.$x(
@@ -260,7 +260,7 @@ export async function insertFromGlobalInserter( category, searchTerm ) {
await insertButton.click();
// Extra wait for the reusable block to be ready.
- if ( category === 'Reusable' ) {
+ if ( category === 'Synced patterns' ) {
await canvas().waitForSelector(
'.block-library-block__reusable-block-container'
);
@@ -347,7 +347,7 @@ export async function insertPattern( searchTerm ) {
* insert.
*/
export async function insertReusableBlock( searchTerm ) {
- await insertFromGlobalInserter( 'Reusable', searchTerm );
+ await insertFromGlobalInserter( 'Synced patterns', searchTerm );
}
/**
diff --git a/packages/e2e-tests/specs/editor/various/block-editor-keyboard-shortcuts.test.js b/packages/e2e-tests/specs/editor/various/block-editor-keyboard-shortcuts.test.js
index f1e6be7b816ab..97248c472e4ac 100644
--- a/packages/e2e-tests/specs/editor/various/block-editor-keyboard-shortcuts.test.js
+++ b/packages/e2e-tests/specs/editor/various/block-editor-keyboard-shortcuts.test.js
@@ -90,7 +90,7 @@ describe( 'block editor keyboard shortcuts', () => {
} );
it( 'should prevent deleting multiple selected blocks from inputs', async () => {
await clickBlockToolbarButton( 'Options' );
- await clickMenuItem( 'Create Reusable block' );
+ await clickMenuItem( 'Create a Pattern' );
const reusableBlockNameInputSelector =
'.reusable-blocks-menu-items__convert-modal .components-text-control__input';
const nameInput = await page.waitForSelector(
diff --git a/packages/e2e-tests/specs/editor/various/reusable-blocks.test.js b/packages/e2e-tests/specs/editor/various/reusable-blocks.test.js
index 3215e4185c08f..416782733dfaa 100644
--- a/packages/e2e-tests/specs/editor/various/reusable-blocks.test.js
+++ b/packages/e2e-tests/specs/editor/various/reusable-blocks.test.js
@@ -23,6 +23,10 @@ const reusableBlockNameInputSelector =
'.reusable-blocks-menu-items__convert-modal .components-text-control__input';
const reusableBlockInspectorNameInputSelector =
'.block-editor-block-inspector .components-text-control__input';
+const syncToggleSelector =
+ '.reusable-blocks-menu-items__convert-modal .components-form-toggle__input';
+const syncToggleSelectorChecked =
+ '.reusable-blocks-menu-items__convert-modal .components-form-toggle.is-checked';
const saveAll = async () => {
const publishButtonSelector =
@@ -193,7 +197,7 @@ describe( 'Reusable blocks', () => {
// Convert block to a reusable block.
await clickBlockToolbarButton( 'Options' );
- await clickMenuItem( 'Create Reusable block' );
+ await clickMenuItem( 'Create a Pattern' );
// Set title.
const nameInput = await page.waitForSelector(
@@ -201,11 +205,14 @@ describe( 'Reusable blocks', () => {
);
await nameInput.click();
await page.keyboard.type( 'Multi-selection reusable block' );
+ const syncToggle = await page.waitForSelector( syncToggleSelector );
+ syncToggle.click();
+ await page.waitForSelector( syncToggleSelectorChecked );
await page.keyboard.press( 'Enter' );
// Wait for creation to finish.
await page.waitForXPath(
- '//*[contains(@class, "components-snackbar")]/*[text()="Reusable block created."]'
+ '//*[contains(@class, "components-snackbar")]/*[text()="Synced Pattern created."]'
);
await clearAllBlocks();
@@ -259,7 +266,7 @@ describe( 'Reusable blocks', () => {
// Save the reusable block.
await page.click( publishButtonSelector );
await page.waitForXPath(
- '//*[contains(@class, "components-snackbar")]/*[text()="Reusable block updated."]'
+ '//*[contains(@class, "components-snackbar")]/*[text()="Pattern updated."]'
);
await createNewPost();
@@ -340,12 +347,12 @@ describe( 'Reusable blocks', () => {
await canvas().click( 'p[aria-label="Paragraph block"]' );
await page.keyboard.type( '2' );
const selector =
- '//div[@aria-label="Block: Reusable block"]//p[@aria-label="Paragraph block"][.="12"]';
+ '//div[@aria-label="Block: Pattern"]//p[@aria-label="Paragraph block"][.="12"]';
const reusableBlockWithParagraph = await page.$x( selector );
expect( reusableBlockWithParagraph ).toBeTruthy();
// Convert back to regular blocks.
- await clickBlockToolbarButton( 'Select Reusable block' );
+ await clickBlockToolbarButton( 'Select Pattern' );
await clickBlockToolbarButton( 'Convert to regular block' );
await page.waitForXPath( selector, {
hidden: true,
@@ -376,15 +383,18 @@ describe( 'Reusable blocks', () => {
// Convert to reusable.
await clickBlockToolbarButton( 'Options' );
- await clickMenuItem( 'Create Reusable block' );
+ await clickMenuItem( 'Create a Pattern' );
const nameInput = await page.waitForSelector(
reusableBlockNameInputSelector
);
await nameInput.click();
await page.keyboard.type( 'Block with styles' );
+ const syncToggle = await page.waitForSelector( syncToggleSelector );
+ syncToggle.click();
+ await page.waitForSelector( syncToggleSelectorChecked );
await page.keyboard.press( 'Enter' );
const reusableBlock = await canvas().waitForSelector(
- '.block-editor-block-list__block[aria-label="Block: Reusable block"]'
+ '.block-editor-block-list__block[aria-label="Block: Pattern"]'
);
expect( reusableBlock ).toBeTruthy();
} );
diff --git a/packages/edit-post/src/components/sidebar/post-status/index.js b/packages/edit-post/src/components/sidebar/post-status/index.js
index 8304bb8b4f6ea..5cc9b70bf9ac3 100644
--- a/packages/edit-post/src/components/sidebar/post-status/index.js
+++ b/packages/edit-post/src/components/sidebar/post-status/index.js
@@ -8,7 +8,7 @@ import {
} from '@wordpress/components';
import { withSelect, withDispatch } from '@wordpress/data';
import { compose, ifCondition } from '@wordpress/compose';
-import { PostSwitchToDraftButton } from '@wordpress/editor';
+import { PostSwitchToDraftButton, PostSyncStatus } from '@wordpress/editor';
/**
* Internal dependencies
@@ -51,6 +51,7 @@ function PostStatus( { isOpened, onTogglePanel } ) {
+
{ fills }
- { __( 'Manage Reusable blocks' ) }
+ { __( 'Manage Patterns' ) }
{
+ const { getEditedPostAttribute } = select( editorStore );
+ return {
+ meta: getEditedPostAttribute( 'meta' ),
+ postType: getEditedPostAttribute( 'type' ),
+ };
+ }, [] );
+ if ( postType !== 'wp_block' ) {
+ return null;
+ }
+ const onUpdateSync = ( syncStatus ) =>
+ editPost( {
+ meta: {
+ ...meta,
+ wp_block:
+ syncStatus === 'unsynced'
+ ? { sync_status: syncStatus }
+ : null,
+ },
+ } );
+ const syncStatus = meta?.wp_block?.sync_status;
+ const isFullySynced = ! syncStatus;
+
+ return (
+
+ { __( 'Syncing' ) }
+ {
+ onUpdateSync(
+ syncStatus === 'unsynced' ? 'fully' : 'unsynced'
+ );
+ } }
+ />
+
+ );
+}
diff --git a/packages/editor/src/components/post-sync-status/style.scss b/packages/editor/src/components/post-sync-status/style.scss
new file mode 100644
index 0000000000000..385577b3334d8
--- /dev/null
+++ b/packages/editor/src/components/post-sync-status/style.scss
@@ -0,0 +1,16 @@
+.edit-post-sync-status {
+ width: 100%;
+ position: relative;
+ justify-content: flex-start;
+
+ > span {
+ display: block;
+ width: 45%;
+ flex-shrink: 0;
+ }
+
+ .components-base-control {
+ // Match padding on tertiary buttons for alignment.
+ padding-left: $grid-unit-15 * 0.5;
+ }
+}
diff --git a/packages/editor/src/style.scss b/packages/editor/src/style.scss
index 9d035ec4d654a..dbffbbef4d521 100644
--- a/packages/editor/src/style.scss
+++ b/packages/editor/src/style.scss
@@ -11,6 +11,7 @@
@import "./components/post-publish-button/style.scss";
@import "./components/post-publish-panel/style.scss";
@import "./components/post-saved-state/style.scss";
+@import "./components/post-sync-status/style.scss";
@import "./components/post-taxonomies/style.scss";
@import "./components/post-text-editor/style.scss";
@import "./components/post-url/style.scss";
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 18824b892544a..c8203fdd73c40 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
@@ -14,6 +14,7 @@ import {
TextControl,
__experimentalHStack as HStack,
__experimentalVStack as VStack,
+ ToggleControl,
} from '@wordpress/components';
import { symbol } from '@wordpress/icons';
import { useDispatch, useSelect } from '@wordpress/data';
@@ -38,6 +39,7 @@ export default function ReusableBlockConvertButton( {
clientIds,
rootClientId,
} ) {
+ const [ syncType, setSyncType ] = useState( 'unsynced' );
const [ isModalOpen, setIsModalOpen ] = useState( false );
const [ title, setTitle ] = useState( '' );
const canConvert = useSelect(
@@ -77,7 +79,7 @@ export default function ReusableBlockConvertButton( {
return _canConvert;
},
- [ clientIds ]
+ [ clientIds, rootClientId ]
);
const { __experimentalConvertBlocksToReusable: convertBlocksToReusable } =
@@ -88,17 +90,32 @@ export default function ReusableBlockConvertButton( {
const onConvert = useCallback(
async function ( reusableBlockTitle ) {
try {
- await convertBlocksToReusable( clientIds, reusableBlockTitle );
- createSuccessNotice( __( 'Reusable block created.' ), {
- type: 'snackbar',
- } );
+ await convertBlocksToReusable(
+ clientIds,
+ reusableBlockTitle,
+ syncType
+ );
+ createSuccessNotice(
+ syncType === 'fully'
+ ? __( 'Synced Pattern created.' )
+ : __( 'Unsynced Pattern created.' ),
+ {
+ type: 'snackbar',
+ }
+ );
} catch ( error ) {
createErrorNotice( error.message, {
type: 'snackbar',
} );
}
},
- [ clientIds ]
+ [
+ convertBlocksToReusable,
+ clientIds,
+ syncType,
+ createSuccessNotice,
+ createErrorNotice,
+ ]
);
if ( ! canConvert ) {
@@ -111,15 +128,13 @@ export default function ReusableBlockConvertButton( {
<>
{ isModalOpen && (
{
setIsModalOpen( false );
setTitle( '' );
@@ -142,6 +157,23 @@ export default function ReusableBlockConvertButton( {
value={ title }
onChange={ setTitle }
/>
+
+ {
+ setSyncType(
+ syncType === 'fully'
+ ? 'unsynced'
+ : 'fully'
+ );
+ } }
+ />