diff --git a/lib/experimental/blocks.php b/lib/experimental/blocks.php index 73c23999cc3c46..31c5d314eecf28 100644 --- a/lib/experimental/blocks.php +++ b/lib/experimental/blocks.php @@ -142,17 +142,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 e7827435407199..3f9f9d827f85ec 100644 --- a/lib/experimental/connection-sources/index.php +++ b/lib/experimental/connection-sources/index.php @@ -12,7 +12,7 @@ // 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 ); + 'pattern_attributes' => function ( $block_instance ) { + return _wp_array_get( $block_instance->context, array( 'dynamicContent', $block_instance->attributes['metadata']['id'] ), false ); }, ); diff --git a/package-lock.json b/package-lock.json index b2deb50ef559fa..f37fe7a47d40bc 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": "^4.0.2" }, "engines": { "node": ">=16.0.0" @@ -56056,6 +56057,23 @@ "react-dom": "^18.0.0" } }, + "packages/patterns/node_modules/nanoid": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-4.0.2.tgz", + "integrity": "sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^14 || ^16 || >=18" + } + }, "packages/plugins": { "name": "@wordpress/plugins", "version": "6.14.0", @@ -70916,7 +70934,15 @@ "@wordpress/icons": "file:../icons", "@wordpress/notices": "file:../notices", "@wordpress/private-apis": "file:../private-apis", - "@wordpress/url": "file:../url" + "@wordpress/url": "file:../url", + "nanoid": "^4.0.2" + }, + "dependencies": { + "nanoid": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-4.0.2.tgz", + "integrity": "sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==" + } } }, "@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 ef0e3b40e5a72b..6398e6616a3c60 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' ]; @@ -98,13 +95,15 @@ function applyInitialDynamicContent( dynamicContent, 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 ( dynamicContent[ id ] ) { - newAttributes[ attributeKey ] = dynamicContent[ id ]; + for ( const attributeKey of attributes ) { + defaultValues[ blockId ] = block.attributes[ attributeKey ]; + if ( dynamicContent[ blockId ] ) { + newAttributes[ attributeKey ] = dynamicContent[ blockId ]; } } return { @@ -123,11 +122,14 @@ function getDynamicContentFromBlocks( blocks, defaultValues ) { dynamicContent, getDynamicContentFromBlocks( 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 ] ) { - dynamicContent[ id ] = block.attributes[ attributeKey ]; + for ( const attributeKey of attributes ) { + if ( + block.attributes[ attributeKey ] !== defaultValues[ blockId ] + ) { + dynamicContent[ blockId ] = block.attributes[ attributeKey ]; } } } diff --git a/packages/edit-site/src/hooks/index.js b/packages/edit-site/src/hooks/index.js index 4e871f4e3824eb..9be1160f7f3672 100644 --- a/packages/edit-site/src/hooks/index.js +++ b/packages/edit-site/src/hooks/index.js @@ -5,3 +5,4 @@ import './components'; import './push-changes-to-global-styles'; import './template-part-edit'; import './navigation-menu-edit'; +import './pattern-partial-syncing'; diff --git a/packages/edit-site/src/hooks/pattern-partial-syncing.js b/packages/edit-site/src/hooks/pattern-partial-syncing.js new file mode 100644 index 00000000000000..c9927afba1d1a1 --- /dev/null +++ b/packages/edit-site/src/hooks/pattern-partial-syncing.js @@ -0,0 +1,69 @@ +/** + * 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 editSiteStore } from '../store'; +import { unlock } from '../lock-unlock'; + +const { PartialSyncingControls, PATTERN_TYPES } = 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( editSiteStore ).getEditedPostType() === + PATTERN_TYPES.user, + [] + ); + + // Check if editing a pattern and the current block is a paragraph block. + // Currently, only the paragraph block is supported. + const shouldShowPartialSyncingControls = + hasCustomFieldsSupport && + props.isSelected && + isEditingPattern && + blockEditingMode === 'default' && + [ 'core/paragraph' ].includes( props.name ); + + return ( + <> + + { shouldShowPartialSyncingControls && ( + + ) } + + ); + } +); + +if ( window.__experimentalConnections ) { + addFilter( + 'editor.BlockEdit', + 'core/edit-site/with-partial-syncing-controls', + withPartialSyncingControls + ); +} diff --git a/packages/patterns/package.json b/packages/patterns/package.json index bab11059bf92c9..8172ad2a435547 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": "^4.0.2" }, "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..9ccfc519ecafb7 --- /dev/null +++ b/packages/patterns/src/components/partial-syncing-controls.js @@ -0,0 +1,74 @@ +/** + * External dependencies + */ +import { nanoid } from 'nanoid'; + +/** + * WordPress dependencies + */ +import { BaseControl, CheckboxControl } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { InspectorControls } from '@wordpress/block-editor'; + +function PartialSyncingControls( { attributes, setAttributes } ) { + // Only the `content` attribute of the paragraph block is currently supported. + const attributeName = 'content'; + + const isPartiallySynced = + attributes.connections?.attributes?.[ attributeName ]?.source === + 'pattern_attributes'; + + function updateConnections( isChecked ) { + if ( ! isChecked ) { + setAttributes( { + connections: undefined, + } ); + return; + } + if ( typeof attributes.metadata?.id === 'string' ) { + setAttributes( { + connections: { + attributes: { + [ attributeName ]: { + source: 'pattern_attributes', + }, + }, + }, + } ); + return; + } + + const id = nanoid( 6 ); + setAttributes( { + connections: { + attributes: { + [ attributeName ]: { source: 'pattern_attributes' }, + }, + }, + metadata: { + ...attributes.metadata, + id, + }, + } ); + } + + return ( + + + + { __( 'Synced attributes' ) } + + { + updateConnections( isChecked ); + } } + /> + + + ); +} + +export default PartialSyncingControls; diff --git a/packages/patterns/src/private-apis.js b/packages/patterns/src/private-apis.js index 770a78fd4fa9de..c24fe2aea90c79 100644 --- a/packages/patterns/src/private-apis.js +++ b/packages/patterns/src/private-apis.js @@ -7,6 +7,7 @@ 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, @@ -22,6 +23,7 @@ lock( privateApis, { RenamePatternModal, PatternsMenuItems, RenamePatternCategoryModal, + PartialSyncingControls, PATTERN_TYPES, PATTERN_DEFAULT_CATEGORY, PATTERN_USER_CATEGORY,