diff --git a/backport-changelog/6.6/6694.md b/backport-changelog/6.6/6694.md new file mode 100644 index 00000000000000..a9eb5a7f37ef5b --- /dev/null +++ b/backport-changelog/6.6/6694.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/6694 + +* https://github.com/WordPress/gutenberg/pull/60694 diff --git a/lib/compat/wordpress-6.6/blocks.php b/lib/compat/wordpress-6.6/blocks.php new file mode 100644 index 00000000000000..0d8805a489d9cb --- /dev/null +++ b/lib/compat/wordpress-6.6/blocks.php @@ -0,0 +1,46 @@ + array( 'content' ), + 'core/heading' => array( 'content' ), + 'core/image' => array( 'id', 'url', 'title', 'alt' ), + 'core/button' => array( 'url', 'text', 'linkTarget', 'rel' ), + ); + + $bindings = $parsed_block['attrs']['metadata']['bindings'] ?? array(); + if ( + isset( $bindings['__default']['source'] ) && + 'core/pattern-overrides' === $bindings['__default']['source'] + ) { + $updated_bindings = array(); + + // Build an binding array of all supported attributes. + // Note that this also omits the `__default` attribute from the + // resulting array. + foreach ( $supported_block_attrs[ $parsed_block['blockName'] ] as $attribute_name ) { + // Retain any non-pattern override bindings that might be present. + $updated_bindings[ $attribute_name ] = isset( $bindings[ $attribute_name ] ) + ? $bindings[ $attribute_name ] + : array( 'source' => 'core/pattern-overrides' ); + } + $parsed_block['attrs']['metadata']['bindings'] = $updated_bindings; + } + + return $parsed_block; +} + +add_filter( 'render_block_data', 'gutenberg_replace_pattern_override_default_binding', 10, 1 ); diff --git a/lib/load.php b/lib/load.php index 6179ade9a2288e..23985f9c8a92e9 100644 --- a/lib/load.php +++ b/lib/load.php @@ -131,6 +131,7 @@ function gutenberg_is_experiment_enabled( $name ) { // WordPress 6.6 compat. require __DIR__ . '/compat/wordpress-6.6/admin-bar.php'; +require __DIR__ . '/compat/wordpress-6.6/blocks.php'; require __DIR__ . '/compat/wordpress-6.6/compat.php'; require __DIR__ . '/compat/wordpress-6.6/resolve-patterns.php'; require __DIR__ . '/compat/wordpress-6.6/block-bindings/pattern-overrides.php'; diff --git a/packages/block-editor/src/hooks/use-bindings-attributes.js b/packages/block-editor/src/hooks/use-bindings-attributes.js index b7a4ca0379dd1b..334c751bc01b0b 100644 --- a/packages/block-editor/src/hooks/use-bindings-attributes.js +++ b/packages/block-editor/src/hooks/use-bindings-attributes.js @@ -4,7 +4,7 @@ import { store as blocksStore } from '@wordpress/blocks'; import { createHigherOrderComponent } from '@wordpress/compose'; import { useRegistry, useSelect } from '@wordpress/data'; -import { useCallback } from '@wordpress/element'; +import { useCallback, useMemo } from '@wordpress/element'; import { addFilter } from '@wordpress/hooks'; /** @@ -29,6 +29,41 @@ const BLOCK_BINDINGS_ALLOWED_BLOCKS = { 'core/button': [ 'url', 'text', 'linkTarget', 'rel' ], }; +const DEFAULT_ATTRIBUTE = '__default'; + +/** + * Returns the bindings with the `__default` binding for pattern overrides + * replaced with the full-set of supported attributes. e.g.: + * + * bindings passed in: `{ __default: { source: 'core/pattern-overrides' } }` + * bindings returned: `{ content: { source: 'core/pattern-overrides' } }` + * + * @param {string} blockName The block name (e.g. 'core/paragraph'). + * @param {Object} bindings A block's bindings from the metadata attribute. + * + * @return {Object} The bindings with default replaced for pattern overrides. + */ +function replacePatternOverrideDefaultBindings( blockName, bindings ) { + // The `__default` binding currently only works for pattern overrides. + if ( + bindings?.[ DEFAULT_ATTRIBUTE ]?.source === 'core/pattern-overrides' + ) { + const supportedAttributes = BLOCK_BINDINGS_ALLOWED_BLOCKS[ blockName ]; + const bindingsWithDefaults = {}; + for ( const attributeName of supportedAttributes ) { + // If the block has mixed binding sources, retain any non pattern override bindings. + const bindingSource = bindings[ attributeName ] + ? bindings[ attributeName ] + : { source: 'core/pattern-overrides' }; + bindingsWithDefaults[ attributeName ] = bindingSource; + } + + return bindingsWithDefaults; + } + + return bindings; +} + /** * Based on the given block name, * check if it is possible to bind the block. @@ -61,8 +96,15 @@ export const withBlockBindingSupport = createHigherOrderComponent( const sources = useSelect( ( select ) => unlock( select( blocksStore ) ).getAllBlockBindingsSources() ); - const bindings = props.attributes.metadata?.bindings; const { name, clientId, context } = props; + const bindings = useMemo( + () => + replacePatternOverrideDefaultBindings( + name, + props.attributes.metadata?.bindings + ), + [ props.attributes.metadata?.bindings, name ] + ); const boundAttributes = useSelect( () => { if ( ! bindings ) { return; @@ -128,8 +170,8 @@ export const withBlockBindingSupport = createHigherOrderComponent( continue; } - const source = - sources[ bindings[ attributeName ].source ]; + const binding = bindings[ attributeName ]; + const source = sources[ binding?.source ]; if ( ! source?.setValue && ! source?.setValues ) { continue; } @@ -157,12 +199,13 @@ export const withBlockBindingSupport = createHigherOrderComponent( attributeName, value, ] of Object.entries( attributes ) ) { + const binding = bindings[ attributeName ]; source.setValue( { registry, context, clientId, attributeName, - args: bindings[ attributeName ].args, + args: binding.args, value, } ); } diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js index a4f054db46665a..8d4147cb318555 100644 --- a/packages/block-library/src/block/edit.js +++ b/packages/block-library/src/block/edit.js @@ -40,7 +40,8 @@ import { name as patternBlockName } from './index'; import { unlock } from '../lock-unlock'; const { useLayoutClasses } = unlock( blockEditorPrivateApis ); -const { isOverridableBlock } = unlock( patternsPrivateApis ); +const { isOverridableBlock, hasOverridableBlocks } = + unlock( patternsPrivateApis ); const fullAlignments = [ 'full', 'wide', 'left', 'right' ]; @@ -73,15 +74,6 @@ const useInferredLayout = ( blocks, parentLayout ) => { }, [ blocks, parentLayout ] ); }; -function hasOverridableBlocks( blocks ) { - return blocks.some( ( block ) => { - if ( isOverridableBlock( block ) ) { - return true; - } - return hasOverridableBlocks( block.innerBlocks ); - } ); -} - function setBlockEditMode( setEditMode, blocks, mode ) { blocks.forEach( ( block ) => { const editMode = diff --git a/packages/block-library/src/block/index.php b/packages/block-library/src/block/index.php index 9403205c186596..8beef975fad6f3 100644 --- a/packages/block-library/src/block/index.php +++ b/packages/block-library/src/block/index.php @@ -78,7 +78,7 @@ function render_block_core_block( $attributes ) { * filter so that it is available when a pattern's inner blocks are * rendering via do_blocks given it only receives the inner content. */ - $has_pattern_overrides = isset( $attributes['content'] ); + $has_pattern_overrides = isset( $attributes['content'] ) && null !== get_block_bindings_source( 'core/pattern-overrides' ); if ( $has_pattern_overrides ) { $filter_block_context = static function ( $context ) use ( $attributes ) { $context['pattern/overrides'] = $attributes['content']; diff --git a/packages/blocks/src/api/parser/convert-legacy-block.js b/packages/blocks/src/api/parser/convert-legacy-block.js index 8396b98109792f..055679302efd64 100644 --- a/packages/blocks/src/api/parser/convert-legacy-block.js +++ b/packages/blocks/src/api/parser/convert-legacy-block.js @@ -88,25 +88,41 @@ export function convertLegacyBlockNameAndAttributes( name, attributes ) { ( name === 'core/paragraph' || name === 'core/heading' || name === 'core/image' || - name === 'core/button' ) + name === 'core/button' ) && + newAttributes.metadata.bindings.__default?.source !== + 'core/pattern-overrides' ) { const bindings = [ 'content', 'url', 'title', + 'id', 'alt', 'text', 'linkTarget', ]; + // Delete any existing individual bindings and add a default binding. + // It was only possible to add all the default attributes through the UI, + // So as soon as we find an attribute, we can assume all default attributes are overridable. + let hasPatternOverrides = false; bindings.forEach( ( binding ) => { if ( - newAttributes.metadata.bindings[ binding ]?.source?.name === - 'pattern_attributes' + newAttributes.metadata.bindings[ binding ]?.source === + 'core/pattern-overrides' ) { - newAttributes.metadata.bindings[ binding ].source = - 'core/pattern-overrides'; + hasPatternOverrides = true; + newAttributes.metadata = { + ...newAttributes.metadata, + bindings: { ...newAttributes.metadata.bindings }, + }; + delete newAttributes.metadata.bindings[ binding ]; } } ); + if ( hasPatternOverrides ) { + newAttributes.metadata.bindings.__default = { + source: 'core/pattern-overrides', + }; + } } } return [ name, newAttributes ]; diff --git a/packages/patterns/src/api/index.js b/packages/patterns/src/api/index.js index 448001f891fbae..4321dc4262145a 100644 --- a/packages/patterns/src/api/index.js +++ b/packages/patterns/src/api/index.js @@ -22,3 +22,19 @@ export function isOverridableBlock( block ) { ) ); } + +/** + * Determines whether the blocks list has overridable blocks. + * + * @param {WPBlock[]} blocks The blocks list. + * + * @return {boolean} `true` if the list has overridable blocks, `false` otherwise. + */ +export function hasOverridableBlocks( blocks ) { + return blocks.some( ( block ) => { + if ( isOverridableBlock( block ) ) { + return true; + } + return hasOverridableBlocks( block.innerBlocks ); + } ); +} diff --git a/packages/patterns/src/components/pattern-overrides-controls.js b/packages/patterns/src/components/pattern-overrides-controls.js index 3ece910a0df47c..9869c5b072c856 100644 --- a/packages/patterns/src/components/pattern-overrides-controls.js +++ b/packages/patterns/src/components/pattern-overrides-controls.js @@ -9,66 +9,48 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import { - PARTIAL_SYNCING_SUPPORTED_BLOCKS, - PATTERN_OVERRIDES_BINDING_SOURCE, -} from '../constants'; +import { PATTERN_OVERRIDES_BINDING_SOURCE } from '../constants'; import { AllowOverridesModal, DisallowOverridesModal, } from './allow-overrides-modal'; -function removeBindings( bindings, syncedAttributes ) { - let updatedBindings = {}; - for ( const attributeName of syncedAttributes ) { - // Omit any bindings that's not the same source from the `updatedBindings` object. - if ( - bindings?.[ attributeName ]?.source !== - PATTERN_OVERRIDES_BINDING_SOURCE && - bindings?.[ attributeName ]?.source !== undefined - ) { - updatedBindings[ attributeName ] = bindings[ attributeName ]; - } - } +function removeBindings( bindings ) { + let updatedBindings = { ...bindings }; + delete updatedBindings.__default; if ( ! Object.keys( updatedBindings ).length ) { updatedBindings = undefined; } return updatedBindings; } -function addBindings( bindings, syncedAttributes ) { - const updatedBindings = { ...bindings }; - for ( const attributeName of syncedAttributes ) { - if ( ! bindings?.[ attributeName ] ) { - updatedBindings[ attributeName ] = { - source: PATTERN_OVERRIDES_BINDING_SOURCE, - }; - } - } - return updatedBindings; +function addBindings( bindings ) { + return { + ...bindings, + __default: { source: PATTERN_OVERRIDES_BINDING_SOURCE }, + }; } -function PatternOverridesControls( { attributes, name, setAttributes } ) { +function PatternOverridesControls( { attributes, setAttributes } ) { const controlId = useId(); const [ showAllowOverridesModal, setShowAllowOverridesModal ] = useState( false ); const [ showDisallowOverridesModal, setShowDisallowOverridesModal ] = useState( false ); - const syncedAttributes = PARTIAL_SYNCING_SUPPORTED_BLOCKS[ name ]; - const attributeSources = syncedAttributes.map( - ( attributeName ) => - attributes.metadata?.bindings?.[ attributeName ]?.source - ); - const isConnectedToOtherSources = attributeSources.every( - ( source ) => source && source !== 'core/pattern-overrides' - ); + const hasName = !! attributes.metadata?.name; + const defaultBindings = attributes.metadata?.bindings?.__default; + const allowOverrides = + hasName && defaultBindings?.source === PATTERN_OVERRIDES_BINDING_SOURCE; + const isConnectedToOtherSources = + defaultBindings?.source && + defaultBindings.source !== PATTERN_OVERRIDES_BINDING_SOURCE; function updateBindings( isChecked, customName ) { const prevBindings = attributes?.metadata?.bindings; const updatedBindings = isChecked - ? addBindings( prevBindings, syncedAttributes ) - : removeBindings( prevBindings, syncedAttributes ); + ? addBindings( prevBindings ) + : removeBindings( prevBindings ); const updatedMetadata = { ...attributes.metadata, @@ -89,13 +71,6 @@ function PatternOverridesControls( { attributes, name, setAttributes } ) { return null; } - const hasName = !! attributes.metadata?.name; - const allowOverrides = - hasName && - attributeSources.some( - ( source ) => source === PATTERN_OVERRIDES_BINDING_SOURCE - ); - return ( <> diff --git a/packages/patterns/src/private-apis.js b/packages/patterns/src/private-apis.js index 05417de2b2c669..0553378cb56043 100644 --- a/packages/patterns/src/private-apis.js +++ b/packages/patterns/src/private-apis.js @@ -11,7 +11,7 @@ import { default as DuplicatePatternModal, useDuplicatePatternProps, } from './components/duplicate-pattern-modal'; -import { isOverridableBlock } from './api'; +import { isOverridableBlock, hasOverridableBlocks } from './api'; import RenamePatternModal from './components/rename-pattern-modal'; import PatternsMenuItems from './components'; import RenamePatternCategoryModal from './components/rename-pattern-category-modal'; @@ -34,6 +34,7 @@ lock( privateApis, { CreatePatternModalContents, DuplicatePatternModal, isOverridableBlock, + hasOverridableBlocks, useDuplicatePatternProps, RenamePatternModal, PatternsMenuItems, diff --git a/test/e2e/specs/editor/various/pattern-overrides.spec.js b/test/e2e/specs/editor/various/pattern-overrides.spec.js index 976f1c378daa97..f4648a03efe956 100644 --- a/test/e2e/specs/editor/various/pattern-overrides.spec.js +++ b/test/e2e/specs/editor/various/pattern-overrides.spec.js @@ -105,7 +105,7 @@ test.describe( 'Pattern Overrides', () => { metadata: { name: editableParagraphName, bindings: { - content: { + __default: { source: 'core/pattern-overrides', }, }, @@ -234,7 +234,7 @@ test.describe( 'Pattern Overrides', () => { const paragraphName = 'paragraph-name'; const { id } = await requestUtils.createBlock( { title: 'Pattern', - content: ` + content: `

