From c517e410017f4d65a7c6f03a31c5c2fa15cbbd65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Zi=C3=B3=C5=82kowski?= Date: Mon, 2 Dec 2024 13:32:53 +0100 Subject: [PATCH] Extensibility: Make Block Bindings work with `editor.BlockEdit` hook (#67370) * Block Bindings: Move the place when the attributes get overriden * Fix failing unit tests * Wrap Edit with bindings logic only when the block supports it * Extend the test plugin with `editor.BlockEdit` filter * Add a new test covering the extensibility inside inspector controls * Fix the issue with missing context established by sources Co-authored-by: gziolo Co-authored-by: SantosGuillamot --- .../src/components/block-edit/edit.js | 12 +- .../with-block-bindings-support.js} | 103 ++++-------------- .../block-list/use-block-props/index.js | 2 +- .../src/components/rich-text/index.js | 2 +- .../block-editor/src/hooks/block-bindings.js | 10 +- packages/block-editor/src/hooks/index.js | 1 - .../block-editor/src/utils/block-bindings.js | 37 +++++++ packages/e2e-tests/plugins/block-bindings.php | 6 +- .../e2e-tests/plugins/block-bindings/index.js | 45 ++++++++ .../various/block-bindings/post-meta.spec.js | 41 +++++++ 10 files changed, 165 insertions(+), 94 deletions(-) rename packages/block-editor/src/{hooks/use-bindings-attributes.js => components/block-edit/with-block-bindings-support.js} (73%) diff --git a/packages/block-editor/src/components/block-edit/edit.js b/packages/block-editor/src/components/block-edit/edit.js index 83d0e3f406f82..6b1ddd86f4c76 100644 --- a/packages/block-editor/src/components/block-edit/edit.js +++ b/packages/block-editor/src/components/block-edit/edit.js @@ -18,6 +18,8 @@ import { useContext, useMemo } from '@wordpress/element'; * Internal dependencies */ import BlockContext from '../block-context'; +import { withBlockBindingsSupport } from './with-block-bindings-support'; +import { canBindBlock } from '../../utils/block-bindings'; /** * Default value used for blocks which do not define their own context needs, @@ -47,6 +49,8 @@ const Edit = ( props ) => { const EditWithFilters = withFilters( 'editor.BlockEdit' )( Edit ); +const EditWithFiltersAndBindings = withBlockBindingsSupport( EditWithFilters ); + const EditWithGeneratedProps = ( props ) => { const { attributes = {}, name } = props; const blockType = getBlockType( name ); @@ -67,8 +71,12 @@ const EditWithGeneratedProps = ( props ) => { return null; } + const EditComponent = canBindBlock( name ) + ? EditWithFiltersAndBindings + : EditWithFilters; + if ( blockType.apiVersion > 1 ) { - return ; + return ; } // Generate a class name for the block's editable form. @@ -82,7 +90,7 @@ const EditWithGeneratedProps = ( props ) => { ); return ( - ( props ) => { const registry = useRegistry(); const blockContext = useContext( BlockContext ); @@ -108,9 +72,9 @@ export const withBlockBindingSupport = createHigherOrderComponent( () => replacePatternOverrideDefaultBindings( name, - props.attributes.metadata?.bindings + props.attributes?.metadata?.bindings ), - [ props.attributes.metadata?.bindings, name ] + [ props.attributes?.metadata?.bindings, name ] ); // While this hook doesn't directly call any selectors, `useSelect` is @@ -196,7 +160,7 @@ export const withBlockBindingSupport = createHigherOrderComponent( const hasParentPattern = !! updatedContext[ 'pattern/overrides' ]; const hasPatternOverridesDefaultBinding = - props.attributes.metadata?.bindings?.[ DEFAULT_ATTRIBUTE ] + props.attributes?.metadata?.bindings?.[ DEFAULT_ATTRIBUTE ] ?.source === 'core/pattern-overrides'; const _setAttributes = useCallback( @@ -283,40 +247,13 @@ export const withBlockBindingSupport = createHigherOrderComponent( ); return ( - <> - - + ); }, 'withBlockBindingSupport' ); - -/** - * Filters a registered block's settings to enhance a block's `edit` component - * to upgrade bound attributes. - * - * @param {WPBlockSettings} settings - Registered block settings. - * @param {string} name - Block name. - * @return {WPBlockSettings} Filtered block settings. - */ -function shimAttributeSource( settings, name ) { - if ( ! canBindBlock( name ) ) { - return settings; - } - - return { - ...settings, - edit: withBlockBindingSupport( settings.edit ), - }; -} - -addFilter( - 'blocks.registerBlockType', - 'core/editor/custom-sources-backwards-compatibility/shim-attribute-source', - shimAttributeSource -); diff --git a/packages/block-editor/src/components/block-list/use-block-props/index.js b/packages/block-editor/src/components/block-list/use-block-props/index.js index 4696149dc3875..7e50b75e1b956 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/index.js +++ b/packages/block-editor/src/components/block-list/use-block-props/index.js @@ -29,7 +29,7 @@ import { useBlockRefProvider } from './use-block-refs'; import { useIntersectionObserver } from './use-intersection-observer'; import { useScrollIntoView } from './use-scroll-into-view'; import { useFlashEditableBlocks } from '../../use-flash-editable-blocks'; -import { canBindBlock } from '../../../hooks/use-bindings-attributes'; +import { canBindBlock } from '../../../utils/block-bindings'; import { useFirefoxDraggableCompatibility } from './use-firefox-draggable-compatibility'; /** diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index bc8eca6ea94d0..768ffbb0cdd2d 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -39,7 +39,7 @@ import FormatEdit from './format-edit'; import { getAllowedFormats } from './utils'; import { Content, valueToHTMLString } from './content'; import { withDeprecations } from './with-deprecations'; -import { canBindBlock } from '../../hooks/use-bindings-attributes'; +import { canBindBlock } from '../../utils/block-bindings'; import BlockContext from '../block-context'; export const keyboardShortcutContext = createContext(); diff --git a/packages/block-editor/src/hooks/block-bindings.js b/packages/block-editor/src/hooks/block-bindings.js index e10696cc1257d..cec80dffaeaa1 100644 --- a/packages/block-editor/src/hooks/block-bindings.js +++ b/packages/block-editor/src/hooks/block-bindings.js @@ -23,15 +23,15 @@ import { useViewportMatch } from '@wordpress/compose'; /** * Internal dependencies */ -import { - canBindAttribute, - getBindableAttributes, -} from '../hooks/use-bindings-attributes'; import { unlock } from '../lock-unlock'; import InspectorControls from '../components/inspector-controls'; import BlockContext from '../components/block-context'; import { useBlockEditContext } from '../components/block-edit'; -import { useBlockBindingsUtils } from '../utils/block-bindings'; +import { + canBindAttribute, + getBindableAttributes, + useBlockBindingsUtils, +} from '../utils/block-bindings'; import { store as blockEditorStore } from '../store'; const { Menu } = unlock( componentsPrivateApis ); diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index 66ff60b691b66..7f9b29376ad1f 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -32,7 +32,6 @@ import './metadata'; import blockHooks from './block-hooks'; import blockBindingsPanel from './block-bindings'; import './block-renaming'; -import './use-bindings-attributes'; import './grid-visualizer'; createBlockEditFilter( diff --git a/packages/block-editor/src/utils/block-bindings.js b/packages/block-editor/src/utils/block-bindings.js index dcf80d985473b..82f0dff53531a 100644 --- a/packages/block-editor/src/utils/block-bindings.js +++ b/packages/block-editor/src/utils/block-bindings.js @@ -13,6 +13,43 @@ function isObjectEmpty( object ) { return ! object || Object.keys( object ).length === 0; } +export const BLOCK_BINDINGS_ALLOWED_BLOCKS = { + 'core/paragraph': [ 'content' ], + 'core/heading': [ 'content' ], + 'core/image': [ 'id', 'url', 'title', 'alt' ], + 'core/button': [ 'url', 'text', 'linkTarget', 'rel' ], +}; + +/** + * Based on the given block name, + * check if it is possible to bind the block. + * + * @param {string} blockName - The block name. + * @return {boolean} Whether it is possible to bind the block to sources. + */ +export function canBindBlock( blockName ) { + return blockName in BLOCK_BINDINGS_ALLOWED_BLOCKS; +} + +/** + * Based on the given block name and attribute name, + * check if it is possible to bind the block attribute. + * + * @param {string} blockName - The block name. + * @param {string} attributeName - The attribute name. + * @return {boolean} Whether it is possible to bind the block attribute. + */ +export function canBindAttribute( blockName, attributeName ) { + return ( + canBindBlock( blockName ) && + BLOCK_BINDINGS_ALLOWED_BLOCKS[ blockName ].includes( attributeName ) + ); +} + +export function getBindableAttributes( blockName ) { + return BLOCK_BINDINGS_ALLOWED_BLOCKS[ blockName ]; +} + /** * Contains utils to update the block `bindings` metadata. * diff --git a/packages/e2e-tests/plugins/block-bindings.php b/packages/e2e-tests/plugins/block-bindings.php index b86673c2c523d..1fd6d8468c77d 100644 --- a/packages/e2e-tests/plugins/block-bindings.php +++ b/packages/e2e-tests/plugins/block-bindings.php @@ -41,7 +41,11 @@ function gutenberg_test_block_bindings_registration() { plugins_url( 'block-bindings/index.js', __FILE__ ), array( 'wp-blocks', - 'wp-private-apis', + 'wp-block-editor', + 'wp-components', + 'wp-compose', + 'wp-element', + 'wp-hooks', ), filemtime( plugin_dir_path( __FILE__ ) . 'block-bindings/index.js' ), true diff --git a/packages/e2e-tests/plugins/block-bindings/index.js b/packages/e2e-tests/plugins/block-bindings/index.js index 5c364257caed1..63c463e197fa8 100644 --- a/packages/e2e-tests/plugins/block-bindings/index.js +++ b/packages/e2e-tests/plugins/block-bindings/index.js @@ -1,4 +1,9 @@ const { registerBlockBindingsSource } = wp.blocks; +const { InspectorControls } = wp.blockEditor; +const { PanelBody, TextControl } = wp.components; +const { createHigherOrderComponent } = wp.compose; +const { createElement: el, Fragment } = wp.element; +const { addFilter } = wp.hooks; const { fieldsList } = window.testingBindings || {}; const getValues = ( { bindings } ) => { @@ -46,3 +51,43 @@ registerBlockBindingsSource( { getValues, canUserEditValue: () => true, } ); + +const withBlockBindingsInspectorControl = createHigherOrderComponent( + ( BlockEdit ) => { + return ( props ) => { + if ( ! props.attributes?.metadata?.bindings?.content ) { + return el( BlockEdit, props ); + } + + return el( + Fragment, + {}, + el( BlockEdit, props ), + el( + InspectorControls, + {}, + el( + PanelBody, + { title: 'Bindings' }, + el( TextControl, { + __next40pxDefaultSize: true, + __nextHasNoMarginBottom: true, + label: 'Content', + value: props.attributes.content, + onChange: ( content ) => + props.setAttributes( { + content, + } ), + } ) + ) + ) + ); + }; + } +); + +addFilter( + 'editor.BlockEdit', + 'testing/bindings-inspector-control', + withBlockBindingsInspectorControl +); diff --git a/test/e2e/specs/editor/various/block-bindings/post-meta.spec.js b/test/e2e/specs/editor/various/block-bindings/post-meta.spec.js index 32334bfc777f2..318707e22f098 100644 --- a/test/e2e/specs/editor/various/block-bindings/post-meta.spec.js +++ b/test/e2e/specs/editor/various/block-bindings/post-meta.spec.js @@ -524,6 +524,47 @@ test.describe( 'Post Meta source', () => { previewPage.locator( '#connected-paragraph' ) ).toHaveText( 'new value' ); } ); + + test( 'should be possible to edit the value of the connected custom fields in the inspector control registered by the plugin', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + anchor: 'connected-paragraph', + content: 'fallback content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { + key: 'movie_field', + }, + }, + }, + }, + }, + } ); + const contentInput = page.getByRole( 'textbox', { + name: 'Content', + } ); + await expect( contentInput ).toHaveValue( + 'Movie field default value' + ); + await contentInput.fill( 'new value' ); + // Check that the paragraph content attribute didn't change. + const [ paragraphBlockObject ] = await editor.getBlocks(); + expect( paragraphBlockObject.attributes.content ).toBe( + 'fallback content' + ); + // Check the value of the custom field is being updated by visiting the frontend. + const previewPage = await editor.openPreviewPage(); + await expect( + previewPage.locator( '#connected-paragraph' ) + ).toHaveText( 'new value' ); + } ); + test( 'should be possible to connect movie fields through the attributes panel', async ( { editor, page,