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( { <> { - setIsModalOpen( true ); - } } + onClick={ () => setIsModalOpen( true ) } > - { __( 'Create Reusable block' ) } + { __( 'Create a Pattern' ) } { isModalOpen && ( { setIsModalOpen( false ); setTitle( '' ); @@ -142,6 +157,23 @@ export default function ReusableBlockConvertButton( { value={ title } onChange={ setTitle } /> + + { + setSyncType( + syncType === 'fully' + ? 'unsynced' + : 'fully' + ); + } } + />