From dc91763473b69f73f98868d428bd3863406a4ca5 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Mon, 21 Aug 2023 16:38:06 +1000 Subject: [PATCH 1/9] Try using the block connections api --- lib/block-supports/pattern.php | 36 ++++ lib/experimental/blocks.php | 10 +- lib/experimental/connection-sources/index.php | 7 +- lib/load.php | 1 + .../block-editor/src/hooks/custom-fields.js | 90 ++++++---- .../block-editor/src/store/private-actions.js | 8 + packages/block-editor/src/store/reducer.js | 8 + packages/block-library/src/block/edit.js | 163 ++++++++++++++++-- packages/block-library/src/block/index.js | 5 +- packages/block-library/src/block/index.php | 43 +++++ packages/block-library/src/block/v1/edit.js | 163 ++++++++++++++++++ 11 files changed, 479 insertions(+), 55 deletions(-) create mode 100644 lib/block-supports/pattern.php create mode 100644 packages/block-library/src/block/v1/edit.js diff --git a/lib/block-supports/pattern.php b/lib/block-supports/pattern.php new file mode 100644 index 00000000000000..66bc1e699efda9 --- /dev/null +++ b/lib/block-supports/pattern.php @@ -0,0 +1,36 @@ +supports, array( '__experimentalConnections' ), false ) : false; + + if ( $pattern_support ) { + if ( ! $block_type->uses_context ) { + $block_type->uses_context = array(); + } + + if ( ! in_array( 'dynamicContent', $block_type->uses_context, true ) ) { + $block_type->uses_context[] = 'dynamicContent'; + } + } + } + + // Register the block support. + WP_Block_Supports::get_instance()->register( + 'pattern', + array( + 'register_attribute' => 'gutenberg_register_pattern_support', + ) + ); +} diff --git a/lib/experimental/blocks.php b/lib/experimental/blocks.php index f9f2412ae51205..73c23999cc3c46 100644 --- a/lib/experimental/blocks.php +++ b/lib/experimental/blocks.php @@ -132,9 +132,8 @@ function gutenberg_render_block_connections( $block_content, $block, $block_inst continue; } - // If the source value is not "meta_fields", skip it because the only supported - // connection source is meta (custom fields) for now. - if ( 'meta_fields' !== $attribute_value['source'] ) { + // Skip if the source value is not "meta_fields" or "pattern_attributes". + if ( 'meta_fields' !== $attribute_value['source'] && 'pattern_attributes' !== $attribute_value['source'] ) { continue; } @@ -154,6 +153,10 @@ function gutenberg_render_block_connections( $block_content, $block, $block_inst $attribute_value['value'] ); + if ( false === $custom_value ) { + continue; + } + $tags = new WP_HTML_Tag_Processor( $block_content ); $found = $tags->next_tag( array( @@ -181,5 +184,6 @@ function gutenberg_render_block_connections( $block_content, $block, $block_inst return $block_content; } + add_filter( 'render_block', 'gutenberg_render_block_connections', 10, 3 ); } diff --git a/lib/experimental/connection-sources/index.php b/lib/experimental/connection-sources/index.php index b63abcad96f628..e7827435407199 100644 --- a/lib/experimental/connection-sources/index.php +++ b/lib/experimental/connection-sources/index.php @@ -6,10 +6,13 @@ */ return array( - 'name' => 'meta', - 'meta_fields' => function ( $block_instance, $meta_field ) { + 'name' => 'meta', + 'meta_fields' => function ( $block_instance, $meta_field ) { // We should probably also check if the meta field exists but for now it's okay because // if it doesn't, `get_post_meta()` will just return an empty string. return get_post_meta( $block_instance->context['postId'], $meta_field, true ); }, + 'pattern_attributes' => function ( $block_instance, $meta_field ) { + return _wp_array_get( $block_instance->context, array( 'dynamicContent', $meta_field ), false ); + }, ); diff --git a/lib/load.php b/lib/load.php index 38111d9ed5d3d5..e940ab94e055f5 100644 --- a/lib/load.php +++ b/lib/load.php @@ -270,6 +270,7 @@ function () { require __DIR__ . '/block-supports/shadow.php'; require __DIR__ . '/block-supports/background.php'; require __DIR__ . '/block-supports/behaviors.php'; +require __DIR__ . '/block-supports/pattern.php'; // Data views. require_once __DIR__ . '/experimental/data-views.php'; diff --git a/packages/block-editor/src/hooks/custom-fields.js b/packages/block-editor/src/hooks/custom-fields.js index adb9df15824a77..60c924f40c9408 100644 --- a/packages/block-editor/src/hooks/custom-fields.js +++ b/packages/block-editor/src/hooks/custom-fields.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { addFilter } from '@wordpress/hooks'; -import { PanelBody, TextControl } from '@wordpress/components'; +import { PanelBody, TextControl, SelectControl } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; import { hasBlockSupport } from '@wordpress/blocks'; import { createHigherOrderComponent } from '@wordpress/compose'; @@ -47,45 +47,71 @@ function CustomFieldsControl( props ) { if ( props.name === 'core/paragraph' ) attributeName = 'content'; if ( props.name === 'core/image' ) attributeName = 'url'; + const connectionSource = + props.attributes?.connections?.attributes?.[ attributeName ]?.source || + ''; + const connectionValue = + props.attributes?.connections?.attributes?.[ attributeName ]?.value || + ''; + + function updateConnections( source, value ) { + if ( value === '' ) { + props.setAttributes( { + connections: undefined, + placeholder: undefined, + } ); + } else { + props.setAttributes( { + connections: { + attributes: { + // The attributeName will be either `content` or `url`. + [ attributeName ]: { + // Source will be variable, could be post_meta, user_meta, term_meta, etc. + // Could even be a custom source like a social media attribute. + source, + value, + }, + }, + }, + placeholder: sprintf( + 'This content will be replaced on the frontend by the value of "%s" custom field.', + value + ), + } ); + } + } + return ( + { + updateConnections( nextSource, connectionValue ); + } } + /> { - if ( nextValue === '' ) { - props.setAttributes( { - connections: undefined, - [ attributeName ]: undefined, - placeholder: undefined, - } ); - } else { - props.setAttributes( { - connections: { - attributes: { - // The attributeName will be either `content` or `url`. - [ attributeName ]: { - // Source will be variable, could be post_meta, user_meta, term_meta, etc. - // Could even be a custom source like a social media attribute. - source: 'meta_fields', - value: nextValue, - }, - }, - }, - [ attributeName ]: undefined, - placeholder: sprintf( - 'This content will be replaced on the frontend by the value of "%s" custom field.', - nextValue - ), - } ); - } + updateConnections( connectionSource, nextValue ); } } /> diff --git a/packages/block-editor/src/store/private-actions.js b/packages/block-editor/src/store/private-actions.js index dc62614b576812..1c29948d814165 100644 --- a/packages/block-editor/src/store/private-actions.js +++ b/packages/block-editor/src/store/private-actions.js @@ -290,3 +290,11 @@ export function deleteStyleOverride( id ) { id, }; } + +export function syncDerivedBlockAttributes( clientId, attributes ) { + return { + type: 'SYNC_DERIVED_BLOCK_ATTRIBUTES', + clientIds: [ clientId ], + attributes, + }; +} diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index d5ff85e9e4257b..5319a3b2553654 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -283,6 +283,7 @@ const withBlockTree = false ); break; + case 'SYNC_DERIVED_BLOCK_ATTRIBUTES': case 'UPDATE_BLOCK_ATTRIBUTES': { newState.tree = new Map( newState.tree ); action.clientIds.forEach( ( clientId ) => { @@ -456,6 +457,12 @@ function withPersistentBlockChange( reducer ) { return ( state, action ) => { let nextState = reducer( state, action ); + if ( action.type === 'SYNC_DERIVED_BLOCK_ATTRIBUTES' ) { + return nextState.isPersistentChange + ? { ...nextState, isPersistentChange: false } + : nextState; + } + const isExplicitPersistentChange = action.type === 'MARK_LAST_CHANGE_AS_PERSISTENT' || markNextChangeAsNotPersistent; @@ -860,6 +867,7 @@ export const blocks = pipe( return newState; } + case 'SYNC_DERIVED_BLOCK_ATTRIBUTES': case 'UPDATE_BLOCK_ATTRIBUTES': { // Avoid a state change if none of the block IDs are known. if ( action.clientIds.every( ( id ) => ! state.get( id ) ) ) { diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js index 979ae04c62282c..ef0e3b40e5a72b 100644 --- a/packages/block-library/src/block/edit.js +++ b/packages/block-library/src/block/edit.js @@ -6,11 +6,9 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { - useEntityBlockEditor, - useEntityProp, - useEntityRecord, -} from '@wordpress/core-data'; +import { useRegistry, useSelect, useDispatch } from '@wordpress/data'; +import { useRef, useMemo, useEffect } from '@wordpress/element'; +import { useEntityProp, useEntityRecord } from '@wordpress/core-data'; import { Placeholder, Spinner, @@ -27,8 +25,9 @@ import { useBlockProps, Warning, privateApis as blockEditorPrivateApis, + store as blockEditorStore, } from '@wordpress/block-editor'; -import { useRef, useMemo } from '@wordpress/element'; +import { getBlockSupport, parse } from '@wordpress/blocks'; /** * Internal dependencies @@ -36,6 +35,27 @@ import { useRef, useMemo } from '@wordpress/element'; import { unlock } from '../lock-unlock'; const { useLayoutClasses } = unlock( blockEditorPrivateApis ); + +function isPartiallySynced( block ) { + return ( + !! getBlockSupport( block.name, '__experimentalConnections', false ) && + !! block.attributes.connections?.attributes && + Object.values( block.attributes.connections.attributes ).some( + ( connection ) => connection.source === 'pattern_attributes' + ) + ); +} +function getPartiallySyncedAttributes( block ) { + const attributes = {}; + for ( const [ attribute, connection ] of Object.entries( + block.attributes.connections.attributes + ) ) { + if ( connection.source !== 'pattern_attributes' ) continue; + attributes[ attribute ] = connection.value; + } + return attributes; +} + const fullAlignments = [ 'full', 'wide', 'left', 'right' ]; const useInferredLayout = ( blocks, parentLayout ) => { @@ -67,11 +87,62 @@ const useInferredLayout = ( blocks, parentLayout ) => { }, [ blocks, parentLayout ] ); }; +function applyInitialDynamicContent( + blocks, + dynamicContent = {}, + defaultValues +) { + return blocks.map( ( block ) => { + const innerBlocks = applyInitialDynamicContent( + block.innerBlocks, + dynamicContent, + defaultValues + ); + if ( ! isPartiallySynced( block ) ) return { ...block, innerBlocks }; + const attributes = getPartiallySyncedAttributes( block ); + const newAttributes = { ...block.attributes }; + for ( const [ attributeKey, id ] of Object.entries( attributes ) ) { + defaultValues[ id ] = block.attributes[ attributeKey ]; + if ( dynamicContent[ id ] ) { + newAttributes[ attributeKey ] = dynamicContent[ id ]; + } + } + return { + ...block, + attributes: newAttributes, + innerBlocks, + }; + } ); +} + +function getDynamicContentFromBlocks( blocks, defaultValues ) { + /** @type {Record} */ + const dynamicContent = {}; + for ( const block of blocks ) { + Object.assign( + dynamicContent, + getDynamicContentFromBlocks( block.innerBlocks, defaultValues ) + ); + if ( ! isPartiallySynced( block ) ) continue; + const attributes = getPartiallySyncedAttributes( block ); + for ( const [ attributeKey, id ] of Object.entries( attributes ) ) { + if ( block.attributes[ attributeKey ] !== defaultValues[ id ] ) { + dynamicContent[ id ] = block.attributes[ attributeKey ]; + } + } + } + return Object.keys( dynamicContent ).length > 0 + ? dynamicContent + : undefined; +} + export default function ReusableBlockEdit( { name, - attributes: { ref }, + attributes: { ref, dynamicContent }, __unstableParentLayout: parentLayout, + clientId: patternClientId, } ) { + const registry = useRegistry(); const hasAlreadyRendered = useHasRecursion( ref ); const { record, hasResolved } = useEntityRecord( 'postType', @@ -79,11 +150,46 @@ export default function ReusableBlockEdit( { ref ); const isMissing = hasResolved && ! record; + const initialDynamicContent = useRef( dynamicContent ); + const defaultValuesRef = useRef( {} ); + const { + replaceInnerBlocks, + __unstableMarkNextChangeAsNotPersistent, + setBlockEditingMode, + } = useDispatch( blockEditorStore ); + const { getBlockEditingMode } = useSelect( blockEditorStore ); - const [ blocks, onInput, onChange ] = useEntityBlockEditor( - 'postType', - 'wp_block', - { id: ref } + useEffect( () => { + if ( ! record?.content?.raw ) return; + const initialBlocks = parse( record.content.raw ); + + const editingMode = getBlockEditingMode( patternClientId ); + registry.batch( () => { + setBlockEditingMode( patternClientId, 'default' ); + __unstableMarkNextChangeAsNotPersistent(); + replaceInnerBlocks( + patternClientId, + applyInitialDynamicContent( + initialBlocks, + initialDynamicContent.current, + defaultValuesRef.current + ) + ); + setBlockEditingMode( patternClientId, editingMode ); + } ); + }, [ + __unstableMarkNextChangeAsNotPersistent, + patternClientId, + record, + replaceInnerBlocks, + registry, + getBlockEditingMode, + setBlockEditingMode, + ] ); + + const innerBlocks = useSelect( + ( select ) => select( blockEditorStore ).getBlocks( patternClientId ), + [ patternClientId ] ); const [ title, setTitle ] = useEntityProp( @@ -93,7 +199,10 @@ export default function ReusableBlockEdit( { ref ); - const { alignment, layout } = useInferredLayout( blocks, parentLayout ); + const { alignment, layout } = useInferredLayout( + innerBlocks, + parentLayout + ); const layoutClasses = useLayoutClasses( { layout }, name ); const blockProps = useBlockProps( { @@ -105,16 +214,38 @@ export default function ReusableBlockEdit( { } ); const innerBlocksProps = useInnerBlocksProps( blockProps, { - value: blocks, layout, - onInput, - onChange, - renderAppender: blocks?.length + renderAppender: innerBlocks?.length ? undefined : InnerBlocks.ButtonBlockAppender, } ); + // Sync the `dynamicContent` attribute from the updated blocks. + // `syncDerivedBlockAttributes` is an action that just like `updateBlockAttributes` + // but won't create an undo level. + // This can be abstracted into a `useSyncDerivedAttributes` hook if needed. + useEffect( () => { + const { getBlocks } = registry.select( blockEditorStore ); + const { syncDerivedBlockAttributes } = unlock( + registry.dispatch( blockEditorStore ) + ); + let prevBlocks = getBlocks( patternClientId ); + return registry.subscribe( () => { + const blocks = getBlocks( patternClientId ); + if ( blocks !== prevBlocks ) { + prevBlocks = blocks; + syncDerivedBlockAttributes( patternClientId, { + dynamicContent: getDynamicContentFromBlocks( + blocks, + defaultValuesRef.current + ), + } ); + } + }, blockEditorStore ); + }, [ patternClientId, registry ] ); + let children = null; + if ( hasAlreadyRendered ) { children = ( diff --git a/packages/block-library/src/block/index.js b/packages/block-library/src/block/index.js index 95e090f0afd6ad..40a54b14772d70 100644 --- a/packages/block-library/src/block/index.js +++ b/packages/block-library/src/block/index.js @@ -8,14 +8,15 @@ import { symbol as icon } from '@wordpress/icons'; */ import initBlock from '../utils/init-block'; import metadata from './block.json'; -import edit from './edit'; +import editV1 from './v1/edit'; +import editV2 from './edit'; const { name } = metadata; export { metadata, name }; export const settings = { - edit, + edit: window.__experimentalConnections ? editV2 : editV1, icon, }; diff --git a/packages/block-library/src/block/index.php b/packages/block-library/src/block/index.php index d51b35d68b23d9..89c62d17eaeb6c 100644 --- a/packages/block-library/src/block/index.php +++ b/packages/block-library/src/block/index.php @@ -46,8 +46,26 @@ function render_block_core_block( $attributes ) { $content = $wp_embed->run_shortcode( $reusable_block->post_content ); $content = $wp_embed->autoembed( $content ); + /** + * We set the `dynamicContent` context through the `render_block_context` + * filter so that it is available when a pattern's inner blocks are + * rendering via do_blocks given it only receives the inner content. + */ + if ( isset( $attributes['dynamicContent'] ) ) { + $filter_block_context = static function ( $context ) use ( $attributes ) { + $context['dynamicContent'] = $attributes['dynamicContent']; + return $context; + }; + add_filter( 'render_block_context', $filter_block_context, 1 ); + } + $content = do_blocks( $content ); unset( $seen_refs[ $attributes['ref'] ] ); + + if ( isset( $attributes['dynamicContent'] ) ) { + remove_filter( 'render_block_context', $filter_block_context, 1 ); + } + return $content; } @@ -63,3 +81,28 @@ function register_block_core_block() { ); } add_action( 'init', 'register_block_core_block' ); + +$gutenberg_experiments = get_option( 'gutenberg-experiments' ); +if ( $gutenberg_experiments && array_key_exists( 'gutenberg-connections', $gutenberg_experiments ) ) { + /** + * Registers the dynamicContent attribute for core/block. + * + * @param array $args Array of arguments for registering a block type. + * @param string $block_name Block name including namespace. + * @return array $args + */ + function register_block_core_block_args( $args, $block_name ) { + if ( 'core/block' === $block_name ) { + $args['attributes'] = array_merge( + $args['attributes'], + array( + 'dynamicContent' => array( + 'type' => 'object', + ), + ) + ); + } + return $args; + } + add_filter( 'register_block_type_args', 'register_block_core_block_args', 10, 2 ); +} diff --git a/packages/block-library/src/block/v1/edit.js b/packages/block-library/src/block/v1/edit.js new file mode 100644 index 00000000000000..5975711376c650 --- /dev/null +++ b/packages/block-library/src/block/v1/edit.js @@ -0,0 +1,163 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { + useEntityBlockEditor, + useEntityProp, + useEntityRecord, +} from '@wordpress/core-data'; +import { + Placeholder, + Spinner, + TextControl, + PanelBody, +} from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { + useInnerBlocksProps, + __experimentalRecursionProvider as RecursionProvider, + __experimentalUseHasRecursion as useHasRecursion, + InnerBlocks, + InspectorControls, + useBlockProps, + Warning, + privateApis as blockEditorPrivateApis, +} from '@wordpress/block-editor'; +import { useRef, useMemo } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { unlock } from '../../lock-unlock'; + +const { useLayoutClasses } = unlock( blockEditorPrivateApis ); +const fullAlignments = [ 'full', 'wide', 'left', 'right' ]; + +const useInferredLayout = ( blocks, parentLayout ) => { + const initialInferredAlignmentRef = useRef(); + + return useMemo( () => { + // Exit early if the pattern's blocks haven't loaded yet. + if ( ! blocks?.length ) { + return {}; + } + + let alignment = initialInferredAlignmentRef.current; + + // Only track the initial alignment so that temporarily removed + // alignments can be reapplied. + if ( alignment === undefined ) { + const isConstrained = parentLayout?.type === 'constrained'; + const hasFullAlignment = blocks.some( ( block ) => + fullAlignments.includes( block.attributes.align ) + ); + + alignment = isConstrained && hasFullAlignment ? 'full' : null; + initialInferredAlignmentRef.current = alignment; + } + + const layout = alignment ? parentLayout : undefined; + + return { alignment, layout }; + }, [ blocks, parentLayout ] ); +}; + +export default function ReusableBlockEdit( { + name, + attributes: { ref }, + __unstableParentLayout: parentLayout, +} ) { + const hasAlreadyRendered = useHasRecursion( ref ); + const { record, hasResolved } = useEntityRecord( + 'postType', + 'wp_block', + ref + ); + const isMissing = hasResolved && ! record; + + const [ blocks, onInput, onChange ] = useEntityBlockEditor( + 'postType', + 'wp_block', + { id: ref } + ); + + const [ title, setTitle ] = useEntityProp( + 'postType', + 'wp_block', + 'title', + ref + ); + + const { alignment, layout } = useInferredLayout( blocks, parentLayout ); + const layoutClasses = useLayoutClasses( { layout }, name ); + + const blockProps = useBlockProps( { + className: classnames( + 'block-library-block__reusable-block-container', + layout && layoutClasses, + { [ `align${ alignment }` ]: alignment } + ), + } ); + + const innerBlocksProps = useInnerBlocksProps( blockProps, { + value: blocks, + layout, + onInput, + onChange, + renderAppender: blocks?.length + ? undefined + : InnerBlocks.ButtonBlockAppender, + } ); + + let children = null; + + if ( hasAlreadyRendered ) { + children = ( + + { __( 'Block cannot be rendered inside itself.' ) } + + ); + } + + if ( isMissing ) { + children = ( + + { __( 'Block has been deleted or is unavailable.' ) } + + ); + } + + if ( ! hasResolved ) { + children = ( + + + + ); + } + + return ( + + + + + + + { children === null ? ( +
+ ) : ( +
{ children }
+ ) } + + ); +} From d6ba27c5a5e8c1bafd420dbc6e931fb3e794f263 Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Wed, 29 Nov 2023 09:39:24 +0800 Subject: [PATCH 2/9] Fix native --- packages/block-library/src/block/{ => v1}/edit.native.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename packages/block-library/src/block/{ => v1}/edit.native.js (98%) diff --git a/packages/block-library/src/block/edit.native.js b/packages/block-library/src/block/v1/edit.native.js similarity index 98% rename from packages/block-library/src/block/edit.native.js rename to packages/block-library/src/block/v1/edit.native.js index 9ab6ccf86a1e19..3a649921b3dda1 100644 --- a/packages/block-library/src/block/edit.native.js +++ b/packages/block-library/src/block/v1/edit.native.js @@ -42,8 +42,8 @@ import { store as noticesStore } from '@wordpress/notices'; /** * Internal dependencies */ -import styles from './editor.scss'; -import EditTitle from './edit-title'; +import styles from '../editor.scss'; +import EditTitle from '../edit-title'; export default function ReusableBlockEdit( { attributes: { ref }, From 6a6d651978316c57ee32fd8b83838e1895d1dd9c Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Wed, 29 Nov 2023 16:26:37 +0800 Subject: [PATCH 3/9] Rename dynamicContent to overrides --- lib/block-supports/pattern.php | 6 +-- lib/experimental/connection-sources/index.php | 2 +- packages/block-library/src/block/edit.js | 40 ++++++++----------- packages/block-library/src/block/index.php | 12 +++--- 4 files changed, 27 insertions(+), 33 deletions(-) diff --git a/lib/block-supports/pattern.php b/lib/block-supports/pattern.php index 66bc1e699efda9..cc925b14bcb801 100644 --- a/lib/block-supports/pattern.php +++ b/lib/block-supports/pattern.php @@ -8,7 +8,7 @@ $gutenberg_experiments = get_option( 'gutenberg-experiments' ); if ( $gutenberg_experiments && array_key_exists( 'gutenberg-connections', $gutenberg_experiments ) ) { /** - * Registers the dynamicContent context for block types that support it. + * Registers the overrides context for block types that support it. * * @param WP_Block_Type $block_type Block Type. */ @@ -20,8 +20,8 @@ function gutenberg_register_pattern_support( $block_type ) { $block_type->uses_context = array(); } - if ( ! in_array( 'dynamicContent', $block_type->uses_context, true ) ) { - $block_type->uses_context[] = 'dynamicContent'; + if ( ! in_array( 'overrides', $block_type->uses_context, true ) ) { + $block_type->uses_context[] = 'overrides'; } } } diff --git a/lib/experimental/connection-sources/index.php b/lib/experimental/connection-sources/index.php index e7827435407199..4bfc9f89d9adf5 100644 --- a/lib/experimental/connection-sources/index.php +++ b/lib/experimental/connection-sources/index.php @@ -13,6 +13,6 @@ return get_post_meta( $block_instance->context['postId'], $meta_field, true ); }, 'pattern_attributes' => function ( $block_instance, $meta_field ) { - return _wp_array_get( $block_instance->context, array( 'dynamicContent', $meta_field ), false ); + return _wp_array_get( $block_instance->context, array( 'overrides', $meta_field ), false ); }, ); diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js index ef0e3b40e5a72b..5a8e153eb6033b 100644 --- a/packages/block-library/src/block/edit.js +++ b/packages/block-library/src/block/edit.js @@ -87,15 +87,11 @@ const useInferredLayout = ( blocks, parentLayout ) => { }, [ blocks, parentLayout ] ); }; -function applyInitialDynamicContent( - blocks, - dynamicContent = {}, - defaultValues -) { +function applyInitialOverrides( blocks, overrides = {}, defaultValues ) { return blocks.map( ( block ) => { - const innerBlocks = applyInitialDynamicContent( + const innerBlocks = applyInitialOverrides( block.innerBlocks, - dynamicContent, + overrides, defaultValues ); if ( ! isPartiallySynced( block ) ) return { ...block, innerBlocks }; @@ -103,8 +99,8 @@ function applyInitialDynamicContent( const newAttributes = { ...block.attributes }; for ( const [ attributeKey, id ] of Object.entries( attributes ) ) { defaultValues[ id ] = block.attributes[ attributeKey ]; - if ( dynamicContent[ id ] ) { - newAttributes[ attributeKey ] = dynamicContent[ id ]; + if ( overrides[ id ] ) { + newAttributes[ attributeKey ] = overrides[ id ]; } } return { @@ -115,30 +111,28 @@ function applyInitialDynamicContent( } ); } -function getDynamicContentFromBlocks( blocks, defaultValues ) { +function getOverridesFromBlocks( blocks, defaultValues ) { /** @type {Record} */ - const dynamicContent = {}; + const overrides = {}; for ( const block of blocks ) { Object.assign( - dynamicContent, - getDynamicContentFromBlocks( block.innerBlocks, defaultValues ) + overrides, + getOverridesFromBlocks( block.innerBlocks, defaultValues ) ); if ( ! isPartiallySynced( block ) ) continue; const attributes = getPartiallySyncedAttributes( block ); for ( const [ attributeKey, id ] of Object.entries( attributes ) ) { if ( block.attributes[ attributeKey ] !== defaultValues[ id ] ) { - dynamicContent[ id ] = block.attributes[ attributeKey ]; + overrides[ id ] = block.attributes[ attributeKey ]; } } } - return Object.keys( dynamicContent ).length > 0 - ? dynamicContent - : undefined; + return Object.keys( overrides ).length > 0 ? overrides : undefined; } export default function ReusableBlockEdit( { name, - attributes: { ref, dynamicContent }, + attributes: { ref, overrides }, __unstableParentLayout: parentLayout, clientId: patternClientId, } ) { @@ -150,7 +144,7 @@ export default function ReusableBlockEdit( { ref ); const isMissing = hasResolved && ! record; - const initialDynamicContent = useRef( dynamicContent ); + const initialOverrides = useRef( overrides ); const defaultValuesRef = useRef( {} ); const { replaceInnerBlocks, @@ -169,9 +163,9 @@ export default function ReusableBlockEdit( { __unstableMarkNextChangeAsNotPersistent(); replaceInnerBlocks( patternClientId, - applyInitialDynamicContent( + applyInitialOverrides( initialBlocks, - initialDynamicContent.current, + initialOverrides.current, defaultValuesRef.current ) ); @@ -220,7 +214,7 @@ export default function ReusableBlockEdit( { : InnerBlocks.ButtonBlockAppender, } ); - // Sync the `dynamicContent` attribute from the updated blocks. + // Sync the `overrides` attribute from the updated blocks. // `syncDerivedBlockAttributes` is an action that just like `updateBlockAttributes` // but won't create an undo level. // This can be abstracted into a `useSyncDerivedAttributes` hook if needed. @@ -235,7 +229,7 @@ export default function ReusableBlockEdit( { if ( blocks !== prevBlocks ) { prevBlocks = blocks; syncDerivedBlockAttributes( patternClientId, { - dynamicContent: getDynamicContentFromBlocks( + overrides: getOverridesFromBlocks( blocks, defaultValuesRef.current ), diff --git a/packages/block-library/src/block/index.php b/packages/block-library/src/block/index.php index 89c62d17eaeb6c..1a6d29569654a5 100644 --- a/packages/block-library/src/block/index.php +++ b/packages/block-library/src/block/index.php @@ -47,13 +47,13 @@ function render_block_core_block( $attributes ) { $content = $wp_embed->autoembed( $content ); /** - * We set the `dynamicContent` context through the `render_block_context` + * We set the `overrides` context through the `render_block_context` * filter so that it is available when a pattern's inner blocks are * rendering via do_blocks given it only receives the inner content. */ - if ( isset( $attributes['dynamicContent'] ) ) { + if ( isset( $attributes['overrides'] ) ) { $filter_block_context = static function ( $context ) use ( $attributes ) { - $context['dynamicContent'] = $attributes['dynamicContent']; + $context['overrides'] = $attributes['overrides']; return $context; }; add_filter( 'render_block_context', $filter_block_context, 1 ); @@ -62,7 +62,7 @@ function render_block_core_block( $attributes ) { $content = do_blocks( $content ); unset( $seen_refs[ $attributes['ref'] ] ); - if ( isset( $attributes['dynamicContent'] ) ) { + if ( isset( $attributes['overrides'] ) ) { remove_filter( 'render_block_context', $filter_block_context, 1 ); } @@ -85,7 +85,7 @@ function register_block_core_block() { $gutenberg_experiments = get_option( 'gutenberg-experiments' ); if ( $gutenberg_experiments && array_key_exists( 'gutenberg-connections', $gutenberg_experiments ) ) { /** - * Registers the dynamicContent attribute for core/block. + * Registers the overrides attribute for core/block. * * @param array $args Array of arguments for registering a block type. * @param string $block_name Block name including namespace. @@ -96,7 +96,7 @@ function register_block_core_block_args( $args, $block_name ) { $args['attributes'] = array_merge( $args['attributes'], array( - 'dynamicContent' => array( + 'overrides' => array( 'type' => 'object', ), ) From 66ed4763ba01865adae05e3c684faf0aa14b05c6 Mon Sep 17 00:00:00 2001 From: Glen Davies Date: Thu, 30 Nov 2023 09:55:51 +1300 Subject: [PATCH 4/9] Add new experiment flag for partial syncing --- lib/block-supports/pattern.php | 2 +- lib/experimental/editor-settings.php | 4 ++++ lib/experiments-page.php | 12 ++++++++++++ packages/block-library/src/block/index.js | 2 +- packages/block-library/src/block/index.php | 2 +- 5 files changed, 19 insertions(+), 3 deletions(-) diff --git a/lib/block-supports/pattern.php b/lib/block-supports/pattern.php index cc925b14bcb801..ee0436afc05fc2 100644 --- a/lib/block-supports/pattern.php +++ b/lib/block-supports/pattern.php @@ -6,7 +6,7 @@ */ $gutenberg_experiments = get_option( 'gutenberg-experiments' ); -if ( $gutenberg_experiments && array_key_exists( 'gutenberg-connections', $gutenberg_experiments ) ) { +if ( $gutenberg_experiments && array_key_exists( 'gutenberg-pattern-partial-syncing', $gutenberg_experiments ) ) { /** * Registers the overrides context for block types that support it. * diff --git a/lib/experimental/editor-settings.php b/lib/experimental/editor-settings.php index 2c7d6310005bfa..5f61684e8b1342 100644 --- a/lib/experimental/editor-settings.php +++ b/lib/experimental/editor-settings.php @@ -33,6 +33,10 @@ function gutenberg_enable_experiments() { if ( gutenberg_is_experiment_enabled( 'gutenberg-no-tinymce' ) ) { wp_add_inline_script( 'wp-block-library', 'window.__experimentalDisableTinymce = true', 'before' ); } + + if ( $gutenberg_experiments && array_key_exists( 'gutenberg-pattern-partial-syncing', $gutenberg_experiments ) ) { + wp_add_inline_script( 'wp-block-editor', 'window.__experimentalPatternPartialSyncing = true', 'before' ); + } } add_action( 'admin_init', 'gutenberg_enable_experiments' ); diff --git a/lib/experiments-page.php b/lib/experiments-page.php index 0bcd28b2aa2c49..b77a69b692ff1f 100644 --- a/lib/experiments-page.php +++ b/lib/experiments-page.php @@ -138,6 +138,18 @@ function gutenberg_initialize_experiments_settings() { ) ); + add_settings_field( + 'gutenberg-pattern-partial-syncing', + __( 'Synced patterns partial syncing', 'gutenberg' ), + 'gutenberg_display_experiment_field', + 'gutenberg-experiments', + 'gutenberg_experiments_section', + array( + 'label' => __( 'Test partial syncing of patterns', 'gutenberg' ), + 'id' => 'gutenberg-pattern-partial-syncing', + ) + ); + register_setting( 'gutenberg-experiments', 'gutenberg-experiments' diff --git a/packages/block-library/src/block/index.js b/packages/block-library/src/block/index.js index 40a54b14772d70..0d117e6f3938ab 100644 --- a/packages/block-library/src/block/index.js +++ b/packages/block-library/src/block/index.js @@ -16,7 +16,7 @@ const { name } = metadata; export { metadata, name }; export const settings = { - edit: window.__experimentalConnections ? editV2 : editV1, + edit: window.__experimentalPatternPartialSyncing ? editV2 : editV1, icon, }; diff --git a/packages/block-library/src/block/index.php b/packages/block-library/src/block/index.php index 1a6d29569654a5..2220242ee2787c 100644 --- a/packages/block-library/src/block/index.php +++ b/packages/block-library/src/block/index.php @@ -83,7 +83,7 @@ function register_block_core_block() { add_action( 'init', 'register_block_core_block' ); $gutenberg_experiments = get_option( 'gutenberg-experiments' ); -if ( $gutenberg_experiments && array_key_exists( 'gutenberg-connections', $gutenberg_experiments ) ) { +if ( $gutenberg_experiments && array_key_exists( 'gutenberg-pattern-partial-syncing', $gutenberg_experiments ) ) { /** * Registers the overrides attribute for core/block. * From fd0d8e3eba374ecf5b6199914a21b8a18af8a4ae Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Thu, 30 Nov 2023 10:42:06 +0800 Subject: [PATCH 5/9] Automatically assign block ids to pattern blocks for use in partial synching of pattern instances (#56495) * Automatically assign block ids * Downgrade package to fix tooling * Move to the editor package and allow core/button * Fix test resolver --- lib/experimental/blocks.php | 28 ++++-- lib/experimental/connection-sources/index.php | 5 +- package-lock.json | 6 +- .../block-editor/src/hooks/custom-fields.js | 90 ++++++----------- packages/block-library/src/block/edit.js | 36 +++---- packages/block-library/src/button/block.json | 3 +- packages/editor/src/hooks/index.js | 1 + .../src/hooks/pattern-partial-syncing.js | 73 ++++++++++++++ packages/patterns/package.json | 3 +- .../components/partial-syncing-controls.js | 98 +++++++++++++++++++ packages/patterns/src/constants.js | 11 +++ packages/patterns/src/private-apis.js | 4 + test/unit/scripts/resolver.js | 3 +- 13 files changed, 270 insertions(+), 91 deletions(-) create mode 100644 packages/editor/src/hooks/pattern-partial-syncing.js create mode 100644 packages/patterns/src/components/partial-syncing-controls.js diff --git a/lib/experimental/blocks.php b/lib/experimental/blocks.php index 73c23999cc3c46..323ae54d273386 100644 --- a/lib/experimental/blocks.php +++ b/lib/experimental/blocks.php @@ -97,9 +97,11 @@ function gutenberg_render_block_connections( $block_content, $block, $block_inst // Allowlist of blocks that support block connections. // Currently, we only allow the following blocks and attributes: // - Paragraph: content. + // - Button: text. // - Image: url. $blocks_attributes_allowlist = array( 'core/paragraph' => array( 'content' ), + 'core/button' => array( 'text' ), 'core/image' => array( 'url' ), ); @@ -142,17 +144,25 @@ function gutenberg_render_block_connections( $block_content, $block, $block_inst continue; } - // If the attribute does not specify the name of the custom field, skip it. - if ( ! isset( $attribute_value['value'] ) ) { - continue; + if ( 'pattern_attributes' === $attribute_value['source'] ) { + if ( ! _wp_array_get( $block_instance->attributes, array( 'metadata', 'id' ), false ) ) { + continue; + } + + $custom_value = $connection_sources[ $attribute_value['source'] ]( $block_instance ); + } else { + // If the attribute does not specify the name of the custom field, skip it. + if ( ! isset( $attribute_value['value'] ) ) { + continue; + } + + // Get the content from the connection source. + $custom_value = $connection_sources[ $attribute_value['source'] ]( + $block_instance, + $attribute_value['value'] + ); } - // Get the content from the connection source. - $custom_value = $connection_sources[ $attribute_value['source'] ]( - $block_instance, - $attribute_value['value'] - ); - if ( false === $custom_value ) { continue; } diff --git a/lib/experimental/connection-sources/index.php b/lib/experimental/connection-sources/index.php index 4bfc9f89d9adf5..435f97142f31ac 100644 --- a/lib/experimental/connection-sources/index.php +++ b/lib/experimental/connection-sources/index.php @@ -12,7 +12,8 @@ // if it doesn't, `get_post_meta()` will just return an empty string. return get_post_meta( $block_instance->context['postId'], $meta_field, true ); }, - 'pattern_attributes' => function ( $block_instance, $meta_field ) { - return _wp_array_get( $block_instance->context, array( 'overrides', $meta_field ), false ); + 'pattern_attributes' => function ( $block_instance ) { + $block_id = $block_instance->attributes['metadata']['id']; + return _wp_array_get( $block_instance->context, array( 'overrides', $block_id ), false ); }, ); diff --git a/package-lock.json b/package-lock.json index b2deb50ef559fa..7c3539dd39fbe2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56046,7 +56046,8 @@ "@wordpress/icons": "file:../icons", "@wordpress/notices": "file:../notices", "@wordpress/private-apis": "file:../private-apis", - "@wordpress/url": "file:../url" + "@wordpress/url": "file:../url", + "nanoid": "^3.3.4" }, "engines": { "node": ">=16.0.0" @@ -70916,7 +70917,8 @@ "@wordpress/icons": "file:../icons", "@wordpress/notices": "file:../notices", "@wordpress/private-apis": "file:../private-apis", - "@wordpress/url": "file:../url" + "@wordpress/url": "file:../url", + "nanoid": "^3.3.4" } }, "@wordpress/plugins": { diff --git a/packages/block-editor/src/hooks/custom-fields.js b/packages/block-editor/src/hooks/custom-fields.js index 60c924f40c9408..adb9df15824a77 100644 --- a/packages/block-editor/src/hooks/custom-fields.js +++ b/packages/block-editor/src/hooks/custom-fields.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { addFilter } from '@wordpress/hooks'; -import { PanelBody, TextControl, SelectControl } from '@wordpress/components'; +import { PanelBody, TextControl } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; import { hasBlockSupport } from '@wordpress/blocks'; import { createHigherOrderComponent } from '@wordpress/compose'; @@ -47,71 +47,45 @@ function CustomFieldsControl( props ) { if ( props.name === 'core/paragraph' ) attributeName = 'content'; if ( props.name === 'core/image' ) attributeName = 'url'; - const connectionSource = - props.attributes?.connections?.attributes?.[ attributeName ]?.source || - ''; - const connectionValue = - props.attributes?.connections?.attributes?.[ attributeName ]?.value || - ''; - - function updateConnections( source, value ) { - if ( value === '' ) { - props.setAttributes( { - connections: undefined, - placeholder: undefined, - } ); - } else { - props.setAttributes( { - connections: { - attributes: { - // The attributeName will be either `content` or `url`. - [ attributeName ]: { - // Source will be variable, could be post_meta, user_meta, term_meta, etc. - // Could even be a custom source like a social media attribute. - source, - value, - }, - }, - }, - placeholder: sprintf( - 'This content will be replaced on the frontend by the value of "%s" custom field.', - value - ), - } ); - } - } - return ( - { - updateConnections( nextSource, connectionValue ); - } } - /> { - updateConnections( connectionSource, nextValue ); + if ( nextValue === '' ) { + props.setAttributes( { + connections: undefined, + [ attributeName ]: undefined, + placeholder: undefined, + } ); + } else { + props.setAttributes( { + connections: { + attributes: { + // The attributeName will be either `content` or `url`. + [ attributeName ]: { + // Source will be variable, could be post_meta, user_meta, term_meta, etc. + // Could even be a custom source like a social media attribute. + source: 'meta_fields', + value: nextValue, + }, + }, + }, + [ attributeName ]: undefined, + placeholder: sprintf( + 'This content will be replaced on the frontend by the value of "%s" custom field.', + nextValue + ), + } ); + } } } /> diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js index 5a8e153eb6033b..e86ed9b59c62b2 100644 --- a/packages/block-library/src/block/edit.js +++ b/packages/block-library/src/block/edit.js @@ -46,14 +46,11 @@ function isPartiallySynced( block ) { ); } function getPartiallySyncedAttributes( block ) { - const attributes = {}; - for ( const [ attribute, connection ] of Object.entries( - block.attributes.connections.attributes - ) ) { - if ( connection.source !== 'pattern_attributes' ) continue; - attributes[ attribute ] = connection.value; - } - return attributes; + return Object.entries( block.attributes.connections.attributes ) + .filter( + ( [ , connection ] ) => connection.source === 'pattern_attributes' + ) + .map( ( [ attributeKey ] ) => attributeKey ); } const fullAlignments = [ 'full', 'wide', 'left', 'right' ]; @@ -94,13 +91,15 @@ function applyInitialOverrides( blocks, overrides = {}, defaultValues ) { overrides, defaultValues ); - if ( ! isPartiallySynced( block ) ) return { ...block, innerBlocks }; + const blockId = block.attributes.metadata?.id; + if ( ! isPartiallySynced( block ) || ! blockId ) + return { ...block, innerBlocks }; const attributes = getPartiallySyncedAttributes( block ); const newAttributes = { ...block.attributes }; - for ( const [ attributeKey, id ] of Object.entries( attributes ) ) { - defaultValues[ id ] = block.attributes[ attributeKey ]; - if ( overrides[ id ] ) { - newAttributes[ attributeKey ] = overrides[ id ]; + for ( const attributeKey of attributes ) { + defaultValues[ blockId ] = block.attributes[ attributeKey ]; + if ( overrides[ blockId ] ) { + newAttributes[ attributeKey ] = overrides[ blockId ]; } } return { @@ -119,11 +118,14 @@ function getOverridesFromBlocks( blocks, defaultValues ) { overrides, getOverridesFromBlocks( block.innerBlocks, defaultValues ) ); - if ( ! isPartiallySynced( block ) ) continue; + const blockId = block.attributes.metadata?.id; + if ( ! isPartiallySynced( block ) || ! blockId ) continue; const attributes = getPartiallySyncedAttributes( block ); - for ( const [ attributeKey, id ] of Object.entries( attributes ) ) { - if ( block.attributes[ attributeKey ] !== defaultValues[ id ] ) { - overrides[ id ] = block.attributes[ attributeKey ]; + for ( const attributeKey of attributes ) { + if ( + block.attributes[ attributeKey ] !== defaultValues[ blockId ] + ) { + overrides[ blockId ] = block.attributes[ attributeKey ]; } } } diff --git a/packages/block-library/src/button/block.json b/packages/block-library/src/button/block.json index eec327b4ca48e4..4286f200fda4b9 100644 --- a/packages/block-library/src/button/block.json +++ b/packages/block-library/src/button/block.json @@ -118,7 +118,8 @@ "width": true } }, - "__experimentalSelector": ".wp-block-button .wp-block-button__link" + "__experimentalSelector": ".wp-block-button .wp-block-button__link", + "__experimentalConnections": true }, "styles": [ { "name": "fill", "label": "Fill", "isDefault": true }, diff --git a/packages/editor/src/hooks/index.js b/packages/editor/src/hooks/index.js index 6e0934d63c0cfa..5a48ec1bf49566 100644 --- a/packages/editor/src/hooks/index.js +++ b/packages/editor/src/hooks/index.js @@ -3,3 +3,4 @@ */ import './custom-sources-backwards-compatibility'; import './default-autocompleters'; +import './pattern-partial-syncing'; diff --git a/packages/editor/src/hooks/pattern-partial-syncing.js b/packages/editor/src/hooks/pattern-partial-syncing.js new file mode 100644 index 00000000000000..75a969435f8292 --- /dev/null +++ b/packages/editor/src/hooks/pattern-partial-syncing.js @@ -0,0 +1,73 @@ +/** + * WordPress dependencies + */ +import { addFilter } from '@wordpress/hooks'; +import { privateApis } from '@wordpress/patterns'; +import { createHigherOrderComponent } from '@wordpress/compose'; +import { useBlockEditingMode } from '@wordpress/block-editor'; +import { hasBlockSupport } from '@wordpress/blocks'; +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { store as editorStore } from '../store'; +import { unlock } from '../lock-unlock'; + +const { + PartialSyncingControls, + PATTERN_TYPES, + PARTIAL_SYNCING_SUPPORTED_BLOCKS, +} = unlock( privateApis ); + +/** + * Override the default edit UI to include a new block inspector control for + * assigning a partial syncing controls to supported blocks in the pattern editor. + * Currently, only the `core/paragraph` block is supported. + * + * @param {Component} BlockEdit Original component. + * + * @return {Component} Wrapped component. + */ +const withPartialSyncingControls = createHigherOrderComponent( + ( BlockEdit ) => ( props ) => { + const blockEditingMode = useBlockEditingMode(); + const hasCustomFieldsSupport = hasBlockSupport( + props.name, + '__experimentalConnections', + false + ); + const isEditingPattern = useSelect( + ( select ) => + select( editorStore ).getCurrentPostType() === + PATTERN_TYPES.user, + [] + ); + + const shouldShowPartialSyncingControls = + hasCustomFieldsSupport && + props.isSelected && + isEditingPattern && + blockEditingMode === 'default' && + Object.keys( PARTIAL_SYNCING_SUPPORTED_BLOCKS ).includes( + props.name + ); + + return ( + <> + + { shouldShowPartialSyncingControls && ( + + ) } + + ); + } +); + +if ( window.__experimentalConnections ) { + addFilter( + 'editor.BlockEdit', + 'core/editor/with-partial-syncing-controls', + withPartialSyncingControls + ); +} diff --git a/packages/patterns/package.json b/packages/patterns/package.json index bab11059bf92c9..783193ad2a6865 100644 --- a/packages/patterns/package.json +++ b/packages/patterns/package.json @@ -44,7 +44,8 @@ "@wordpress/icons": "file:../icons", "@wordpress/notices": "file:../notices", "@wordpress/private-apis": "file:../private-apis", - "@wordpress/url": "file:../url" + "@wordpress/url": "file:../url", + "nanoid": "^3.3.4" }, "peerDependencies": { "react": "^18.0.0", diff --git a/packages/patterns/src/components/partial-syncing-controls.js b/packages/patterns/src/components/partial-syncing-controls.js new file mode 100644 index 00000000000000..42c39ce69e87bf --- /dev/null +++ b/packages/patterns/src/components/partial-syncing-controls.js @@ -0,0 +1,98 @@ +/** + * External dependencies + */ +import { nanoid } from 'nanoid'; + +/** + * WordPress dependencies + */ +import { InspectorControls } from '@wordpress/block-editor'; +import { BaseControl, CheckboxControl } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { PARTIAL_SYNCING_SUPPORTED_BLOCKS } from '../constants'; + +function PartialSyncingControls( { name, attributes, setAttributes } ) { + const syncedAttributes = PARTIAL_SYNCING_SUPPORTED_BLOCKS[ name ]; + + function updateConnections( attributeName, isChecked ) { + if ( ! isChecked ) { + let updatedConnections = { + ...attributes.connections, + attributes: { + ...attributes.connections?.attributes, + [ attributeName ]: undefined, + }, + }; + if ( Object.keys( updatedConnections.attributes ).length === 1 ) { + updatedConnections.attributes = undefined; + } + if ( + Object.keys( updatedConnections ).length === 1 && + updateConnections.attributes === undefined + ) { + updatedConnections = undefined; + } + setAttributes( { + connections: updatedConnections, + } ); + return; + } + + const updatedConnections = { + ...attributes.connections, + attributes: { + ...attributes.connections?.attributes, + [ attributeName ]: { + source: 'pattern_attributes', + }, + }, + }; + + if ( typeof attributes.metadata?.id === 'string' ) { + setAttributes( { connections: updatedConnections } ); + return; + } + + const id = nanoid( 6 ); + setAttributes( { + connections: updatedConnections, + metadata: { + ...attributes.metadata, + id, + }, + } ); + } + + return ( + + + + { __( 'Synced attributes' ) } + + { Object.entries( syncedAttributes ).map( + ( [ attributeName, label ] ) => ( + { + updateConnections( attributeName, isChecked ); + } } + /> + ) + ) } + + + ); +} + +export default PartialSyncingControls; diff --git a/packages/patterns/src/constants.js b/packages/patterns/src/constants.js index 465970b17b7aae..c61150874c5621 100644 --- a/packages/patterns/src/constants.js +++ b/packages/patterns/src/constants.js @@ -1,3 +1,8 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + export const PATTERN_TYPES = { theme: 'pattern', user: 'wp_block', @@ -14,3 +19,9 @@ export const PATTERN_SYNC_TYPES = { full: 'fully', unsynced: 'unsynced', }; + +// TODO: This should not be hardcoded. Maybe there should be a config and/or an UI. +export const PARTIAL_SYNCING_SUPPORTED_BLOCKS = { + 'core/paragraph': { content: __( 'Content' ) }, + 'core/button': { text: __( 'Text' ) }, +}; diff --git a/packages/patterns/src/private-apis.js b/packages/patterns/src/private-apis.js index 770a78fd4fa9de..b357efb1bc107a 100644 --- a/packages/patterns/src/private-apis.js +++ b/packages/patterns/src/private-apis.js @@ -7,12 +7,14 @@ import DuplicatePatternModal from './components/duplicate-pattern-modal'; import RenamePatternModal from './components/rename-pattern-modal'; import PatternsMenuItems from './components'; import RenamePatternCategoryModal from './components/rename-pattern-category-modal'; +import PartialSyncingControls from './components/partial-syncing-controls'; import { PATTERN_TYPES, PATTERN_DEFAULT_CATEGORY, PATTERN_USER_CATEGORY, EXCLUDED_PATTERN_SOURCES, PATTERN_SYNC_TYPES, + PARTIAL_SYNCING_SUPPORTED_BLOCKS, } from './constants'; export const privateApis = {}; @@ -22,9 +24,11 @@ lock( privateApis, { RenamePatternModal, PatternsMenuItems, RenamePatternCategoryModal, + PartialSyncingControls, PATTERN_TYPES, PATTERN_DEFAULT_CATEGORY, PATTERN_USER_CATEGORY, EXCLUDED_PATTERN_SOURCES, PATTERN_SYNC_TYPES, + PARTIAL_SYNCING_SUPPORTED_BLOCKS, } ); diff --git a/test/unit/scripts/resolver.js b/test/unit/scripts/resolver.js index 2c359145f0b3e8..7672bb723e1248 100644 --- a/test/unit/scripts/resolver.js +++ b/test/unit/scripts/resolver.js @@ -24,7 +24,8 @@ module.exports = ( path, options ) => { pkg.name === 'uuid' || pkg.name === 'react-colorful' || pkg.name === '@eslint/eslintrc' || - pkg.name === 'expect' + pkg.name === 'expect' || + pkg.name === 'nanoid' ) { delete pkg.exports; delete pkg.module; From f5f6282300ebf19dfdc88591157215d31b91138c Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Thu, 30 Nov 2023 11:39:48 +0800 Subject: [PATCH 6/9] Fix the flag and remove support for core/button --- lib/experimental/blocks.php | 7 ++++--- packages/block-editor/src/hooks/custom-fields.js | 7 ++++++- packages/block-library/src/button/block.json | 3 +-- packages/editor/src/hooks/pattern-partial-syncing.js | 2 +- packages/patterns/src/constants.js | 1 - 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/experimental/blocks.php b/lib/experimental/blocks.php index 323ae54d273386..88e46b478389d2 100644 --- a/lib/experimental/blocks.php +++ b/lib/experimental/blocks.php @@ -82,7 +82,10 @@ function wp_enqueue_block_view_script( $block_name, $args ) { $gutenberg_experiments = get_option( 'gutenberg-experiments' ); -if ( $gutenberg_experiments && array_key_exists( 'gutenberg-connections', $gutenberg_experiments ) ) { +if ( $gutenberg_experiments && ( + array_key_exists( 'gutenberg-connections', $gutenberg_experiments ) || + array_key_exists( 'gutenberg-pattern-partial-syncing', $gutenberg_experiments ) +) ) { /** * Renders the block meta attributes. * @@ -97,11 +100,9 @@ function gutenberg_render_block_connections( $block_content, $block, $block_inst // Allowlist of blocks that support block connections. // Currently, we only allow the following blocks and attributes: // - Paragraph: content. - // - Button: text. // - Image: url. $blocks_attributes_allowlist = array( 'core/paragraph' => array( 'content' ), - 'core/button' => array( 'text' ), 'core/image' => array( 'url' ), ); diff --git a/packages/block-editor/src/hooks/custom-fields.js b/packages/block-editor/src/hooks/custom-fields.js index adb9df15824a77..8ab816abc7352a 100644 --- a/packages/block-editor/src/hooks/custom-fields.js +++ b/packages/block-editor/src/hooks/custom-fields.js @@ -128,12 +128,17 @@ const withCustomFieldsControls = createHigherOrderComponent( ( BlockEdit ) => { }; }, 'withCustomFieldsControls' ); -if ( window.__experimentalConnections ) { +if ( + window.__experimentalConnections || + window.__experimentalPatternPartialSyncing +) { addFilter( 'blocks.registerBlockType', 'core/editor/connections/attribute', addAttribute ); +} +if ( window.__experimentalConnections ) { addFilter( 'editor.BlockEdit', 'core/editor/connections/with-inspector-controls', diff --git a/packages/block-library/src/button/block.json b/packages/block-library/src/button/block.json index 4286f200fda4b9..eec327b4ca48e4 100644 --- a/packages/block-library/src/button/block.json +++ b/packages/block-library/src/button/block.json @@ -118,8 +118,7 @@ "width": true } }, - "__experimentalSelector": ".wp-block-button .wp-block-button__link", - "__experimentalConnections": true + "__experimentalSelector": ".wp-block-button .wp-block-button__link" }, "styles": [ { "name": "fill", "label": "Fill", "isDefault": true }, diff --git a/packages/editor/src/hooks/pattern-partial-syncing.js b/packages/editor/src/hooks/pattern-partial-syncing.js index 75a969435f8292..26aeb30ca40e57 100644 --- a/packages/editor/src/hooks/pattern-partial-syncing.js +++ b/packages/editor/src/hooks/pattern-partial-syncing.js @@ -64,7 +64,7 @@ const withPartialSyncingControls = createHigherOrderComponent( } ); -if ( window.__experimentalConnections ) { +if ( window.__experimentalPatternPartialSyncing ) { addFilter( 'editor.BlockEdit', 'core/editor/with-partial-syncing-controls', diff --git a/packages/patterns/src/constants.js b/packages/patterns/src/constants.js index c61150874c5621..3e533d834fd75c 100644 --- a/packages/patterns/src/constants.js +++ b/packages/patterns/src/constants.js @@ -23,5 +23,4 @@ export const PATTERN_SYNC_TYPES = { // TODO: This should not be hardcoded. Maybe there should be a config and/or an UI. export const PARTIAL_SYNCING_SUPPORTED_BLOCKS = { 'core/paragraph': { content: __( 'Content' ) }, - 'core/button': { text: __( 'Text' ) }, }; From a1ecae4392aa7dc730ecb36f0b4c3abb0cb6a864 Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Thu, 30 Nov 2023 14:35:35 +0800 Subject: [PATCH 7/9] Address code reviews --- packages/block-library/src/block/index.php | 9 +++++++-- packages/editor/src/hooks/pattern-partial-syncing.js | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/block-library/src/block/index.php b/packages/block-library/src/block/index.php index 2220242ee2787c..265e0ac75e267c 100644 --- a/packages/block-library/src/block/index.php +++ b/packages/block-library/src/block/index.php @@ -46,12 +46,17 @@ function render_block_core_block( $attributes ) { $content = $wp_embed->run_shortcode( $reusable_block->post_content ); $content = $wp_embed->autoembed( $content ); + $gutenberg_experiments = get_option( 'gutenberg-experiments' ); + $has_partial_synced_overrides = $gutenberg_experiments + && array_key_exists( 'gutenberg-pattern-partial-syncing', $gutenberg_experiments ) + && isset( $attributes['overrides'] ); + /** * We set the `overrides` context through the `render_block_context` * filter so that it is available when a pattern's inner blocks are * rendering via do_blocks given it only receives the inner content. */ - if ( isset( $attributes['overrides'] ) ) { + if ( $has_partial_synced_overrides ) { $filter_block_context = static function ( $context ) use ( $attributes ) { $context['overrides'] = $attributes['overrides']; return $context; @@ -62,7 +67,7 @@ function render_block_core_block( $attributes ) { $content = do_blocks( $content ); unset( $seen_refs[ $attributes['ref'] ] ); - if ( isset( $attributes['overrides'] ) ) { + if ( $has_partial_synced_overrides ) { remove_filter( 'render_block_context', $filter_block_context, 1 ); } diff --git a/packages/editor/src/hooks/pattern-partial-syncing.js b/packages/editor/src/hooks/pattern-partial-syncing.js index 26aeb30ca40e57..40bd1e16dfc00d 100644 --- a/packages/editor/src/hooks/pattern-partial-syncing.js +++ b/packages/editor/src/hooks/pattern-partial-syncing.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { addFilter } from '@wordpress/hooks'; -import { privateApis } from '@wordpress/patterns'; +import { privateApis as patternsPrivateApis } from '@wordpress/patterns'; import { createHigherOrderComponent } from '@wordpress/compose'; import { useBlockEditingMode } from '@wordpress/block-editor'; import { hasBlockSupport } from '@wordpress/blocks'; @@ -18,7 +18,7 @@ const { PartialSyncingControls, PATTERN_TYPES, PARTIAL_SYNCING_SUPPORTED_BLOCKS, -} = unlock( privateApis ); +} = unlock( patternsPrivateApis ); /** * Override the default edit UI to include a new block inspector control for From 728f5922847bedf5d8f4e3c98e97e749046a0403 Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Thu, 30 Nov 2023 16:59:29 +0800 Subject: [PATCH 8/9] Rename to pattern/overrides --- lib/block-supports/pattern.php | 4 ++-- lib/experimental/connection-sources/index.php | 2 +- packages/block-library/src/block/index.php | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/block-supports/pattern.php b/lib/block-supports/pattern.php index ee0436afc05fc2..a783135c793e3f 100644 --- a/lib/block-supports/pattern.php +++ b/lib/block-supports/pattern.php @@ -20,8 +20,8 @@ function gutenberg_register_pattern_support( $block_type ) { $block_type->uses_context = array(); } - if ( ! in_array( 'overrides', $block_type->uses_context, true ) ) { - $block_type->uses_context[] = 'overrides'; + if ( ! in_array( 'pattern/overrides', $block_type->uses_context, true ) ) { + $block_type->uses_context[] = 'pattern/overrides'; } } } diff --git a/lib/experimental/connection-sources/index.php b/lib/experimental/connection-sources/index.php index 435f97142f31ac..bf89ba177b6e94 100644 --- a/lib/experimental/connection-sources/index.php +++ b/lib/experimental/connection-sources/index.php @@ -14,6 +14,6 @@ }, 'pattern_attributes' => function ( $block_instance ) { $block_id = $block_instance->attributes['metadata']['id']; - return _wp_array_get( $block_instance->context, array( 'overrides', $block_id ), false ); + return _wp_array_get( $block_instance->context, array( 'pattern/overrides', $block_id ), false ); }, ); diff --git a/packages/block-library/src/block/index.php b/packages/block-library/src/block/index.php index 265e0ac75e267c..36783043d12171 100644 --- a/packages/block-library/src/block/index.php +++ b/packages/block-library/src/block/index.php @@ -52,13 +52,13 @@ function render_block_core_block( $attributes ) { && isset( $attributes['overrides'] ); /** - * We set the `overrides` context through the `render_block_context` + * We set the `pattern/overrides` context through the `render_block_context` * filter so that it is available when a pattern's inner blocks are * rendering via do_blocks given it only receives the inner content. */ if ( $has_partial_synced_overrides ) { $filter_block_context = static function ( $context ) use ( $attributes ) { - $context['overrides'] = $attributes['overrides']; + $context['pattern/overrides'] = $attributes['overrides']; return $context; }; add_filter( 'render_block_context', $filter_block_context, 1 ); From cc1d5bc43745ebdff9d276a56302ffc3d3664554 Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Thu, 30 Nov 2023 17:49:43 +0800 Subject: [PATCH 9/9] Fix php lint error --- packages/block-library/src/block/index.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-library/src/block/index.php b/packages/block-library/src/block/index.php index 36783043d12171..54b54fad139ff3 100644 --- a/packages/block-library/src/block/index.php +++ b/packages/block-library/src/block/index.php @@ -46,7 +46,7 @@ function render_block_core_block( $attributes ) { $content = $wp_embed->run_shortcode( $reusable_block->post_content ); $content = $wp_embed->autoembed( $content ); - $gutenberg_experiments = get_option( 'gutenberg-experiments' ); + $gutenberg_experiments = get_option( 'gutenberg-experiments' ); $has_partial_synced_overrides = $gutenberg_experiments && array_key_exists( 'gutenberg-pattern-partial-syncing', $gutenberg_experiments ) && isset( $attributes['overrides'] );