Editable

`, status: 'publish', @@ -324,7 +324,7 @@ test.describe( 'Pattern Overrides', () => { const { id } = await requestUtils.createBlock( { title: 'Button with target', content: ` -
+ `, @@ -434,14 +434,14 @@ test.describe( 'Pattern Overrides', () => { const headingName = 'Editable heading'; const innerPattern = await requestUtils.createBlock( { title: 'Inner Pattern', - content: ` + content: `

Inner paragraph

`, status: 'publish', } ); const outerPattern = await requestUtils.createBlock( { title: 'Outer Pattern', - content: ` + content: `

Outer heading

`, @@ -535,10 +535,10 @@ test.describe( 'Pattern Overrides', () => { const paragraphName = 'Editable paragraph'; const { id } = await requestUtils.createBlock( { title: 'Pattern', - content: ` + content: `

Heading

- +

Paragraph

`, status: 'publish', @@ -694,7 +694,7 @@ test.describe( 'Pattern Overrides', () => { ); const { id } = await requestUtils.createBlock( { title: 'Pattern', - content: ` + content: `
`, status: 'publish', @@ -878,4 +878,82 @@ test.describe( 'Pattern Overrides', () => { editorSettings.getByRole( 'button', { name: 'Enable overrides' } ) ).toBeHidden(); } ); + + // @see https://github.com/WordPress/gutenberg/pull/60694 + test( 'handles back-compat from individual attributes to __default', async ( { + page, + admin, + requestUtils, + editor, + } ) => { + const imageName = 'Editable image'; + const TEST_IMAGE_FILE_PATH = path.resolve( + __dirname, + '../../../assets/10x10_e2e_test_image_z9T8jK.png' + ); + const { id } = await requestUtils.createBlock( { + title: 'Pattern', + content: ` +
+`, + status: 'publish', + } ); + + await admin.createNewPost(); + + await editor.insertBlock( { + name: 'core/block', + attributes: { ref: id }, + } ); + + const blocks = await editor.getBlocks( { full: true } ); + expect( blocks ).toMatchObject( [ + { + name: 'core/block', + attributes: { ref: id }, + }, + ] ); + expect( + await editor.getBlocks( { clientId: blocks[ 0 ].clientId } ) + ).toMatchObject( [ + { + name: 'core/image', + attributes: { + metadata: { + name: imageName, + bindings: { + __default: { + source: 'core/pattern-overrides', + }, + }, + }, + }, + }, + ] ); + + const imageBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Image', + } ); + await editor.selectBlocks( imageBlock ); + await imageBlock + .getByTestId( 'form-file-upload-input' ) + .setInputFiles( TEST_IMAGE_FILE_PATH ); + await expect( imageBlock.getByRole( 'img' ) ).toHaveCount( 1 ); + await expect( imageBlock.getByRole( 'img' ) ).toHaveAttribute( + 'src', + /\/wp-content\/uploads\// + ); + await editor.showBlockToolbar(); + await editor.clickBlockToolbarButton( 'Alt' ); + await page + .getByRole( 'textbox', { name: 'alternative text' } ) + .fill( 'Test Image' ); + + const postId = await editor.publishPost(); + + await page.goto( `/?p=${ postId }` ); + await expect( + page.getByRole( 'img', { name: 'Test Image' } ) + ).toHaveAttribute( 'src', /\/wp-content\/uploads\// ); + } ); } );