From 1ba4af19e82c63f0827d036e7d38aa8f76ac3b92 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:56 +0200 Subject: [PATCH 01/60] Create bindings util for transforming block attributes --- .../block-list/use-block-props/index.js | 2 +- .../src/components/block-toolbar/index.js | 2 +- .../src/components/rich-text/index.js | 2 +- .../src/hooks/use-bindings-attributes.js | 113 +++++------------- packages/block-editor/src/utils/bindings.js | 108 +++++++++++++++++ 5 files changed, 141 insertions(+), 86 deletions(-) create mode 100644 packages/block-editor/src/utils/bindings.js 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 64e40559bb473..16ac0be3af36a 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 @@ -30,7 +30,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/bindings'; /** * This hook is used to lightly mark an element as a block element. The element diff --git a/packages/block-editor/src/components/block-toolbar/index.js b/packages/block-editor/src/components/block-toolbar/index.js index c3570c4a007f1..5dc91de198fc2 100644 --- a/packages/block-editor/src/components/block-toolbar/index.js +++ b/packages/block-editor/src/components/block-toolbar/index.js @@ -37,7 +37,7 @@ import NavigableToolbar from '../navigable-toolbar'; import Shuffle from './shuffle'; import BlockBindingsIndicator from '../block-bindings-toolbar-indicator'; import { useHasBlockToolbar } from './use-has-block-toolbar'; -import { canBindBlock } from '../../hooks/use-bindings-attributes'; +import { canBindBlock } from '../../utils/bindings'; /** * Renders the block toolbar. * diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 2a0afcf24f0dd..c32fa4a068187 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -38,7 +38,7 @@ import { getAllowedFormats } from './utils'; import { Content, valueToHTMLString } from './content'; import { withDeprecations } from './with-deprecations'; import { unlock } from '../../lock-unlock'; -import { canBindBlock } from '../../hooks/use-bindings-attributes'; +import { canBindBlock } from '../../utils/bindings'; export const keyboardShortcutContext = createContext(); export const inputEventContext = createContext(); diff --git a/packages/block-editor/src/hooks/use-bindings-attributes.js b/packages/block-editor/src/hooks/use-bindings-attributes.js index b7a4ca0379dd1..a752d9e7b1e55 100644 --- a/packages/block-editor/src/hooks/use-bindings-attributes.js +++ b/packages/block-editor/src/hooks/use-bindings-attributes.js @@ -3,7 +3,7 @@ */ import { store as blocksStore } from '@wordpress/blocks'; import { createHigherOrderComponent } from '@wordpress/compose'; -import { useRegistry, useSelect } from '@wordpress/data'; +import { useRegistry } from '@wordpress/data'; import { useCallback } from '@wordpress/element'; import { addFilter } from '@wordpress/hooks'; @@ -11,6 +11,11 @@ import { addFilter } from '@wordpress/hooks'; * Internal dependencies */ import { unlock } from '../lock-unlock'; +import { + canBindBlock, + canBindAttribute, + transformBlockAttributesWithBindingsValues, +} from '../utils/bindings'; /** @typedef {import('@wordpress/compose').WPHigherOrderComponent} WPHigherOrderComponent */ /** @typedef {import('@wordpress/blocks').WPBlockSettings} WPBlockSettings */ @@ -22,90 +27,32 @@ import { unlock } from '../lock-unlock'; * @return {WPHigherOrderComponent} Higher-order component. */ -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 const withBlockBindingSupport = createHigherOrderComponent( ( BlockEdit ) => ( props ) => { const registry = useRegistry(); - const sources = useSelect( ( select ) => - unlock( select( blocksStore ) ).getAllBlockBindingsSources() - ); - const bindings = props.attributes.metadata?.bindings; - const { name, clientId, context } = props; - const boundAttributes = useSelect( () => { - if ( ! bindings ) { - return; - } - - const attributes = {}; - - for ( const [ attributeName, boundAttribute ] of Object.entries( - bindings - ) ) { - const source = sources[ boundAttribute.source ]; - if ( - ! source?.getValue || - ! canBindAttribute( name, attributeName ) - ) { - continue; - } - - const args = { - registry, - context, - clientId, - attributeName, - args: boundAttribute.args, - }; - - attributes[ attributeName ] = source.getValue( args ); - - if ( attributes[ attributeName ] === undefined ) { - if ( attributeName === 'url' ) { - attributes[ attributeName ] = null; - } else { - attributes[ attributeName ] = - source.getPlaceholder?.( args ); - } - } - } - - return attributes; - }, [ bindings, name, clientId, context, registry, sources ] ); - - const { setAttributes } = props; + const { + attributes: blockAttributes, + name, + clientId, + context, + setAttributes, + } = props; + const bindings = blockAttributes?.metadata?.bindings; + + let newAttributes = blockAttributes; + if ( bindings ) { + newAttributes = transformBlockAttributesWithBindingsValues( + blockAttributes, + clientId, + name, + context, + registry + ); + } + const sources = unlock( + registry.select( blocksStore ) + ).getAllBlockBindingsSources(); const _setAttributes = useCallback( ( nextAttributes ) => { registry.batch( () => { @@ -190,7 +137,7 @@ export const withBlockBindingSupport = createHigherOrderComponent( <> @@ -220,6 +167,6 @@ function shimAttributeSource( settings, name ) { addFilter( 'blocks.registerBlockType', - 'core/editor/custom-sources-backwards-compatibility/shim-attribute-source', + 'core/editor/use-bindings-attributes', shimAttributeSource ); diff --git a/packages/block-editor/src/utils/bindings.js b/packages/block-editor/src/utils/bindings.js new file mode 100644 index 0000000000000..b2fc5010d01a5 --- /dev/null +++ b/packages/block-editor/src/utils/bindings.js @@ -0,0 +1,108 @@ +/** + * WordPress dependencies + */ +import { store as blocksStore } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import { unlock } from '../lock-unlock'; + +/** + * List of blocks and block attributes that can be bound. + */ +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 ) + ); +} + +/** + * Process the block attributes and replace them with the values obtained from the bindings. + * + * @param {Object} attributes - The block attributes to process. + * @param {string} clientId - The block clientId. + * @param {string} blockName - The block name. + * @param {Object} blockContext - The block context, which is needed for the binding sources. + * @param {Object} registry - The data registry. + * + * @return {Object} The new attributes object with the bindings values. + */ +export function transformBlockAttributesWithBindingsValues( + attributes, + clientId, + blockName, + blockContext, + registry +) { + const bindings = attributes?.metadata?.bindings; + if ( ! bindings ) { + return attributes; + } + + const sources = unlock( + registry.select( blocksStore ) + ).getAllBlockBindingsSources(); + + const newAttributes = { ...attributes }; + + for ( const [ attributeName, boundAttribute ] of Object.entries( + bindings + ) ) { + const source = sources[ boundAttribute.source ]; + if ( + ! source?.getValue || + ! canBindAttribute( blockName, attributeName ) + ) { + continue; + } + + const args = { + registry, + context: blockContext, + clientId, + attributeName, + args: boundAttribute.args, + }; + + newAttributes[ attributeName ] = source.getValue( args ); + + if ( newAttributes[ attributeName ] === undefined ) { + if ( attributeName === 'url' ) { + newAttributes[ attributeName ] = null; + } else { + newAttributes[ attributeName ] = + source.getPlaceholder?.( args ); + } + } + } + + return newAttributes; +} From 616eafac4eb927cd79f472ee3568460b7c2263ed Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:56 +0200 Subject: [PATCH 02/60] Get attributes and name from the store --- .../block-editor/src/hooks/use-bindings-attributes.js | 2 -- packages/block-editor/src/utils/bindings.js | 11 +++++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/block-editor/src/hooks/use-bindings-attributes.js b/packages/block-editor/src/hooks/use-bindings-attributes.js index a752d9e7b1e55..d83fcd9ac36fd 100644 --- a/packages/block-editor/src/hooks/use-bindings-attributes.js +++ b/packages/block-editor/src/hooks/use-bindings-attributes.js @@ -42,9 +42,7 @@ export const withBlockBindingSupport = createHigherOrderComponent( let newAttributes = blockAttributes; if ( bindings ) { newAttributes = transformBlockAttributesWithBindingsValues( - blockAttributes, clientId, - name, context, registry ); diff --git a/packages/block-editor/src/utils/bindings.js b/packages/block-editor/src/utils/bindings.js index b2fc5010d01a5..c77ab14ad48b1 100644 --- a/packages/block-editor/src/utils/bindings.js +++ b/packages/block-editor/src/utils/bindings.js @@ -7,6 +7,7 @@ import { store as blocksStore } from '@wordpress/blocks'; * Internal dependencies */ import { unlock } from '../lock-unlock'; +import { store as blockEditorStore } from '../store'; /** * List of blocks and block attributes that can be bound. @@ -47,26 +48,28 @@ export function canBindAttribute( blockName, attributeName ) { /** * Process the block attributes and replace them with the values obtained from the bindings. * - * @param {Object} attributes - The block attributes to process. * @param {string} clientId - The block clientId. - * @param {string} blockName - The block name. * @param {Object} blockContext - The block context, which is needed for the binding sources. * @param {Object} registry - The data registry. * * @return {Object} The new attributes object with the bindings values. */ export function transformBlockAttributesWithBindingsValues( - attributes, clientId, - blockName, blockContext, registry ) { + const attributes = registry + .select( blockEditorStore ) + .getBlockAttributes( clientId ); const bindings = attributes?.metadata?.bindings; if ( ! bindings ) { return attributes; } + const blockName = registry + .select( blockEditorStore ) + .getBlockName( clientId ); const sources = unlock( registry.select( blocksStore ) ).getAllBlockBindingsSources(); From 8bdd7015d68e09db5dc91808b55b89943fdded2f Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:56 +0200 Subject: [PATCH 03/60] Change function to return only the bound attributes --- .../src/hooks/use-bindings-attributes.js | 18 +++++-------- packages/block-editor/src/utils/bindings.js | 26 ++++++++----------- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/packages/block-editor/src/hooks/use-bindings-attributes.js b/packages/block-editor/src/hooks/use-bindings-attributes.js index d83fcd9ac36fd..4be372eec27c3 100644 --- a/packages/block-editor/src/hooks/use-bindings-attributes.js +++ b/packages/block-editor/src/hooks/use-bindings-attributes.js @@ -3,7 +3,7 @@ */ import { store as blocksStore } from '@wordpress/blocks'; import { createHigherOrderComponent } from '@wordpress/compose'; -import { useRegistry } from '@wordpress/data'; +import { useRegistry, useSelect } from '@wordpress/data'; import { useCallback } from '@wordpress/element'; import { addFilter } from '@wordpress/hooks'; @@ -14,7 +14,7 @@ import { unlock } from '../lock-unlock'; import { canBindBlock, canBindAttribute, - transformBlockAttributesWithBindingsValues, + getBoundAttributesValues, } from '../utils/bindings'; /** @typedef {import('@wordpress/compose').WPHigherOrderComponent} WPHigherOrderComponent */ @@ -39,14 +39,10 @@ export const withBlockBindingSupport = createHigherOrderComponent( } = props; const bindings = blockAttributes?.metadata?.bindings; - let newAttributes = blockAttributes; - if ( bindings ) { - newAttributes = transformBlockAttributesWithBindingsValues( - clientId, - context, - registry - ); - } + // It seems if I don't wrap this in a useSelect, the reset in pattern overrides doesn't work as expected. + const boundAttributes = useSelect( () => { + return getBoundAttributesValues( clientId, context, registry ); + }, [ clientId, context, registry ] ); const sources = unlock( registry.select( blocksStore ) @@ -135,7 +131,7 @@ export const withBlockBindingSupport = createHigherOrderComponent( <> diff --git a/packages/block-editor/src/utils/bindings.js b/packages/block-editor/src/utils/bindings.js index c77ab14ad48b1..28281b4be75c8 100644 --- a/packages/block-editor/src/utils/bindings.js +++ b/packages/block-editor/src/utils/bindings.js @@ -46,25 +46,23 @@ export function canBindAttribute( blockName, attributeName ) { } /** - * Process the block attributes and replace them with the values obtained from the bindings. + * Process the bound block attributes and return the values obtained from the bindings. * * @param {string} clientId - The block clientId. * @param {Object} blockContext - The block context, which is needed for the binding sources. * @param {Object} registry - The data registry. * - * @return {Object} The new attributes object with the bindings values. + * @return {Object} Object with the value obtained from the bindings of each bound attribute. */ -export function transformBlockAttributesWithBindingsValues( - clientId, - blockContext, - registry -) { +export function getBoundAttributesValues( clientId, blockContext, registry ) { const attributes = registry .select( blockEditorStore ) .getBlockAttributes( clientId ); const bindings = attributes?.metadata?.bindings; + const boundAttributes = {}; + if ( ! bindings ) { - return attributes; + return boundAttributes; } const blockName = registry @@ -74,8 +72,6 @@ export function transformBlockAttributesWithBindingsValues( registry.select( blocksStore ) ).getAllBlockBindingsSources(); - const newAttributes = { ...attributes }; - for ( const [ attributeName, boundAttribute ] of Object.entries( bindings ) ) { @@ -95,17 +91,17 @@ export function transformBlockAttributesWithBindingsValues( args: boundAttribute.args, }; - newAttributes[ attributeName ] = source.getValue( args ); + boundAttributes[ attributeName ] = source.getValue( args ); - if ( newAttributes[ attributeName ] === undefined ) { + if ( boundAttributes[ attributeName ] === undefined ) { if ( attributeName === 'url' ) { - newAttributes[ attributeName ] = null; + boundAttributes[ attributeName ] = null; } else { - newAttributes[ attributeName ] = + boundAttributes[ attributeName ] = source.getPlaceholder?.( args ); } } } - return newAttributes; + return boundAttributes; } From 10b6284c18f676d4c7417691f2e5720785e2ae5e Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:56 +0200 Subject: [PATCH 04/60] Add action, selector, and reducer for block context --- .../data/data-core-block-editor.md | 26 +++++++++++++++++ packages/block-editor/src/store/actions.js | 16 ++++++++++ packages/block-editor/src/store/reducer.js | 29 +++++++++++++++++++ packages/block-editor/src/store/selectors.js | 12 ++++++++ 4 files changed, 83 insertions(+) diff --git a/docs/reference-guides/data/data-core-block-editor.md b/docs/reference-guides/data/data-core-block-editor.md index 862a8b2d8a06a..3b20d474f9bc3 100644 --- a/docs/reference-guides/data/data-core-block-editor.md +++ b/docs/reference-guides/data/data-core-block-editor.md @@ -196,6 +196,19 @@ _Returns_ - `Object?`: Block attributes. +### getBlockContext + +Returns the Block Context of a block, if any exist. + +_Parameters_ + +- _state_ `Object`: Editor state. +- _clientId_ `?string`: Block client ID. + +_Returns_ + +- `?Object`: Block context of the block if set. + ### getBlockCount Returns the number of blocks currently present in the post. @@ -1839,6 +1852,19 @@ _Returns_ - `Object`: Action object. +### updateBlockContext + +Action that changes the block context of the given block(s) in the store. + +_Parameters_ + +- _clientId_ `string | SettingsByClientId`: Client ID of the block. +- _context_ `Object`: Object with the new context. + +_Returns_ + +- `Object`: Action object + ### updateBlockListSettings Action that changes the nested settings of the given block(s). diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index 74814cf22746b..83d4a6ce66f28 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -1532,6 +1532,22 @@ export function updateBlockListSettings( clientId, settings ) { }; } +/** + * Action that changes the block context of the given block(s) in the store. + * + * @param {string | SettingsByClientId} clientId Client ID of the block. + * @param {Object} context Object with the new context. + * + * @return {Object} Action object + */ +export function updateBlockContext( clientId, context ) { + return { + type: 'UPDATE_BLOCK_CONTEXT', + clientId, + context, + }; +} + /** * Action that updates the block editor settings. * diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index 7c83887876919..4b5bd606e7074 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -1786,6 +1786,34 @@ export const blockListSettings = ( state = {}, action ) => { return state; }; +/** + * Reducer returning an object where each key is a block client ID, its value + * representing the block context. + * + * @param {Object} state Current state. + * @param {Object} action Dispatched action. + * + * @return {Object} Updated state. + */ +export const blockContext = ( state = {}, action ) => { + if ( action.type === 'UPDATE_BLOCK_CONTEXT' ) { + const { clientId } = action; + if ( ! action.context ) { + return state; + } + + if ( fastDeepEqual( state[ clientId ], action.context ) ) { + return state; + } + + return { + ...state, + [ clientId ]: action.context, + }; + } + return state; +}; + /** * Reducer returning which mode is enabled. * @@ -2080,6 +2108,7 @@ const combinedReducers = combineReducers( { initialPosition, blocksMode, blockListSettings, + blockContext, insertionPoint, template, settings, diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 1c685eb4230ba..c3cf71443740d 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -2500,6 +2500,18 @@ export function getBlockListSettings( state, clientId ) { return state.blockListSettings[ clientId ]; } +/** + * Returns the Block Context of a block, if any exist. + * + * @param {Object} state Editor state. + * @param {?string} clientId Block client ID. + * + * @return {?Object} Block context of the block if set. + */ +export function getBlockContext( state, clientId ) { + return state.blockContext[ clientId ]; +} + /** * Returns the editor settings. * From faf2b09c2d938e3934dd0ca272fa678f68f2f8be Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:56 +0200 Subject: [PATCH 05/60] Sync store in edit component --- packages/block-editor/src/components/block-edit/edit.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/block-edit/edit.js b/packages/block-editor/src/components/block-edit/edit.js index 83d0e3f406f82..01fbed823013b 100644 --- a/packages/block-editor/src/components/block-edit/edit.js +++ b/packages/block-editor/src/components/block-edit/edit.js @@ -13,11 +13,13 @@ import { getBlockType, } from '@wordpress/blocks'; import { useContext, useMemo } from '@wordpress/element'; +import { useDispatch } from '@wordpress/data'; /** * Internal dependencies */ import BlockContext from '../block-context'; +import { store as blockEditorStore } from '../../store'; /** * Default value used for blocks which do not define their own context needs, @@ -48,9 +50,14 @@ const Edit = ( props ) => { const EditWithFilters = withFilters( 'editor.BlockEdit' )( Edit ); const EditWithGeneratedProps = ( props ) => { - const { attributes = {}, name } = props; + const { clientId, attributes = {}, name } = props; const blockType = getBlockType( name ); const blockContext = useContext( BlockContext ); + // Sync the block context with the block editor store. + useDispatch( blockEditorStore ).updateBlockContext( + clientId, + blockContext + ); // Assign context values using the block type's declared context needs. const context = useMemo( () => { From 65145f1b0206142af5ca221859f4eb3f689073f0 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:56 +0200 Subject: [PATCH 06/60] Revert "Sync store in edit component" This reverts commit 988c4b60b9f6f0194423e19ac28e5a7ad811ea00. --- packages/block-editor/src/components/block-edit/edit.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/block-editor/src/components/block-edit/edit.js b/packages/block-editor/src/components/block-edit/edit.js index 01fbed823013b..83d0e3f406f82 100644 --- a/packages/block-editor/src/components/block-edit/edit.js +++ b/packages/block-editor/src/components/block-edit/edit.js @@ -13,13 +13,11 @@ import { getBlockType, } from '@wordpress/blocks'; import { useContext, useMemo } from '@wordpress/element'; -import { useDispatch } from '@wordpress/data'; /** * Internal dependencies */ import BlockContext from '../block-context'; -import { store as blockEditorStore } from '../../store'; /** * Default value used for blocks which do not define their own context needs, @@ -50,14 +48,9 @@ const Edit = ( props ) => { const EditWithFilters = withFilters( 'editor.BlockEdit' )( Edit ); const EditWithGeneratedProps = ( props ) => { - const { clientId, attributes = {}, name } = props; + const { attributes = {}, name } = props; const blockType = getBlockType( name ); const blockContext = useContext( BlockContext ); - // Sync the block context with the block editor store. - useDispatch( blockEditorStore ).updateBlockContext( - clientId, - blockContext - ); // Assign context values using the block type's declared context needs. const context = useMemo( () => { From 5438b271dce8726e73bcf228dde1fedc11908573 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:56 +0200 Subject: [PATCH 07/60] Move logic to BlockContextProvider --- .../src/components/block-context/index.js | 7 +++++++ packages/block-editor/src/store/actions.js | 3 +-- packages/block-editor/src/store/reducer.js | 14 +------------- packages/block-editor/src/store/selectors.js | 11 ++++++++++- 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/packages/block-editor/src/components/block-context/index.js b/packages/block-editor/src/components/block-context/index.js index 67ca1960e71fd..bb439539885fe 100644 --- a/packages/block-editor/src/components/block-context/index.js +++ b/packages/block-editor/src/components/block-context/index.js @@ -1,8 +1,14 @@ /** * WordPress dependencies */ +import { useDispatch } from '@wordpress/data'; import { createContext, useContext, useMemo } from '@wordpress/element'; +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../store'; + /** @typedef {import('react').ReactNode} ReactNode */ /** @@ -29,6 +35,7 @@ export function BlockContextProvider( { value, children } ) { () => ( { ...context, ...value } ), [ context, value ] ); + useDispatch( blockEditorStore ).updateBlockContext( nextValue ); return ; } diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index 83d4a6ce66f28..a81c4ce9d27df 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -1540,10 +1540,9 @@ export function updateBlockListSettings( clientId, settings ) { * * @return {Object} Action object */ -export function updateBlockContext( clientId, context ) { +export function updateBlockContext( context ) { return { type: 'UPDATE_BLOCK_CONTEXT', - clientId, context, }; } diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index 4b5bd606e7074..7c436de50a476 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -1797,19 +1797,7 @@ export const blockListSettings = ( state = {}, action ) => { */ export const blockContext = ( state = {}, action ) => { if ( action.type === 'UPDATE_BLOCK_CONTEXT' ) { - const { clientId } = action; - if ( ! action.context ) { - return state; - } - - if ( fastDeepEqual( state[ clientId ], action.context ) ) { - return state; - } - - return { - ...state, - [ clientId ]: action.context, - }; + return action.context; } return state; }; diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index c3cf71443740d..c0ea4bc07f9bc 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -2509,7 +2509,16 @@ export function getBlockListSettings( state, clientId ) { * @return {?Object} Block context of the block if set. */ export function getBlockContext( state, clientId ) { - return state.blockContext[ clientId ]; + let blockContext = { ...state.blockContext }; + // TODO: Review if it's necessary to get the context from the parent blocks. + getBlockParents( state, clientId ).forEach( ( parent ) => { + const block = getBlock( state, parent ); + const blockType = getBlockType( state, block.name ); + if ( blockType?.providesContext ) { + blockContext = { ...blockContext, ...blockType?.providesContext }; + } + } ); + return blockContext; } /** From 0c75a03be995e37f82d6b54257165f2e4393db61 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:56 +0200 Subject: [PATCH 08/60] Change parent context logic --- packages/block-editor/src/store/selectors.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index c0ea4bc07f9bc..5bf5d732e498e 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -2513,9 +2513,16 @@ export function getBlockContext( state, clientId ) { // TODO: Review if it's necessary to get the context from the parent blocks. getBlockParents( state, clientId ).forEach( ( parent ) => { const block = getBlock( state, parent ); - const blockType = getBlockType( state, block.name ); + const blockType = getBlockType( block.name ); if ( blockType?.providesContext ) { - blockContext = { ...blockContext, ...blockType?.providesContext }; + Object.keys( blockType.providesContext ).forEach( + ( attributeName ) => { + blockContext = { + ...blockContext, + [ attributeName ]: block.attributes[ attributeName ], + }; + } + ); } } ); return blockContext; From b79a9520e78ae7d46b98dfde016cf26c0ae28fe3 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:56 +0200 Subject: [PATCH 09/60] Use useLayoutEffect --- .../src/components/block-context/index.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/block-editor/src/components/block-context/index.js b/packages/block-editor/src/components/block-context/index.js index bb439539885fe..0a69bdd63d135 100644 --- a/packages/block-editor/src/components/block-context/index.js +++ b/packages/block-editor/src/components/block-context/index.js @@ -2,7 +2,12 @@ * WordPress dependencies */ import { useDispatch } from '@wordpress/data'; -import { createContext, useContext, useMemo } from '@wordpress/element'; +import { + createContext, + useContext, + useLayoutEffect, + useMemo, +} from '@wordpress/element'; /** * Internal dependencies @@ -35,7 +40,12 @@ export function BlockContextProvider( { value, children } ) { () => ( { ...context, ...value } ), [ context, value ] ); - useDispatch( blockEditorStore ).updateBlockContext( nextValue ); + + // Synced the block context in the store. + const { updateBlockContext } = useDispatch( blockEditorStore ); + useLayoutEffect( () => { + updateBlockContext( nextValue ); + }, [ nextValue ] ); return ; } From 15403348d6ce428b3a406dbcb15c230f14ff78a5 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:56 +0200 Subject: [PATCH 10/60] Go back to syncing store in edit component --- .../src/components/block-context/index.js | 19 +------------------ .../src/components/block-edit/edit.js | 14 ++++++++++++-- packages/block-editor/src/store/actions.js | 3 ++- packages/block-editor/src/store/reducer.js | 14 +++++++++++++- packages/block-editor/src/store/selectors.js | 2 +- 5 files changed, 29 insertions(+), 23 deletions(-) diff --git a/packages/block-editor/src/components/block-context/index.js b/packages/block-editor/src/components/block-context/index.js index 0a69bdd63d135..67ca1960e71fd 100644 --- a/packages/block-editor/src/components/block-context/index.js +++ b/packages/block-editor/src/components/block-context/index.js @@ -1,18 +1,7 @@ /** * WordPress dependencies */ -import { useDispatch } from '@wordpress/data'; -import { - createContext, - useContext, - useLayoutEffect, - useMemo, -} from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { store as blockEditorStore } from '../../store'; +import { createContext, useContext, useMemo } from '@wordpress/element'; /** @typedef {import('react').ReactNode} ReactNode */ @@ -41,12 +30,6 @@ export function BlockContextProvider( { value, children } ) { [ context, value ] ); - // Synced the block context in the store. - const { updateBlockContext } = useDispatch( blockEditorStore ); - useLayoutEffect( () => { - updateBlockContext( nextValue ); - }, [ nextValue ] ); - return ; } diff --git a/packages/block-editor/src/components/block-edit/edit.js b/packages/block-editor/src/components/block-edit/edit.js index 83d0e3f406f82..3ec46eda10e44 100644 --- a/packages/block-editor/src/components/block-edit/edit.js +++ b/packages/block-editor/src/components/block-edit/edit.js @@ -12,12 +12,14 @@ import { hasBlockSupport, getBlockType, } from '@wordpress/blocks'; -import { useContext, useMemo } from '@wordpress/element'; +import { useDispatch } from '@wordpress/data'; +import { useContext, useLayoutEffect, useMemo } from '@wordpress/element'; /** * Internal dependencies */ import BlockContext from '../block-context'; +import { store as blockEditorStore } from '../../store'; /** * Default value used for blocks which do not define their own context needs, @@ -48,10 +50,18 @@ const Edit = ( props ) => { const EditWithFilters = withFilters( 'editor.BlockEdit' )( Edit ); const EditWithGeneratedProps = ( props ) => { - const { attributes = {}, name } = props; + const { clientId, attributes = {}, name } = props; const blockType = getBlockType( name ); const blockContext = useContext( BlockContext ); + // Sync the block context with the block editor store. + const { updateBlockContext } = useDispatch( blockEditorStore ); + useLayoutEffect( () => { + if ( blockContext && Object.keys( blockContext ).length > 0 ) { + updateBlockContext( clientId, blockContext ); + } + }, [ clientId, blockContext, updateBlockContext ] ); + // Assign context values using the block type's declared context needs. const context = useMemo( () => { return blockType && blockType.usesContext diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index a81c4ce9d27df..83d4a6ce66f28 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -1540,9 +1540,10 @@ export function updateBlockListSettings( clientId, settings ) { * * @return {Object} Action object */ -export function updateBlockContext( context ) { +export function updateBlockContext( clientId, context ) { return { type: 'UPDATE_BLOCK_CONTEXT', + clientId, context, }; } diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index 7c436de50a476..4b5bd606e7074 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -1797,7 +1797,19 @@ export const blockListSettings = ( state = {}, action ) => { */ export const blockContext = ( state = {}, action ) => { if ( action.type === 'UPDATE_BLOCK_CONTEXT' ) { - return action.context; + const { clientId } = action; + if ( ! action.context ) { + return state; + } + + if ( fastDeepEqual( state[ clientId ], action.context ) ) { + return state; + } + + return { + ...state, + [ clientId ]: action.context, + }; } return state; }; diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 5bf5d732e498e..8696838425999 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -2509,7 +2509,7 @@ export function getBlockListSettings( state, clientId ) { * @return {?Object} Block context of the block if set. */ export function getBlockContext( state, clientId ) { - let blockContext = { ...state.blockContext }; + let blockContext = { ...state.blockContext[ clientId ] }; // TODO: Review if it's necessary to get the context from the parent blocks. getBlockParents( state, clientId ).forEach( ( parent ) => { const block = getBlock( state, parent ); From 5779969720871b6e30991b985e60a0609edd7e59 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:56 +0200 Subject: [PATCH 11/60] WIP: Move bindings logic to `getBlockAttributes` --- .../data/data-core-block-editor.md | 2 + packages/block-editor/src/store/selectors.js | 92 +++++++++++++++++-- 2 files changed, 87 insertions(+), 7 deletions(-) diff --git a/docs/reference-guides/data/data-core-block-editor.md b/docs/reference-guides/data/data-core-block-editor.md index 3b20d474f9bc3..e50b10e5c2267 100644 --- a/docs/reference-guides/data/data-core-block-editor.md +++ b/docs/reference-guides/data/data-core-block-editor.md @@ -187,6 +187,8 @@ _Returns_ Returns a block's attributes given its client ID, or null if no block exists with the client ID. +Process block bindings to modify the value of the attributes if needed. + _Parameters_ - _state_ `Object`: Editor state. diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 8696838425999..2e87293ab0e8a 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -114,19 +114,97 @@ export function isBlockValid( state, clientId ) { * Returns a block's attributes given its client ID, or null if no block exists with * the client ID. * + * Process block bindings to modify the value of the attributes if needed. + * * @param {Object} state Editor state. * @param {string} clientId Block client ID. * * @return {Object?} Block attributes. */ -export function getBlockAttributes( state, clientId ) { - const block = state.blocks.byClientId.get( clientId ); - if ( ! block ) { - return null; - } +export const getBlockAttributes = createRegistrySelector( + ( select ) => ( state, clientId ) => { + // TODO: Check how to properly access the registry and the block context. + const registry = { select }; + const blockContext = {}; + const block = state.blocks.byClientId.get( clientId ); + if ( ! block ) { + return null; + } - return state.blocks.attributes.get( clientId ); -} + const blockAttributes = state.blocks.attributes.get( clientId ); + + // Change attribute values if bindings are present. + const bindings = blockAttributes?.metadata?.bindings; + if ( ! bindings ) { + return blockAttributes; + } + + const BLOCK_BINDINGS_ALLOWED_BLOCKS = { + 'core/paragraph': [ 'content' ], + 'core/heading': [ 'content' ], + 'core/image': [ 'id', 'url', 'title', 'alt' ], + 'core/button': [ 'url', 'text', 'linkTarget', 'rel' ], + }; + + function canBindBlock( blockName ) { + return blockName in BLOCK_BINDINGS_ALLOWED_BLOCKS; + } + + function canBindAttribute( blockName, attributeName ) { + return ( + canBindBlock( blockName ) && + BLOCK_BINDINGS_ALLOWED_BLOCKS[ blockName ].includes( + attributeName + ) + ); + } + + const sources = unlock( + select( blocksStore ) + ).getAllBlockBindingsSources(); + + for ( const [ attributeName, boundAttribute ] of Object.entries( + bindings + ) ) { + const source = sources[ boundAttribute.source ]; + if ( + ! source?.getValue || + ! canBindAttribute( block.name, attributeName ) + ) { + continue; + } + + const context = {}; + + if ( source.usesContext?.length ) { + for ( const key of source.usesContext ) { + context[ key ] = blockContext[ key ]; + } + } + + const args = { + registry, + context, + clientId, + attributeName, + args: boundAttribute.args, + }; + + blockAttributes[ attributeName ] = source.getValue( args ); + + if ( blockAttributes[ attributeName ] === undefined ) { + if ( attributeName === 'url' ) { + blockAttributes[ attributeName ] = null; + } else { + blockAttributes[ attributeName ] = + source.getPlaceholder?.( args ); + } + } + } + + return blockAttributes; + } +); /** * Returns a block given its client ID. This is a parsed copy of the block, From fc2b3447a76e737d533d3e2640d6505bea7bcde4 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:56 +0200 Subject: [PATCH 12/60] WIP: Move bindings setAttributes logic to updateBlockBindings --- .../data/data-core-block-editor.md | 2 + packages/block-editor/src/store/actions.js | 94 ++++++++++++++++--- 2 files changed, 85 insertions(+), 11 deletions(-) diff --git a/docs/reference-guides/data/data-core-block-editor.md b/docs/reference-guides/data/data-core-block-editor.md index e50b10e5c2267..8ef61891aa76c 100644 --- a/docs/reference-guides/data/data-core-block-editor.md +++ b/docs/reference-guides/data/data-core-block-editor.md @@ -1844,6 +1844,8 @@ _Returns_ Action that updates attributes of multiple blocks with the specified client IDs. +Process block bindings to skip updating the bound attributes and run binding source setValue instead. + _Parameters_ - _clientIds_ `string|string[]`: Block client IDs. diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index 83d4a6ce66f28..46b6b11df79d7 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -15,6 +15,7 @@ import { getBlockSupport, isUnmodifiedDefaultBlock, isUnmodifiedBlock, + store as blocksStore, } from '@wordpress/blocks'; import { speak } from '@wordpress/a11y'; import { __, _n, sprintf } from '@wordpress/i18n'; @@ -155,24 +156,95 @@ export function receiveBlocks( blocks ) { /** * Action that updates attributes of multiple blocks with the specified client IDs. * + * Process block bindings to skip updating the bound attributes and run binding source setValue instead. + * * @param {string|string[]} clientIds Block client IDs. * @param {Object} attributes Block attributes to be merged. Should be keyed by clientIds if * uniqueByBlock is true. * @param {boolean} uniqueByBlock true if each block in clientIds array has a unique set of attributes * @return {Object} Action object. */ -export function updateBlockAttributes( - clientIds, - attributes, - uniqueByBlock = false -) { - return { - type: 'UPDATE_BLOCK_ATTRIBUTES', - clientIds: castArray( clientIds ), - attributes, - uniqueByBlock, +export const updateBlockAttributes = + ( clientIds, attributes, uniqueByBlock = false ) => + ( { dispatch, registry } ) => { + const keptAttributes = { ...attributes }; + for ( const clientId of clientIds ) { + const { metadata: { bindings } = {} } = registry + .select( STORE_NAME ) + .getBlockAttributes( clientId ); + + if ( ! bindings ) { + continue; + } + + const sources = unlock( + registry.select( blocksStore ) + ).getAllBlockBindingsSources(); + + const updatesBySource = new Map(); + + const attributeEntries = Object.entries( + uniqueByBlock ? attributes[ clientId ] : attributes + ); + + attributeEntries.forEach( ( [ attributeName, newValue ] ) => { + if ( ! bindings[ attributeName ] ) { + return; + } + + const source = sources[ bindings[ attributeName ].source ]; + if ( ! source?.setValue && ! source?.setValues ) { + return; + } + updatesBySource.set( source, { + ...updatesBySource.get( source ), + [ attributeName ]: newValue, + } ); + + // Skip attribute. + if ( uniqueByBlock ) { + delete keptAttributes[ clientId ][ attributeName ]; + } else { + delete keptAttributes[ attributeName ]; + } + } ); + + if ( updatesBySource.size ) { + // TODO: Access the block context. + const context = {}; + for ( const [ source, boundAttributes ] of updatesBySource ) { + if ( source.setValues ) { + source.setValues( { + registry, + context, + clientId, + attributes: boundAttributes, + } ); + } else { + for ( const [ attributeName, value ] of Object.entries( + boundAttributes + ) ) { + source.setValue( { + registry, + context, + clientId, + attributeName, + args: bindings[ attributeName ].args, + value, + } ); + } + } + } + } + } + + dispatch( { + type: 'UPDATE_BLOCK_ATTRIBUTES', + clientIds: castArray( clientIds ), + attributes: keptAttributes, + uniqueByBlock, + } ); }; -} /** * Action that updates the block with the specified client ID. From 40bfba44c671418d2db248a545b5d1ab5c17bea3 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:56 +0200 Subject: [PATCH 13/60] Pass only `select` to `getValue` functions --- packages/block-editor/src/store/selectors.js | 5 ++--- packages/editor/src/bindings/pattern-overrides.js | 4 ++-- packages/editor/src/bindings/post-meta.js | 13 +++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 2e87293ab0e8a..8a92f7c0153dc 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -123,8 +123,7 @@ export function isBlockValid( state, clientId ) { */ export const getBlockAttributes = createRegistrySelector( ( select ) => ( state, clientId ) => { - // TODO: Check how to properly access the registry and the block context. - const registry = { select }; + // TODO: Check how to properly access the block context. const blockContext = {}; const block = state.blocks.byClientId.get( clientId ); if ( ! block ) { @@ -183,7 +182,7 @@ export const getBlockAttributes = createRegistrySelector( } const args = { - registry, + select, context, clientId, attributeName, diff --git a/packages/editor/src/bindings/pattern-overrides.js b/packages/editor/src/bindings/pattern-overrides.js index 4065cefe36280..e4be293246f35 100644 --- a/packages/editor/src/bindings/pattern-overrides.js +++ b/packages/editor/src/bindings/pattern-overrides.js @@ -9,9 +9,9 @@ const CONTENT = 'content'; export default { name: 'core/pattern-overrides', label: _x( 'Pattern Overrides', 'block bindings source' ), - getValue( { registry, clientId, attributeName } ) { + getValue( { select, clientId, attributeName } ) { const { getBlockAttributes, getBlockParentsByBlockName } = - registry.select( blockEditorStore ); + select( blockEditorStore ); const currentBlockAttributes = getBlockAttributes( clientId ); const [ patternClientId ] = getBlockParentsByBlockName( clientId, diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index f5b3b526dbfd4..1f138e627525a 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -15,14 +15,15 @@ export default { getPlaceholder( { args } ) { return args.key; }, - getValue( { registry, context, args } ) { + getValue( { select, context, args } ) { const postType = context.postType ? context.postType - : registry.select( editorStore ).getCurrentPostType(); + : select( editorStore ).getCurrentPostType(); - return registry - .select( coreDataStore ) - .getEditedEntityRecord( 'postType', postType, context.postId ) - .meta?.[ args.key ]; + return select( coreDataStore ).getEditedEntityRecord( + 'postType', + postType, + context.postId + ).meta?.[ args.key ]; }, }; From 467db7ec6e7833a6b877d0b335effe44e124ecc6 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:56 +0200 Subject: [PATCH 14/60] Remove old editor hook --- packages/block-editor/src/hooks/index.js | 1 - packages/block-editor/src/store/selectors.js | 21 +------------------- 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index 89e6819c1d031..8b871f5a1d76f 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'; createBlockEditFilter( [ diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 8a92f7c0153dc..270716f883f6d 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -36,6 +36,7 @@ import { getTemporarilyEditingAsBlocks, getTemporarilyEditingFocusModeToRevert, } from './private-selectors'; +import { canBindAttribute } from '../utils/bindings'; /** * A block selection object. @@ -138,26 +139,6 @@ export const getBlockAttributes = createRegistrySelector( return blockAttributes; } - const BLOCK_BINDINGS_ALLOWED_BLOCKS = { - 'core/paragraph': [ 'content' ], - 'core/heading': [ 'content' ], - 'core/image': [ 'id', 'url', 'title', 'alt' ], - 'core/button': [ 'url', 'text', 'linkTarget', 'rel' ], - }; - - function canBindBlock( blockName ) { - return blockName in BLOCK_BINDINGS_ALLOWED_BLOCKS; - } - - function canBindAttribute( blockName, attributeName ) { - return ( - canBindBlock( blockName ) && - BLOCK_BINDINGS_ALLOWED_BLOCKS[ blockName ].includes( - attributeName - ) - ); - } - const sources = unlock( select( blocksStore ) ).getAllBlockBindingsSources(); From 88333f88059b8146f61f03035e6aa4f29e8cb797 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:56 +0200 Subject: [PATCH 15/60] Add fallback to postId until context is ready --- packages/editor/src/bindings/post-meta.js | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index 1f138e627525a..92698b42bbbdc 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -12,10 +12,14 @@ import { store as editorStore } from '../store'; export default { name: 'core/post-meta', label: _x( 'Post Meta', 'block bindings source' ), + usesContext: [ 'postId', 'postType' ], getPlaceholder( { args } ) { return args.key; }, getValue( { select, context, args } ) { + const postId = context.postId + ? context.postId + : select( editorStore ).getCurrentPostId(); const postType = context.postType ? context.postType : select( editorStore ).getCurrentPostType(); @@ -23,7 +27,23 @@ export default { return select( coreDataStore ).getEditedEntityRecord( 'postType', postType, - context.postId + postId ).meta?.[ args.key ]; }, + setValue( { registry, context, args, value } ) { + const postId = context.postId + ? context.postId + : registry.select( editorStore ).getCurrentPostId(); + const postType = context.postType + ? context.postType + : registry.select( editorStore ).getCurrentPostType(); + registry + .dispatch( coreDataStore ) + .editEntityRecord( 'postType', postType, postId, { + meta: { + [ args.key ]: value, + }, + } ); + }, + lockAttributesEditing: () => false, }; From c35eca1468c13c3fac9aaf455960380063c94c6e Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:56 +0200 Subject: [PATCH 16/60] Remove setValue post-meta code --- packages/editor/src/bindings/post-meta.js | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index 92698b42bbbdc..9ba06c7304a21 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -30,20 +30,4 @@ export default { postId ).meta?.[ args.key ]; }, - setValue( { registry, context, args, value } ) { - const postId = context.postId - ? context.postId - : registry.select( editorStore ).getCurrentPostId(); - const postType = context.postType - ? context.postType - : registry.select( editorStore ).getCurrentPostType(); - registry - .dispatch( coreDataStore ) - .editEntityRecord( 'postType', postType, postId, { - meta: { - [ args.key ]: value, - }, - } ); - }, - lockAttributesEditing: () => false, }; From 187480ccebf8e49893eb76b34a44325af327385c Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:56 +0200 Subject: [PATCH 17/60] Simplify fallback conditional --- packages/editor/src/bindings/post-meta.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index 9ba06c7304a21..9071edf98c158 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -17,12 +17,10 @@ export default { return args.key; }, getValue( { select, context, args } ) { - const postId = context.postId - ? context.postId - : select( editorStore ).getCurrentPostId(); - const postType = context.postType - ? context.postType - : select( editorStore ).getCurrentPostType(); + const postId = + context?.postId || select( editorStore ).getCurrentPostId(); + const postType = + context?.postType || select( editorStore ).getCurrentPostType(); return select( coreDataStore ).getEditedEntityRecord( 'postType', From 49d438adf7dd49de99d9d0ab8d6f8ea941cea2a4 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:56 +0200 Subject: [PATCH 18/60] Change bindings destructuring --- packages/block-editor/src/store/actions.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index 46b6b11df79d7..f5f9166f5bc34 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -169,9 +169,10 @@ export const updateBlockAttributes = ( { dispatch, registry } ) => { const keptAttributes = { ...attributes }; for ( const clientId of clientIds ) { - const { metadata: { bindings } = {} } = registry + const blockAttributes = registry .select( STORE_NAME ) .getBlockAttributes( clientId ); + const bindings = blockAttributes?.metadata?.bindings; if ( ! bindings ) { continue; From b8b4af84b9bccef647c67d93ef0a1f74171a177d Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:56 +0200 Subject: [PATCH 19/60] Check canBindAttribute in updateBlockAttributes --- packages/block-editor/src/store/actions.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index f5f9166f5bc34..28ff3172181ce 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -25,6 +25,7 @@ import deprecated from '@wordpress/deprecated'; /** * Internal dependencies */ +import { canBindAttribute } from '../utils/bindings'; import { retrieveSelectedAttribute, findRichTextAttributeKey, @@ -169,10 +170,8 @@ export const updateBlockAttributes = ( { dispatch, registry } ) => { const keptAttributes = { ...attributes }; for ( const clientId of clientIds ) { - const blockAttributes = registry - .select( STORE_NAME ) - .getBlockAttributes( clientId ); - const bindings = blockAttributes?.metadata?.bindings; + const block = registry.select( STORE_NAME ).getBlock( clientId ); + const bindings = block.attributes?.metadata?.bindings; if ( ! bindings ) { continue; @@ -189,7 +188,10 @@ export const updateBlockAttributes = ); attributeEntries.forEach( ( [ attributeName, newValue ] ) => { - if ( ! bindings[ attributeName ] ) { + if ( + ! bindings[ attributeName ] || + ! canBindAttribute( block.name, attributeName ) + ) { return; } From 711d9a677d74ebad4d7cf1cb589eaaef926eabf7 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:56 +0200 Subject: [PATCH 20/60] Update unit tests to expect a dispatch --- .../block-editor/src/store/test/actions.js | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/packages/block-editor/src/store/test/actions.js b/packages/block-editor/src/store/test/actions.js index f960363cdb0dd..9acabbca9b35b 100644 --- a/packages/block-editor/src/store/test/actions.js +++ b/packages/block-editor/src/store/test/actions.js @@ -81,11 +81,25 @@ describe( 'actions', () => { } ); describe( 'updateBlockAttributes', () => { - it( 'should return the UPDATE_BLOCK_ATTRIBUTES action (string)', () => { + let dispatch, registry; + beforeEach( () => { + dispatch = jest.fn(); + registry = createRegistry(); + registry.registerStore( blockEditorStoreName, { + actions, + selectors, + reducer, + } ); + } ); + + it( 'should dispatch the UPDATE_BLOCK_ATTRIBUTES action (string)', () => { const clientId = 'myclientid'; const attributes = {}; - const result = updateBlockAttributes( clientId, attributes ); - expect( result ).toEqual( { + updateBlockAttributes( + clientId, + attributes + )( { dispatch, registry } ); + expect( dispatch ).toHaveBeenCalledWith( { type: 'UPDATE_BLOCK_ATTRIBUTES', clientIds: [ clientId ], attributes, @@ -93,11 +107,14 @@ describe( 'actions', () => { } ); } ); - it( 'should return the UPDATE_BLOCK_ATTRIBUTES action (array)', () => { + it( 'should dispatch the UPDATE_BLOCK_ATTRIBUTES action (array)', () => { const clientIds = [ 'myclientid' ]; const attributes = {}; - const result = updateBlockAttributes( clientIds, attributes ); - expect( result ).toEqual( { + updateBlockAttributes( + clientIds, + attributes + )( { dispatch, registry } ); + expect( dispatch ).toHaveBeenCalledWith( { type: 'UPDATE_BLOCK_ATTRIBUTES', clientIds, attributes, From 44daa6c02230e18a69e85c0e129659244efedcd3 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:56 +0200 Subject: [PATCH 21/60] Add conditional in block --- packages/block-editor/src/store/actions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index 28ff3172181ce..017e5596360a9 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -171,7 +171,7 @@ export const updateBlockAttributes = const keptAttributes = { ...attributes }; for ( const clientId of clientIds ) { const block = registry.select( STORE_NAME ).getBlock( clientId ); - const bindings = block.attributes?.metadata?.bindings; + const bindings = block?.attributes?.metadata?.bindings; if ( ! bindings ) { continue; From 03f4805ed8a56becbc85675d6e5c1fb1c01adaf6 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:57 +0200 Subject: [PATCH 22/60] Don't use `getBlockAttributes` inside `getValue` --- packages/editor/src/bindings/pattern-overrides.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/editor/src/bindings/pattern-overrides.js b/packages/editor/src/bindings/pattern-overrides.js index e4be293246f35..3a74113462e67 100644 --- a/packages/editor/src/bindings/pattern-overrides.js +++ b/packages/editor/src/bindings/pattern-overrides.js @@ -10,9 +10,9 @@ export default { name: 'core/pattern-overrides', label: _x( 'Pattern Overrides', 'block bindings source' ), getValue( { select, clientId, attributeName } ) { - const { getBlockAttributes, getBlockParentsByBlockName } = + const { getBlock, getBlockParentsByBlockName } = select( blockEditorStore ); - const currentBlockAttributes = getBlockAttributes( clientId ); + const { attributes: currentBlockAttributes } = getBlock( clientId ); const [ patternClientId ] = getBlockParentsByBlockName( clientId, 'core/block', @@ -20,7 +20,7 @@ export default { ); const overridableValue = - getBlockAttributes( patternClientId )?.[ CONTENT ]?.[ + getBlock( patternClientId )?.attributes?.[ CONTENT ]?.[ currentBlockAttributes?.metadata?.name ]?.[ attributeName ]; From 7dda2e9b77c31891ebf7192a9c974296d52d7def Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:57 +0200 Subject: [PATCH 23/60] Revert "Don't use `getBlockAttributes` inside `getValue`" This reverts commit 0e91129ee1b9fa7cf34db6567b9dd70e1d6599c6. --- packages/editor/src/bindings/pattern-overrides.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/editor/src/bindings/pattern-overrides.js b/packages/editor/src/bindings/pattern-overrides.js index 3a74113462e67..e4be293246f35 100644 --- a/packages/editor/src/bindings/pattern-overrides.js +++ b/packages/editor/src/bindings/pattern-overrides.js @@ -10,9 +10,9 @@ export default { name: 'core/pattern-overrides', label: _x( 'Pattern Overrides', 'block bindings source' ), getValue( { select, clientId, attributeName } ) { - const { getBlock, getBlockParentsByBlockName } = + const { getBlockAttributes, getBlockParentsByBlockName } = select( blockEditorStore ); - const { attributes: currentBlockAttributes } = getBlock( clientId ); + const currentBlockAttributes = getBlockAttributes( clientId ); const [ patternClientId ] = getBlockParentsByBlockName( clientId, 'core/block', @@ -20,7 +20,7 @@ export default { ); const overridableValue = - getBlock( patternClientId )?.attributes?.[ CONTENT ]?.[ + getBlockAttributes( patternClientId )?.[ CONTENT ]?.[ currentBlockAttributes?.metadata?.name ]?.[ attributeName ]; From ff84c40ccf4b446328850e72321094dde2061412 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:57 +0200 Subject: [PATCH 24/60] Avoid processing bindings recursively --- packages/block-editor/src/store/selectors.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 270716f883f6d..68e0523a6e704 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -111,6 +111,13 @@ export function isBlockValid( state, clientId ) { return !! block && block.isValid; } +/** + * Variable to avoid processing bindings recursively. + * + * @type {boolean} + */ +let isProcessingBindings; + /** * Returns a block's attributes given its client ID, or null if no block exists with * the client ID. @@ -135,9 +142,10 @@ export const getBlockAttributes = createRegistrySelector( // Change attribute values if bindings are present. const bindings = blockAttributes?.metadata?.bindings; - if ( ! bindings ) { + if ( ! bindings || isProcessingBindings ) { return blockAttributes; } + isProcessingBindings = true; const sources = unlock( select( blocksStore ) @@ -182,6 +190,7 @@ export const getBlockAttributes = createRegistrySelector( } } + isProcessingBindings = false; return blockAttributes; } ); From 037dd3700098103dfdba9457566daf20a30a146f Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:57 +0200 Subject: [PATCH 25/60] Access context through selector --- packages/block-editor/src/store/actions.js | 5 +++-- packages/block-editor/src/store/selectors.js | 12 ++---------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index 017e5596360a9..ddd1ca9ad0b19 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -213,8 +213,9 @@ export const updateBlockAttributes = } ); if ( updatesBySource.size ) { - // TODO: Access the block context. - const context = {}; + const context = registry + .select( STORE_NAME ) + .getBlockContext( clientId ); for ( const [ source, boundAttributes ] of updatesBySource ) { if ( source.setValues ) { source.setValues( { diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 68e0523a6e704..257a8a4222aab 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -131,8 +131,6 @@ let isProcessingBindings; */ export const getBlockAttributes = createRegistrySelector( ( select ) => ( state, clientId ) => { - // TODO: Check how to properly access the block context. - const blockContext = {}; const block = state.blocks.byClientId.get( clientId ); if ( ! block ) { return null; @@ -147,6 +145,8 @@ export const getBlockAttributes = createRegistrySelector( } isProcessingBindings = true; + const context = select( STORE_NAME ).getBlockContext( clientId ); + const sources = unlock( select( blocksStore ) ).getAllBlockBindingsSources(); @@ -162,14 +162,6 @@ export const getBlockAttributes = createRegistrySelector( continue; } - const context = {}; - - if ( source.usesContext?.length ) { - for ( const key of source.usesContext ) { - context[ key ] = blockContext[ key ]; - } - } - const args = { select, context, From 747bedee506a0447a7c2d910216db826acfad628 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:57 +0200 Subject: [PATCH 26/60] Update getBlockAttributes logic --- packages/block-editor/src/store/selectors.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 257a8a4222aab..979ee829a8ec9 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -145,8 +145,8 @@ export const getBlockAttributes = createRegistrySelector( } isProcessingBindings = true; + const newAttributes = { ...blockAttributes }; const context = select( STORE_NAME ).getBlockContext( clientId ); - const sources = unlock( select( blocksStore ) ).getAllBlockBindingsSources(); @@ -170,20 +170,20 @@ export const getBlockAttributes = createRegistrySelector( args: boundAttribute.args, }; - blockAttributes[ attributeName ] = source.getValue( args ); + newAttributes[ attributeName ] = source.getValue( args ); - if ( blockAttributes[ attributeName ] === undefined ) { + if ( newAttributes[ attributeName ] === undefined ) { if ( attributeName === 'url' ) { - blockAttributes[ attributeName ] = null; + newAttributes[ attributeName ] = null; } else { - blockAttributes[ attributeName ] = + newAttributes[ attributeName ] = source.getPlaceholder?.( args ); } } } isProcessingBindings = false; - return blockAttributes; + return newAttributes; } ); From cb98f8485116f313f18f94b9ed43bba83a326949 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:57 +0200 Subject: [PATCH 27/60] Don't use fallbacks --- packages/editor/src/bindings/post-meta.js | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index 9071edf98c158..002e99b475878 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -4,28 +4,17 @@ import { store as coreDataStore } from '@wordpress/core-data'; import { _x } from '@wordpress/i18n'; -/** - * Internal dependencies - */ -import { store as editorStore } from '../store'; - export default { name: 'core/post-meta', label: _x( 'Post Meta', 'block bindings source' ), - usesContext: [ 'postId', 'postType' ], getPlaceholder( { args } ) { return args.key; }, getValue( { select, context, args } ) { - const postId = - context?.postId || select( editorStore ).getCurrentPostId(); - const postType = - context?.postType || select( editorStore ).getCurrentPostType(); - return select( coreDataStore ).getEditedEntityRecord( 'postType', - postType, - postId + context?.postType, + context?.postId ).meta?.[ args.key ]; }, }; From 827d8b439801babfc5ef3d27e5dfd60b452753b8 Mon Sep 17 00:00:00 2001 From: Carlos Bravo Date: Thu, 30 May 2024 10:44:57 +0200 Subject: [PATCH 28/60] Add edit value posibility for post meta, add function to check if is admin --- packages/editor/src/bindings/post-meta.js | 21 +++++++++++++++++++++ packages/editor/src/store/selectors.js | 4 ++++ 2 files changed, 25 insertions(+) diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index 002e99b475878..9c03009bc87b6 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -4,6 +4,12 @@ import { store as coreDataStore } from '@wordpress/core-data'; import { _x } from '@wordpress/i18n'; +/** + * Internal dependencies + */ +import { store as editorStore } from '../store'; +import { __experimentalIsAdminUser } from '../store/selectors'; + export default { name: 'core/post-meta', label: _x( 'Post Meta', 'block bindings source' ), @@ -17,4 +23,19 @@ export default { context?.postId ).meta?.[ args.key ]; }, + setValue( { registry, context, args, value } ) { + const postType = context.postType + ? context.postType + : registry.select( editorStore ).getCurrentPostType(); + registry + .dispatch( coreDataStore ) + .editEntityRecord( 'postType', postType, context.postId, { + meta: { + [ args.key ]: value, + }, + } ); + }, + lockAttributesEditing() { + return ! __experimentalIsAdminUser(); + }, }; diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 8b0dfd4b370c4..0cba5ff8b991b 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -1796,3 +1796,7 @@ export const getPostTypeLabel = createRegistrySelector( export function isPublishSidebarOpened( state ) { return state.publishSidebarActive; } + +export const __experimentalIsAdminUser = createRegistrySelector( + ( select ) => () => select( coreStore ).canUser( 'delete', 'settings' ) +); From 5ac4874bf954aafe052d8209219a0780c9fd479a Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:57 +0200 Subject: [PATCH 29/60] Revert "Add edit value posibility for post meta, add function to check if is admin" This reverts commit 9659455eed4343c455760b2f9076fe3a5bdbf2d7. --- packages/editor/src/bindings/post-meta.js | 16 ---------------- packages/editor/src/store/selectors.js | 4 ---- 2 files changed, 20 deletions(-) diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index 9c03009bc87b6..42252a9099af9 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -8,7 +8,6 @@ import { _x } from '@wordpress/i18n'; * Internal dependencies */ import { store as editorStore } from '../store'; -import { __experimentalIsAdminUser } from '../store/selectors'; export default { name: 'core/post-meta', @@ -23,19 +22,4 @@ export default { context?.postId ).meta?.[ args.key ]; }, - setValue( { registry, context, args, value } ) { - const postType = context.postType - ? context.postType - : registry.select( editorStore ).getCurrentPostType(); - registry - .dispatch( coreDataStore ) - .editEntityRecord( 'postType', postType, context.postId, { - meta: { - [ args.key ]: value, - }, - } ); - }, - lockAttributesEditing() { - return ! __experimentalIsAdminUser(); - }, }; diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 0cba5ff8b991b..8b0dfd4b370c4 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -1796,7 +1796,3 @@ export const getPostTypeLabel = createRegistrySelector( export function isPublishSidebarOpened( state ) { return state.publishSidebarActive; } - -export const __experimentalIsAdminUser = createRegistrySelector( - ( select ) => () => select( coreStore ).canUser( 'delete', 'settings' ) -); From 361273a5d974c52852e521599d622060532d4ca7 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:57 +0200 Subject: [PATCH 30/60] Test editing is allowed in paragraph block --- .../editor/various/block-bindings.spec.js | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/test/e2e/specs/editor/various/block-bindings.spec.js b/test/e2e/specs/editor/various/block-bindings.spec.js index 97b8579bb07ba..e99e73d858450 100644 --- a/test/e2e/specs/editor/various/block-bindings.spec.js +++ b/test/e2e/specs/editor/various/block-bindings.spec.js @@ -1342,6 +1342,45 @@ test.describe( 'Block bindings', () => { await expect( newEmptyParagraph ).toHaveText( '' ); await expect( newEmptyParagraph ).toBeEditable(); } ); + + test( 'should be possible to edit the value of the custom field from the paragraph', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + anchor: 'paragraph-binding', + content: 'paragraph default content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + + await expect( paragraphBlock ).toHaveText( + 'Value of the text_custom_field' + ); + await expect( paragraphBlock ).toBeEditable(); + await paragraphBlock.fill( 'new value' ); + // Check that the paragraph content attribute didn't change. + const [ paragraphBlockObject ] = await editor.getBlocks(); + expect( paragraphBlockObject.attributes.content ).toBe( + 'paragraph default content' + ); + // Check the value of the custom field is being updated by visiting the frontend. + const previewPage = await editor.openPreviewPage(); + await expect( + previewPage.locator( '#paragraph-binding' ) + ).toHaveText( 'new value' ); + } ); } ); test.describe( 'Heading', () => { From 892f501d4bdd495c87a661bb174492b5f0d5d80a Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:57 +0200 Subject: [PATCH 31/60] Test protected fields are not editable --- .../editor/various/block-bindings.spec.js | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/test/e2e/specs/editor/various/block-bindings.spec.js b/test/e2e/specs/editor/various/block-bindings.spec.js index e99e73d858450..658c11f3b709b 100644 --- a/test/e2e/specs/editor/various/block-bindings.spec.js +++ b/test/e2e/specs/editor/various/block-bindings.spec.js @@ -1381,6 +1381,64 @@ test.describe( 'Block bindings', () => { previewPage.locator( '#paragraph-binding' ) ).toHaveText( 'new value' ); } ); + + test( 'should NOT be possible to edit the value of the custom field when it is protected', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + anchor: 'protected-field-binding', + content: 'fallback value', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { key: '_protected_field' }, + }, + }, + }, + }, + } ); + + const protectedFieldBlock = editor.canvas.getByRole( + 'document', + { + name: 'Block: Paragraph', + } + ); + + await expect( protectedFieldBlock ).not.toBeEditable(); + } ); + + test( 'should NOT be possible to edit the value of the custom field when it is not shown in the REST API', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + anchor: 'show-in-rest-false-binding', + content: 'fallback value', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { key: 'show_in_rest_false_field' }, + }, + }, + }, + }, + } ); + + const showInRestFalseBlock = editor.canvas.getByRole( + 'document', + { + name: 'Block: Paragraph', + } + ); + + await expect( showInRestFalseBlock ).not.toBeEditable(); + } ); } ); test.describe( 'Heading', () => { From da6ca3465dcd3ffdc31e2760ab698c15c9c8e92c Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:57 +0200 Subject: [PATCH 32/60] Revert "Enable parallel processing for PHPCS sniffs (#61700)" This reverts commit 8331820af51afb6d834b4d3e395a4adc8d993244. --- phpcs.xml.dist | 3 --- 1 file changed, 3 deletions(-) diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 5bc7a5676e712..45d742a498c65 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -42,9 +42,6 @@ - - - ./bin ./gutenberg.php ./lib From 98bb6c92d398d810768b19933d5af25278d9336d Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:57 +0200 Subject: [PATCH 33/60] Add post meta setValue function --- packages/editor/src/bindings/post-meta.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index 42252a9099af9..c10ffa04656ce 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -22,4 +22,25 @@ export default { context?.postId ).meta?.[ args.key ]; }, + setValue( { registry, context, args, value } ) { + const postType = context.postType + ? context.postType + : registry.select( editorStore ).getCurrentPostType(); + registry + .dispatch( coreDataStore ) + .editEntityRecord( 'postType', postType, context.postId, { + meta: { + [ args.key ]: value, + }, + } ); + }, + lockAttributesEditing() { + // TODO: Check that editing is happening in the post editor and not a template. + + // TODO: Check that the custom field is not protected and available in the REST API. + + // TODO: Check that the user has the capability to edit post meta. + + return false; + }, }; From 11b35f99c1988ec6bae06aa5e4156eb08810d029 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:57 +0200 Subject: [PATCH 34/60] Update tests to check contenteditable --- test/e2e/specs/editor/various/block-bindings.spec.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/e2e/specs/editor/various/block-bindings.spec.js b/test/e2e/specs/editor/various/block-bindings.spec.js index 658c11f3b709b..9f7ba4b5a3587 100644 --- a/test/e2e/specs/editor/various/block-bindings.spec.js +++ b/test/e2e/specs/editor/various/block-bindings.spec.js @@ -1408,7 +1408,10 @@ test.describe( 'Block bindings', () => { } ); - await expect( protectedFieldBlock ).not.toBeEditable(); + await expect( protectedFieldBlock ).toHaveAttribute( + 'contenteditable', + 'false' + ); } ); test( 'should NOT be possible to edit the value of the custom field when it is not shown in the REST API', async ( { @@ -1437,7 +1440,10 @@ test.describe( 'Block bindings', () => { } ); - await expect( showInRestFalseBlock ).not.toBeEditable(); + await expect( showInRestFalseBlock ).toHaveAttribute( + 'contenteditable', + 'false' + ); } ); } ); From 1b7ca4515260e30d7e1588736b912c85d6eff3db Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:57 +0200 Subject: [PATCH 35/60] Update lockAttributesEditing default --- packages/blocks/src/store/reducer.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/blocks/src/store/reducer.js b/packages/blocks/src/store/reducer.js index 1d0d8cb2e968f..e7d2f553a8069 100644 --- a/packages/blocks/src/store/reducer.js +++ b/packages/blocks/src/store/reducer.js @@ -381,10 +381,8 @@ export function blockBindingsSources( state = {}, action ) { setValue: action.setValue, setValues: action.setValues, getPlaceholder: action.getPlaceholder, - lockAttributesEditing: () => - action.lockAttributesEditing - ? action.lockAttributesEditing() - : true, + lockAttributesEditing: + action.lockAttributesEditing || ( () => true ), }, }; } From 46020f09b07348866bbc91c77bc0a322ef1a7b04 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:57 +0200 Subject: [PATCH 36/60] Pass arguments to lockAttributesEditing --- .../src/components/rich-text/index.js | 9 ++++++--- packages/block-library/src/button/edit.js | 7 ++++++- packages/block-library/src/image/edit.js | 6 +++++- packages/block-library/src/image/image.js | 18 +++++++++++++++--- 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index c32fa4a068187..cee77baba7847 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -170,7 +170,7 @@ export function RichTextWrapper( const { getBlockBindingsSource } = unlock( select( blocksStore ) ); - for ( const [ attribute, args ] of Object.entries( + for ( const [ attribute, binding ] of Object.entries( blockBindings ) ) { if ( @@ -182,11 +182,14 @@ export function RichTextWrapper( // If the source is not defined, or if its value of `lockAttributesEditing` is `true`, disable it. const blockBindingsSource = getBlockBindingsSource( - args.source + binding.source ); if ( ! blockBindingsSource || - blockBindingsSource.lockAttributesEditing() + blockBindingsSource.lockAttributesEditing( { + select, + args: binding.args, + } ) ) { _disableBoundBlocks = true; break; diff --git a/packages/block-library/src/button/edit.js b/packages/block-library/src/button/edit.js index b6c4095ca82a6..1ae11af9ddde8 100644 --- a/packages/block-library/src/button/edit.js +++ b/packages/block-library/src/button/edit.js @@ -156,6 +156,7 @@ function ButtonEdit( props ) { onReplace, mergeBlocks, clientId, + context, } = props; const { tagName, @@ -247,7 +248,11 @@ function ButtonEdit( props ) { lockUrlControls: !! metadata?.bindings?.url && ( ! blockBindingsSource || - blockBindingsSource?.lockAttributesEditing() ), + blockBindingsSource?.lockAttributesEditing( { + select, + context, + args: metadata?.bindings?.url?.args, + } ) ), }; }, [ isSelected, metadata?.bindings?.url ] diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index 6ff392555a93a..a0bc9f67b6466 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -318,7 +318,11 @@ export function ImageEdit( { lockUrlControls: !! metadata?.bindings?.url && ( ! blockBindingsSource || - blockBindingsSource?.lockAttributesEditing() ), + blockBindingsSource?.lockAttributesEditing( { + select, + context, + args: metadata?.bindings?.url?.args, + } ) ), lockUrlControlsMessage: blockBindingsSource?.label ? sprintf( /* translators: %s: Label of the bindings source. */ diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js index fc1199bb40fb0..40ab680074451 100644 --- a/packages/block-library/src/image/image.js +++ b/packages/block-library/src/image/image.js @@ -463,7 +463,11 @@ export default function Image( { lockUrlControls: !! urlBinding && ( ! urlBindingSource || - urlBindingSource?.lockAttributesEditing() ), + urlBindingSource?.lockAttributesEditing( { + select, + context, + args: urlBinding?.args, + } ) ), lockHrefControls: // Disable editing the link of the URL if the image is inside a pattern instance. // This is a temporary solution until we support overriding the link on the frontend. @@ -475,7 +479,11 @@ export default function Image( { lockAltControls: !! altBinding && ( ! altBindingSource || - altBindingSource?.lockAttributesEditing() ), + altBindingSource?.lockAttributesEditing( { + select, + context, + args: altBinding?.args, + } ) ), lockAltControlsMessage: altBindingSource?.label ? sprintf( /* translators: %s: Label of the bindings source. */ @@ -486,7 +494,11 @@ export default function Image( { lockTitleControls: !! titleBinding && ( ! titleBindingSource || - titleBindingSource?.lockAttributesEditing() ), + titleBindingSource?.lockAttributesEditing( { + select, + context, + args: titleBinding?.args, + } ) ), lockTitleControlsMessage: titleBindingSource?.label ? sprintf( /* translators: %s: Label of the bindings source. */ From 1242f658b27d7d7c2e2836da101c6f3fc64156fa Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:57 +0200 Subject: [PATCH 37/60] Check user can edit post meta --- packages/editor/src/bindings/post-meta.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index c10ffa04656ce..1ae614bf66908 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -34,12 +34,27 @@ export default { }, } ); }, - lockAttributesEditing() { + lockAttributesEditing( { select, context = {} } ) { + const postId = context.postId + ? context.postId + : select( editorStore ).getCurrentPostId(); + const postType = context.postType + ? context.postType + : select( editorStore ).getCurrentPostType(); + // TODO: Check that editing is happening in the post editor and not a template. // TODO: Check that the custom field is not protected and available in the REST API. - // TODO: Check that the user has the capability to edit post meta. + // Check that the user has the capability to edit post meta. + const canUserEdit = select( coreDataStore ).canUserEditEntityRecord( + 'postType', + postType, + postId + ); + if ( ! canUserEdit ) { + return true; + } return false; }, From a28ddb9eb8311cfcafc4ccf7814b9d67f3869890 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:57 +0200 Subject: [PATCH 38/60] Check field is exposed in the REST API --- packages/editor/src/bindings/post-meta.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index 1ae614bf66908..22529db8ba7cf 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -34,7 +34,7 @@ export default { }, } ); }, - lockAttributesEditing( { select, context = {} } ) { + lockAttributesEditing( { select, context = {}, args } ) { const postId = context.postId ? context.postId : select( editorStore ).getCurrentPostId(); @@ -44,7 +44,12 @@ export default { // TODO: Check that editing is happening in the post editor and not a template. - // TODO: Check that the custom field is not protected and available in the REST API. + // Check that the custom field is not protected and available in the REST API. + const isFieldExposed = + select( editorStore ).getEditedPostAttribute( 'meta' )[ args.key ]; + if ( ! isFieldExposed ) { + return true; + } // Check that the user has the capability to edit post meta. const canUserEdit = select( coreDataStore ).canUserEditEntityRecord( From 4fcab57aac4bda4d55acaff6f5d1c6ffddfba095 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:57 +0200 Subject: [PATCH 39/60] Disable editing in templates --- packages/editor/src/bindings/post-meta.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index 22529db8ba7cf..8ed3295f56600 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -42,7 +42,10 @@ export default { ? context.postType : select( editorStore ).getCurrentPostType(); - // TODO: Check that editing is happening in the post editor and not a template. + // Check that editing is happening in the post editor and not a template. + if ( postType === 'wp_template' ) { + return true; + } // Check that the custom field is not protected and available in the REST API. const isFieldExposed = From ff9ffb7ba8e2212a343c1a76617d02d7ee669325 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:57 +0200 Subject: [PATCH 40/60] Add fallback for postId --- packages/editor/src/bindings/post-meta.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index 8ed3295f56600..03f4822ccebe5 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -23,12 +23,14 @@ export default { ).meta?.[ args.key ]; }, setValue( { registry, context, args, value } ) { - const postType = context.postType - ? context.postType - : registry.select( editorStore ).getCurrentPostType(); + const postId = + context.postId || registry.select( editorStore ).getCurrentPostId(); + const postType = + context.postType || + registry.select( editorStore ).getCurrentPostType(); registry .dispatch( coreDataStore ) - .editEntityRecord( 'postType', postType, context.postId, { + .editEntityRecord( 'postType', postType, postId, { meta: { [ args.key ]: value, }, From 870e4433e764876fd908fb61eb4489b9434a1cd1 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:57 +0200 Subject: [PATCH 41/60] Revert "Revert "Enable parallel processing for PHPCS sniffs (#61700)"" This reverts commit e74d71cf0538b4aa8016b627db7849b1206e7a02. --- phpcs.xml.dist | 3 +++ 1 file changed, 3 insertions(+) diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 45d742a498c65..5bc7a5676e712 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -42,6 +42,9 @@ + + + ./bin ./gutenberg.php ./lib From fcca7b1d5b2ca603bae4453a8a149384fd8f7270 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:57 +0200 Subject: [PATCH 42/60] Adapt old tests --- .../editor/various/block-bindings.spec.js | 58 ++++++------------- 1 file changed, 18 insertions(+), 40 deletions(-) diff --git a/test/e2e/specs/editor/various/block-bindings.spec.js b/test/e2e/specs/editor/various/block-bindings.spec.js index 9f7ba4b5a3587..e242e7f06ffd7 100644 --- a/test/e2e/specs/editor/various/block-bindings.spec.js +++ b/test/e2e/specs/editor/various/block-bindings.spec.js @@ -1193,11 +1193,6 @@ test.describe( 'Block bindings', () => { await expect( paragraphBlock ).toHaveText( 'Value of the text_custom_field' ); - // Paragraph is not editable. - await expect( paragraphBlock ).toHaveAttribute( - 'contenteditable', - 'false' - ); // Check the frontend shows the value of the custom field. const postId = await editor.publishPost(); @@ -1331,6 +1326,12 @@ test.describe( 'Block bindings', () => { }, }, } ); + // Select the paragraph and press Enter at the end of it. + const paragraph = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + await editor.selectBlocks( paragraph ); + await page.keyboard.press( 'End' ); await page.keyboard.press( 'Enter' ); const [ initialParagraph, newEmptyParagraph ] = await editor.canvas @@ -1473,11 +1474,6 @@ test.describe( 'Block bindings', () => { await expect( headingBlock ).toHaveText( 'Value of the text_custom_field' ); - // Heading is not editable. - await expect( headingBlock ).toHaveAttribute( - 'contenteditable', - 'false' - ); // Check the frontend shows the value of the custom field. const postId = await editor.publishPost(); @@ -1509,6 +1505,13 @@ test.describe( 'Block bindings', () => { }, }, } ); + + // Select the heading and press Enter at the end of it. + const heading = editor.canvas.getByRole( 'document', { + name: 'Block: Heading', + } ); + await editor.selectBlocks( heading ); + await page.keyboard.press( 'End' ); await page.keyboard.press( 'Enter' ); // Can't use `editor.getBlocks` because it doesn't return the meta value shown in the editor. const [ initialHeading, newEmptyParagraph ] = @@ -1568,12 +1571,6 @@ test.describe( 'Block bindings', () => { 'Value of the text_custom_field' ); - // Button is not editable. - await expect( buttonBlock ).toHaveAttribute( - 'contenteditable', - 'false' - ); - // Check the frontend shows the value of the custom field. const postId = await editor.publishPost(); await page.goto( `/?p=${ postId }` ); @@ -1702,6 +1699,7 @@ test.describe( 'Block bindings', () => { } ) .getByRole( 'textbox' ) .click(); + await page.keyboard.press( 'End' ); await page.keyboard.press( 'Enter' ); const [ initialButton, newEmptyButton ] = await editor.canvas .locator( '[data-type="core/button"]' ) @@ -1826,12 +1824,7 @@ test.describe( 'Block bindings', () => { imagePlaceholderSrc ); - // Alt textarea is disabled and with the custom field value. - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - ).toHaveAttribute( 'readonly' ); + // Alt textarea should have the custom field value. const altValue = await page .getByRole( 'tabpanel', { name: 'Settings' } ) .getByLabel( 'Alternative text' ) @@ -1892,7 +1885,7 @@ test.describe( 'Block bindings', () => { imagePlaceholderSrc ); - // Title input is disabled and with the custom field value. + // Title input should have the custom field value. const advancedButton = page .getByRole( 'tabpanel', { name: 'Settings' } ) .getByRole( 'button', { @@ -1903,11 +1896,6 @@ test.describe( 'Block bindings', () => { if ( isAdvancedPanelOpen === 'false' ) { await advancedButton.click(); } - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - ).toHaveAttribute( 'readonly' ); const titleValue = await page .getByRole( 'tabpanel', { name: 'Settings' } ) .getByLabel( 'Title attribute' ) @@ -1972,19 +1960,14 @@ test.describe( 'Block bindings', () => { imageCustomFieldSrc ); - // Alt textarea is disabled and with the custom field value. - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - ).toHaveAttribute( 'readonly' ); + // Alt textarea should have the custom field value. const altValue = await page .getByRole( 'tabpanel', { name: 'Settings' } ) .getByLabel( 'Alternative text' ) .inputValue(); expect( altValue ).toBe( 'Value of the text_custom_field' ); - // Title input is enabled and with the original value. + // Title input should have the original value. const advancedButton = page .getByRole( 'tabpanel', { name: 'Settings' } ) .getByRole( 'button', { @@ -1995,11 +1978,6 @@ test.describe( 'Block bindings', () => { if ( isAdvancedPanelOpen === 'false' ) { await advancedButton.click(); } - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - ).toBeEnabled(); const titleValue = await page .getByRole( 'tabpanel', { name: 'Settings' } ) .getByLabel( 'Title attribute' ) From e2db4a30c6e7484fa90329631372607b3cf600db Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:57 +0200 Subject: [PATCH 43/60] Simplify lockAttributesEditing fallbacks --- packages/editor/src/bindings/post-meta.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index 03f4822ccebe5..2897d4a4d06c9 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -24,9 +24,10 @@ export default { }, setValue( { registry, context, args, value } ) { const postId = - context.postId || registry.select( editorStore ).getCurrentPostId(); + context?.postId || + registry.select( editorStore ).getCurrentPostId(); const postType = - context.postType || + context?.postType || registry.select( editorStore ).getCurrentPostType(); registry .dispatch( coreDataStore ) @@ -36,13 +37,11 @@ export default { }, } ); }, - lockAttributesEditing( { select, context = {}, args } ) { - const postId = context.postId - ? context.postId - : select( editorStore ).getCurrentPostId(); - const postType = context.postType - ? context.postType - : select( editorStore ).getCurrentPostType(); + lockAttributesEditing( { select, context, args } ) { + const postId = + context?.postId || select( editorStore ).getCurrentPostId(); + const postType = + context?.postType || select( editorStore ).getCurrentPostType(); // Check that editing is happening in the post editor and not a template. if ( postType === 'wp_template' ) { From 23c99fc0a862d3dc7f4418c087d6cb77c4e10aa7 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:57 +0200 Subject: [PATCH 44/60] Don't use fallback for context --- packages/editor/src/bindings/post-meta.js | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index 2897d4a4d06c9..5fa1ca3e3eb6c 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -23,28 +23,17 @@ export default { ).meta?.[ args.key ]; }, setValue( { registry, context, args, value } ) { - const postId = - context?.postId || - registry.select( editorStore ).getCurrentPostId(); - const postType = - context?.postType || - registry.select( editorStore ).getCurrentPostType(); registry .dispatch( coreDataStore ) - .editEntityRecord( 'postType', postType, postId, { + .editEntityRecord( 'postType', context?.postType, context?.postId, { meta: { [ args.key ]: value, }, } ); }, lockAttributesEditing( { select, context, args } ) { - const postId = - context?.postId || select( editorStore ).getCurrentPostId(); - const postType = - context?.postType || select( editorStore ).getCurrentPostType(); - // Check that editing is happening in the post editor and not a template. - if ( postType === 'wp_template' ) { + if ( context?.postType === 'wp_template' ) { return true; } @@ -58,8 +47,8 @@ export default { // Check that the user has the capability to edit post meta. const canUserEdit = select( coreDataStore ).canUserEditEntityRecord( 'postType', - postType, - postId + context?.postType, + context?.postId ); if ( ! canUserEdit ) { return true; From 10996f4d926346aff13ddcb5a4d948121ea8dd35 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:57 +0200 Subject: [PATCH 45/60] Add postType fallback when locking controls --- packages/editor/src/bindings/post-meta.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index 5fa1ca3e3eb6c..62d073b212b0a 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -32,8 +32,11 @@ export default { } ); }, lockAttributesEditing( { select, context, args } ) { + const postType = + context?.postType || select( editorStore ).getCurrentPostType(); + // Check that editing is happening in the post editor and not a template. - if ( context?.postType === 'wp_template' ) { + if ( postType === 'wp_template' ) { return true; } From 658e0a3ceda63a5fe2f5af3d4e87d4a0eea18475 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:57 +0200 Subject: [PATCH 46/60] Pass context in rich text --- packages/block-editor/src/components/rich-text/index.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index cee77baba7847..274af53b90180 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -188,6 +188,10 @@ export function RichTextWrapper( ! blockBindingsSource || blockBindingsSource.lockAttributesEditing( { select, + context: + select( blockEditorStore ).getBlockContext( + clientId + ), args: binding.args, } ) ) { From 46860c72bef779c10bfb1bdf778c7fd5d7443190 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:58 +0200 Subject: [PATCH 47/60] Check contenteditable attribute in test --- test/e2e/specs/editor/various/block-bindings.spec.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/e2e/specs/editor/various/block-bindings.spec.js b/test/e2e/specs/editor/various/block-bindings.spec.js index e242e7f06ffd7..7c7256b367566 100644 --- a/test/e2e/specs/editor/various/block-bindings.spec.js +++ b/test/e2e/specs/editor/various/block-bindings.spec.js @@ -1369,7 +1369,10 @@ test.describe( 'Block bindings', () => { await expect( paragraphBlock ).toHaveText( 'Value of the text_custom_field' ); - await expect( paragraphBlock ).toBeEditable(); + await expect( paragraphBlock ).toHaveAttribute( + 'contenteditable', + 'true' + ); await paragraphBlock.fill( 'new value' ); // Check that the paragraph content attribute didn't change. const [ paragraphBlockObject ] = await editor.getBlocks(); From 180f0aa2942529f1375d2218f4ed92afbd42e3cc Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:58 +0200 Subject: [PATCH 48/60] Change name to `canUserEditValue` --- .../src/components/rich-text/index.js | 5 ++- packages/block-library/src/button/edit.js | 11 +++---- packages/block-library/src/image/edit.js | 11 +++---- packages/block-library/src/image/image.js | 33 +++++++++---------- packages/blocks/src/store/private-actions.js | 2 +- packages/blocks/src/store/reducer.js | 3 +- .../editor/src/bindings/pattern-overrides.js | 4 +-- packages/editor/src/bindings/post-meta.js | 10 +++--- 8 files changed, 35 insertions(+), 44 deletions(-) diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 274af53b90180..d390cc691a6e1 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -180,13 +180,12 @@ export function RichTextWrapper( break; } - // If the source is not defined, or if its value of `lockAttributesEditing` is `true`, disable it. + // If the source is not defined, or if its value of `canUserEditValue` is `false`, disable it. const blockBindingsSource = getBlockBindingsSource( binding.source ); if ( - ! blockBindingsSource || - blockBindingsSource.lockAttributesEditing( { + ! blockBindingsSource?.canUserEditValue( { select, context: select( blockEditorStore ).getBlockContext( diff --git a/packages/block-library/src/button/edit.js b/packages/block-library/src/button/edit.js index 1ae11af9ddde8..e5bd5e6b5f064 100644 --- a/packages/block-library/src/button/edit.js +++ b/packages/block-library/src/button/edit.js @@ -247,12 +247,11 @@ function ButtonEdit( props ) { return { lockUrlControls: !! metadata?.bindings?.url && - ( ! blockBindingsSource || - blockBindingsSource?.lockAttributesEditing( { - select, - context, - args: metadata?.bindings?.url?.args, - } ) ), + ! blockBindingsSource?.canUserEditValue( { + select, + context, + args: metadata?.bindings?.url?.args, + } ), }; }, [ isSelected, metadata?.bindings?.url ] diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index a0bc9f67b6466..673ab44a8c28a 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -317,12 +317,11 @@ export function ImageEdit( { return { lockUrlControls: !! metadata?.bindings?.url && - ( ! blockBindingsSource || - blockBindingsSource?.lockAttributesEditing( { - select, - context, - args: metadata?.bindings?.url?.args, - } ) ), + ! blockBindingsSource?.canUserEditValue( { + select, + context, + args: metadata?.bindings?.url?.args, + } ), lockUrlControlsMessage: blockBindingsSource?.label ? sprintf( /* translators: %s: Label of the bindings source. */ diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js index 40ab680074451..dcd67c53b1869 100644 --- a/packages/block-library/src/image/image.js +++ b/packages/block-library/src/image/image.js @@ -462,12 +462,11 @@ export default function Image( { return { lockUrlControls: !! urlBinding && - ( ! urlBindingSource || - urlBindingSource?.lockAttributesEditing( { - select, - context, - args: urlBinding?.args, - } ) ), + ! urlBindingSource?.canUserEditValue( { + select, + context, + args: urlBinding?.args, + } ), lockHrefControls: // Disable editing the link of the URL if the image is inside a pattern instance. // This is a temporary solution until we support overriding the link on the frontend. @@ -478,12 +477,11 @@ export default function Image( { hasParentPattern, lockAltControls: !! altBinding && - ( ! altBindingSource || - altBindingSource?.lockAttributesEditing( { - select, - context, - args: altBinding?.args, - } ) ), + ! altBindingSource?.canUserEditValue( { + select, + context, + args: altBinding?.args, + } ), lockAltControlsMessage: altBindingSource?.label ? sprintf( /* translators: %s: Label of the bindings source. */ @@ -493,12 +491,11 @@ export default function Image( { : __( 'Connected to dynamic data' ), lockTitleControls: !! titleBinding && - ( ! titleBindingSource || - titleBindingSource?.lockAttributesEditing( { - select, - context, - args: titleBinding?.args, - } ) ), + ! titleBindingSource?.canUserEditValue( { + select, + context, + args: titleBinding?.args, + } ), lockTitleControlsMessage: titleBindingSource?.label ? sprintf( /* translators: %s: Label of the bindings source. */ diff --git a/packages/blocks/src/store/private-actions.js b/packages/blocks/src/store/private-actions.js index a47d9aacab37a..dd6650338d9d1 100644 --- a/packages/blocks/src/store/private-actions.js +++ b/packages/blocks/src/store/private-actions.js @@ -55,6 +55,6 @@ export function registerBlockBindingsSource( source ) { setValue: source.setValue, setValues: source.setValues, getPlaceholder: source.getPlaceholder, - lockAttributesEditing: source.lockAttributesEditing, + canUserEditValue: source.canUserEditValue, }; } diff --git a/packages/blocks/src/store/reducer.js b/packages/blocks/src/store/reducer.js index e7d2f553a8069..c00810c534d55 100644 --- a/packages/blocks/src/store/reducer.js +++ b/packages/blocks/src/store/reducer.js @@ -381,8 +381,7 @@ export function blockBindingsSources( state = {}, action ) { setValue: action.setValue, setValues: action.setValues, getPlaceholder: action.getPlaceholder, - lockAttributesEditing: - action.lockAttributesEditing || ( () => true ), + canUserEditValue: action.canUserEditValue || ( () => false ), }, }; } diff --git a/packages/editor/src/bindings/pattern-overrides.js b/packages/editor/src/bindings/pattern-overrides.js index e4be293246f35..740692b9e9a62 100644 --- a/packages/editor/src/bindings/pattern-overrides.js +++ b/packages/editor/src/bindings/pattern-overrides.js @@ -89,7 +89,5 @@ export default { }, } ); }, - lockAttributesEditing() { - return false; - }, + canUserEditValue: () => true, }; diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index 62d073b212b0a..872b30e26acd8 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -31,20 +31,20 @@ export default { }, } ); }, - lockAttributesEditing( { select, context, args } ) { + canUserEditValue( { select, context, args } ) { const postType = context?.postType || select( editorStore ).getCurrentPostType(); // Check that editing is happening in the post editor and not a template. if ( postType === 'wp_template' ) { - return true; + return false; } // Check that the custom field is not protected and available in the REST API. const isFieldExposed = select( editorStore ).getEditedPostAttribute( 'meta' )[ args.key ]; if ( ! isFieldExposed ) { - return true; + return false; } // Check that the user has the capability to edit post meta. @@ -54,9 +54,9 @@ export default { context?.postId ); if ( ! canUserEdit ) { - return true; + return false; } - return false; + return true; }, }; From d65b373097a0b1a87bd821dc76f0c2121cc73471 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:58 +0200 Subject: [PATCH 49/60] Revert changes caused by rebasing --- .../data/data-core-block-editor.md | 30 ----- .../src/components/block-edit/edit.js | 14 +-- .../block-list/use-block-props/index.js | 3 +- .../src/components/block-toolbar/index.js | 2 +- .../src/components/rich-text/index.js | 2 +- packages/block-editor/src/hooks/index.js | 1 + packages/block-editor/src/store/actions.js | 114 ++---------------- packages/block-editor/src/store/reducer.js | 29 ----- packages/block-editor/src/store/selectors.js | 101 ++-------------- .../block-editor/src/store/test/actions.js | 29 +---- .../editor/src/bindings/pattern-overrides.js | 4 +- packages/editor/src/bindings/post-meta.js | 14 ++- 12 files changed, 40 insertions(+), 303 deletions(-) diff --git a/docs/reference-guides/data/data-core-block-editor.md b/docs/reference-guides/data/data-core-block-editor.md index 8ef61891aa76c..862a8b2d8a06a 100644 --- a/docs/reference-guides/data/data-core-block-editor.md +++ b/docs/reference-guides/data/data-core-block-editor.md @@ -187,8 +187,6 @@ _Returns_ Returns a block's attributes given its client ID, or null if no block exists with the client ID. -Process block bindings to modify the value of the attributes if needed. - _Parameters_ - _state_ `Object`: Editor state. @@ -198,19 +196,6 @@ _Returns_ - `Object?`: Block attributes. -### getBlockContext - -Returns the Block Context of a block, if any exist. - -_Parameters_ - -- _state_ `Object`: Editor state. -- _clientId_ `?string`: Block client ID. - -_Returns_ - -- `?Object`: Block context of the block if set. - ### getBlockCount Returns the number of blocks currently present in the post. @@ -1844,8 +1829,6 @@ _Returns_ Action that updates attributes of multiple blocks with the specified client IDs. -Process block bindings to skip updating the bound attributes and run binding source setValue instead. - _Parameters_ - _clientIds_ `string|string[]`: Block client IDs. @@ -1856,19 +1839,6 @@ _Returns_ - `Object`: Action object. -### updateBlockContext - -Action that changes the block context of the given block(s) in the store. - -_Parameters_ - -- _clientId_ `string | SettingsByClientId`: Client ID of the block. -- _context_ `Object`: Object with the new context. - -_Returns_ - -- `Object`: Action object - ### updateBlockListSettings Action that changes the nested settings of the given block(s). diff --git a/packages/block-editor/src/components/block-edit/edit.js b/packages/block-editor/src/components/block-edit/edit.js index 3ec46eda10e44..83d0e3f406f82 100644 --- a/packages/block-editor/src/components/block-edit/edit.js +++ b/packages/block-editor/src/components/block-edit/edit.js @@ -12,14 +12,12 @@ import { hasBlockSupport, getBlockType, } from '@wordpress/blocks'; -import { useDispatch } from '@wordpress/data'; -import { useContext, useLayoutEffect, useMemo } from '@wordpress/element'; +import { useContext, useMemo } from '@wordpress/element'; /** * Internal dependencies */ import BlockContext from '../block-context'; -import { store as blockEditorStore } from '../../store'; /** * Default value used for blocks which do not define their own context needs, @@ -50,18 +48,10 @@ const Edit = ( props ) => { const EditWithFilters = withFilters( 'editor.BlockEdit' )( Edit ); const EditWithGeneratedProps = ( props ) => { - const { clientId, attributes = {}, name } = props; + const { attributes = {}, name } = props; const blockType = getBlockType( name ); const blockContext = useContext( BlockContext ); - // Sync the block context with the block editor store. - const { updateBlockContext } = useDispatch( blockEditorStore ); - useLayoutEffect( () => { - if ( blockContext && Object.keys( blockContext ).length > 0 ) { - updateBlockContext( clientId, blockContext ); - } - }, [ clientId, blockContext, updateBlockContext ] ); - // Assign context values using the block type's declared context needs. const context = useMemo( () => { return blockType && blockType.usesContext 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 16ac0be3af36a..c704f4b417241 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 @@ -30,8 +30,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 '../../../utils/bindings'; - +import { canBindBlock } from '../../../hooks/use-bindings-attributes'; /** * This hook is used to lightly mark an element as a block element. The element * should be the outermost element of a block. Call this hook and pass the diff --git a/packages/block-editor/src/components/block-toolbar/index.js b/packages/block-editor/src/components/block-toolbar/index.js index 5dc91de198fc2..c3570c4a007f1 100644 --- a/packages/block-editor/src/components/block-toolbar/index.js +++ b/packages/block-editor/src/components/block-toolbar/index.js @@ -37,7 +37,7 @@ import NavigableToolbar from '../navigable-toolbar'; import Shuffle from './shuffle'; import BlockBindingsIndicator from '../block-bindings-toolbar-indicator'; import { useHasBlockToolbar } from './use-has-block-toolbar'; -import { canBindBlock } from '../../utils/bindings'; +import { canBindBlock } from '../../hooks/use-bindings-attributes'; /** * Renders the block toolbar. * diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index d390cc691a6e1..443af014b5593 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -38,7 +38,7 @@ import { getAllowedFormats } from './utils'; import { Content, valueToHTMLString } from './content'; import { withDeprecations } from './with-deprecations'; import { unlock } from '../../lock-unlock'; -import { canBindBlock } from '../../utils/bindings'; +import { canBindBlock } from '../../hooks/use-bindings-attributes'; export const keyboardShortcutContext = createContext(); export const inputEventContext = createContext(); diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index 8b871f5a1d76f..89e6819c1d031 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -32,6 +32,7 @@ import './metadata'; import blockHooks from './block-hooks'; import blockBindingsPanel from './block-bindings'; import './block-renaming'; +import './use-bindings-attributes'; createBlockEditFilter( [ diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index ddd1ca9ad0b19..74814cf22746b 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -15,7 +15,6 @@ import { getBlockSupport, isUnmodifiedDefaultBlock, isUnmodifiedBlock, - store as blocksStore, } from '@wordpress/blocks'; import { speak } from '@wordpress/a11y'; import { __, _n, sprintf } from '@wordpress/i18n'; @@ -25,7 +24,6 @@ import deprecated from '@wordpress/deprecated'; /** * Internal dependencies */ -import { canBindAttribute } from '../utils/bindings'; import { retrieveSelectedAttribute, findRichTextAttributeKey, @@ -157,98 +155,24 @@ export function receiveBlocks( blocks ) { /** * Action that updates attributes of multiple blocks with the specified client IDs. * - * Process block bindings to skip updating the bound attributes and run binding source setValue instead. - * * @param {string|string[]} clientIds Block client IDs. * @param {Object} attributes Block attributes to be merged. Should be keyed by clientIds if * uniqueByBlock is true. * @param {boolean} uniqueByBlock true if each block in clientIds array has a unique set of attributes * @return {Object} Action object. */ -export const updateBlockAttributes = - ( clientIds, attributes, uniqueByBlock = false ) => - ( { dispatch, registry } ) => { - const keptAttributes = { ...attributes }; - for ( const clientId of clientIds ) { - const block = registry.select( STORE_NAME ).getBlock( clientId ); - const bindings = block?.attributes?.metadata?.bindings; - - if ( ! bindings ) { - continue; - } - - const sources = unlock( - registry.select( blocksStore ) - ).getAllBlockBindingsSources(); - - const updatesBySource = new Map(); - - const attributeEntries = Object.entries( - uniqueByBlock ? attributes[ clientId ] : attributes - ); - - attributeEntries.forEach( ( [ attributeName, newValue ] ) => { - if ( - ! bindings[ attributeName ] || - ! canBindAttribute( block.name, attributeName ) - ) { - return; - } - - const source = sources[ bindings[ attributeName ].source ]; - if ( ! source?.setValue && ! source?.setValues ) { - return; - } - updatesBySource.set( source, { - ...updatesBySource.get( source ), - [ attributeName ]: newValue, - } ); - - // Skip attribute. - if ( uniqueByBlock ) { - delete keptAttributes[ clientId ][ attributeName ]; - } else { - delete keptAttributes[ attributeName ]; - } - } ); - - if ( updatesBySource.size ) { - const context = registry - .select( STORE_NAME ) - .getBlockContext( clientId ); - for ( const [ source, boundAttributes ] of updatesBySource ) { - if ( source.setValues ) { - source.setValues( { - registry, - context, - clientId, - attributes: boundAttributes, - } ); - } else { - for ( const [ attributeName, value ] of Object.entries( - boundAttributes - ) ) { - source.setValue( { - registry, - context, - clientId, - attributeName, - args: bindings[ attributeName ].args, - value, - } ); - } - } - } - } - } - - dispatch( { - type: 'UPDATE_BLOCK_ATTRIBUTES', - clientIds: castArray( clientIds ), - attributes: keptAttributes, - uniqueByBlock, - } ); +export function updateBlockAttributes( + clientIds, + attributes, + uniqueByBlock = false +) { + return { + type: 'UPDATE_BLOCK_ATTRIBUTES', + clientIds: castArray( clientIds ), + attributes, + uniqueByBlock, }; +} /** * Action that updates the block with the specified client ID. @@ -1608,22 +1532,6 @@ export function updateBlockListSettings( clientId, settings ) { }; } -/** - * Action that changes the block context of the given block(s) in the store. - * - * @param {string | SettingsByClientId} clientId Client ID of the block. - * @param {Object} context Object with the new context. - * - * @return {Object} Action object - */ -export function updateBlockContext( clientId, context ) { - return { - type: 'UPDATE_BLOCK_CONTEXT', - clientId, - context, - }; -} - /** * Action that updates the block editor settings. * diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index 4b5bd606e7074..7c83887876919 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -1786,34 +1786,6 @@ export const blockListSettings = ( state = {}, action ) => { return state; }; -/** - * Reducer returning an object where each key is a block client ID, its value - * representing the block context. - * - * @param {Object} state Current state. - * @param {Object} action Dispatched action. - * - * @return {Object} Updated state. - */ -export const blockContext = ( state = {}, action ) => { - if ( action.type === 'UPDATE_BLOCK_CONTEXT' ) { - const { clientId } = action; - if ( ! action.context ) { - return state; - } - - if ( fastDeepEqual( state[ clientId ], action.context ) ) { - return state; - } - - return { - ...state, - [ clientId ]: action.context, - }; - } - return state; -}; - /** * Reducer returning which mode is enabled. * @@ -2108,7 +2080,6 @@ const combinedReducers = combineReducers( { initialPosition, blocksMode, blockListSettings, - blockContext, insertionPoint, template, settings, diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 979ee829a8ec9..1c685eb4230ba 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -36,7 +36,6 @@ import { getTemporarilyEditingAsBlocks, getTemporarilyEditingFocusModeToRevert, } from './private-selectors'; -import { canBindAttribute } from '../utils/bindings'; /** * A block selection object. @@ -111,81 +110,23 @@ export function isBlockValid( state, clientId ) { return !! block && block.isValid; } -/** - * Variable to avoid processing bindings recursively. - * - * @type {boolean} - */ -let isProcessingBindings; - /** * Returns a block's attributes given its client ID, or null if no block exists with * the client ID. * - * Process block bindings to modify the value of the attributes if needed. - * * @param {Object} state Editor state. * @param {string} clientId Block client ID. * * @return {Object?} Block attributes. */ -export const getBlockAttributes = createRegistrySelector( - ( select ) => ( state, clientId ) => { - const block = state.blocks.byClientId.get( clientId ); - if ( ! block ) { - return null; - } - - const blockAttributes = state.blocks.attributes.get( clientId ); - - // Change attribute values if bindings are present. - const bindings = blockAttributes?.metadata?.bindings; - if ( ! bindings || isProcessingBindings ) { - return blockAttributes; - } - isProcessingBindings = true; - - const newAttributes = { ...blockAttributes }; - const context = select( STORE_NAME ).getBlockContext( clientId ); - const sources = unlock( - select( blocksStore ) - ).getAllBlockBindingsSources(); - - for ( const [ attributeName, boundAttribute ] of Object.entries( - bindings - ) ) { - const source = sources[ boundAttribute.source ]; - if ( - ! source?.getValue || - ! canBindAttribute( block.name, attributeName ) - ) { - continue; - } - - const args = { - select, - context, - clientId, - attributeName, - args: boundAttribute.args, - }; - - newAttributes[ attributeName ] = source.getValue( args ); - - if ( newAttributes[ attributeName ] === undefined ) { - if ( attributeName === 'url' ) { - newAttributes[ attributeName ] = null; - } else { - newAttributes[ attributeName ] = - source.getPlaceholder?.( args ); - } - } - } - - isProcessingBindings = false; - return newAttributes; +export function getBlockAttributes( state, clientId ) { + const block = state.blocks.byClientId.get( clientId ); + if ( ! block ) { + return null; } -); + + return state.blocks.attributes.get( clientId ); +} /** * Returns a block given its client ID. This is a parsed copy of the block, @@ -2559,34 +2500,6 @@ export function getBlockListSettings( state, clientId ) { return state.blockListSettings[ clientId ]; } -/** - * Returns the Block Context of a block, if any exist. - * - * @param {Object} state Editor state. - * @param {?string} clientId Block client ID. - * - * @return {?Object} Block context of the block if set. - */ -export function getBlockContext( state, clientId ) { - let blockContext = { ...state.blockContext[ clientId ] }; - // TODO: Review if it's necessary to get the context from the parent blocks. - getBlockParents( state, clientId ).forEach( ( parent ) => { - const block = getBlock( state, parent ); - const blockType = getBlockType( block.name ); - if ( blockType?.providesContext ) { - Object.keys( blockType.providesContext ).forEach( - ( attributeName ) => { - blockContext = { - ...blockContext, - [ attributeName ]: block.attributes[ attributeName ], - }; - } - ); - } - } ); - return blockContext; -} - /** * Returns the editor settings. * diff --git a/packages/block-editor/src/store/test/actions.js b/packages/block-editor/src/store/test/actions.js index 9acabbca9b35b..f960363cdb0dd 100644 --- a/packages/block-editor/src/store/test/actions.js +++ b/packages/block-editor/src/store/test/actions.js @@ -81,25 +81,11 @@ describe( 'actions', () => { } ); describe( 'updateBlockAttributes', () => { - let dispatch, registry; - beforeEach( () => { - dispatch = jest.fn(); - registry = createRegistry(); - registry.registerStore( blockEditorStoreName, { - actions, - selectors, - reducer, - } ); - } ); - - it( 'should dispatch the UPDATE_BLOCK_ATTRIBUTES action (string)', () => { + it( 'should return the UPDATE_BLOCK_ATTRIBUTES action (string)', () => { const clientId = 'myclientid'; const attributes = {}; - updateBlockAttributes( - clientId, - attributes - )( { dispatch, registry } ); - expect( dispatch ).toHaveBeenCalledWith( { + const result = updateBlockAttributes( clientId, attributes ); + expect( result ).toEqual( { type: 'UPDATE_BLOCK_ATTRIBUTES', clientIds: [ clientId ], attributes, @@ -107,14 +93,11 @@ describe( 'actions', () => { } ); } ); - it( 'should dispatch the UPDATE_BLOCK_ATTRIBUTES action (array)', () => { + it( 'should return the UPDATE_BLOCK_ATTRIBUTES action (array)', () => { const clientIds = [ 'myclientid' ]; const attributes = {}; - updateBlockAttributes( - clientIds, - attributes - )( { dispatch, registry } ); - expect( dispatch ).toHaveBeenCalledWith( { + const result = updateBlockAttributes( clientIds, attributes ); + expect( result ).toEqual( { type: 'UPDATE_BLOCK_ATTRIBUTES', clientIds, attributes, diff --git a/packages/editor/src/bindings/pattern-overrides.js b/packages/editor/src/bindings/pattern-overrides.js index 740692b9e9a62..107ed72e722ba 100644 --- a/packages/editor/src/bindings/pattern-overrides.js +++ b/packages/editor/src/bindings/pattern-overrides.js @@ -9,9 +9,9 @@ const CONTENT = 'content'; export default { name: 'core/pattern-overrides', label: _x( 'Pattern Overrides', 'block bindings source' ), - getValue( { select, clientId, attributeName } ) { + getValue( { registry, clientId, attributeName } ) { const { getBlockAttributes, getBlockParentsByBlockName } = - select( blockEditorStore ); + registry.select( blockEditorStore ); const currentBlockAttributes = getBlockAttributes( clientId ); const [ patternClientId ] = getBlockParentsByBlockName( clientId, diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index 872b30e26acd8..36582d8749463 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -15,12 +15,14 @@ export default { getPlaceholder( { args } ) { return args.key; }, - getValue( { select, context, args } ) { - return select( coreDataStore ).getEditedEntityRecord( - 'postType', - context?.postType, - context?.postId - ).meta?.[ args.key ]; + getValue( { registry, context, args } ) { + return registry + .select( coreDataStore ) + .getEditedEntityRecord( + 'postType', + context?.postType, + context?.postId + ).meta?.[ args.key ]; }, setValue( { registry, context, args, value } ) { registry From 944f8f25b6ee1141e5a7c05505674cc209be56bf Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:58 +0200 Subject: [PATCH 50/60] Add space back --- .../src/components/block-list/use-block-props/index.js | 1 + 1 file changed, 1 insertion(+) 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 c704f4b417241..64e40559bb473 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 @@ -31,6 +31,7 @@ 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'; + /** * This hook is used to lightly mark an element as a block element. The element * should be the outermost element of a block. Call this hook and pass the From 458a06824b2452592857c19f037f8406ef678024 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:58 +0200 Subject: [PATCH 51/60] Pass block context through rich text --- packages/block-editor/src/components/rich-text/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 443af014b5593..cc22c9b804130 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -11,6 +11,7 @@ import { useCallback, forwardRef, createContext, + useContext, } from '@wordpress/element'; import { useDispatch, useRegistry, useSelect } from '@wordpress/data'; import { useMergeRefs, useInstanceId } from '@wordpress/compose'; @@ -39,6 +40,7 @@ import { Content, valueToHTMLString } from './content'; import { withDeprecations } from './with-deprecations'; import { unlock } from '../../lock-unlock'; import { canBindBlock } from '../../hooks/use-bindings-attributes'; +import BlockContext from '../block-context'; export const keyboardShortcutContext = createContext(); export const inputEventContext = createContext(); @@ -121,6 +123,7 @@ export function RichTextWrapper( const context = useBlockEditContext(); const { clientId, isSelected: isBlockSelected, name: blockName } = context; const blockBindings = context[ blockBindingsKey ]; + const blockContext = useContext( BlockContext ); const selector = ( select ) => { // Avoid subscribing to the block editor store if the block is not // selected. @@ -187,10 +190,7 @@ export function RichTextWrapper( if ( ! blockBindingsSource?.canUserEditValue( { select, - context: - select( blockEditorStore ).getBlockContext( - clientId - ), + context: blockContext, args: binding.args, } ) ) { From f7cd1d8c3311b14687ebb2c9c004b3ce460dd01c Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:58 +0200 Subject: [PATCH 52/60] Change imports --- .../src/components/block-list/use-block-props/index.js | 2 +- packages/block-editor/src/components/block-toolbar/index.js | 2 +- packages/block-editor/src/components/rich-text/index.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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 64e40559bb473..16ac0be3af36a 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 @@ -30,7 +30,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/bindings'; /** * This hook is used to lightly mark an element as a block element. The element diff --git a/packages/block-editor/src/components/block-toolbar/index.js b/packages/block-editor/src/components/block-toolbar/index.js index c3570c4a007f1..5dc91de198fc2 100644 --- a/packages/block-editor/src/components/block-toolbar/index.js +++ b/packages/block-editor/src/components/block-toolbar/index.js @@ -37,7 +37,7 @@ import NavigableToolbar from '../navigable-toolbar'; import Shuffle from './shuffle'; import BlockBindingsIndicator from '../block-bindings-toolbar-indicator'; import { useHasBlockToolbar } from './use-has-block-toolbar'; -import { canBindBlock } from '../../hooks/use-bindings-attributes'; +import { canBindBlock } from '../../utils/bindings'; /** * Renders the block toolbar. * diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index cc22c9b804130..31bc4f891c694 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 { getAllowedFormats } from './utils'; import { Content, valueToHTMLString } from './content'; import { withDeprecations } from './with-deprecations'; import { unlock } from '../../lock-unlock'; -import { canBindBlock } from '../../hooks/use-bindings-attributes'; +import { canBindBlock } from '../../utils/bindings'; import BlockContext from '../block-context'; export const keyboardShortcutContext = createContext(); From caf6e92af62563a9f8899eed96d677878c66017d Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:58 +0200 Subject: [PATCH 53/60] Transform block attributes into bindings in split selection --- packages/block-editor/src/store/actions.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index 74814cf22746b..91e8ca384f92d 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -29,6 +29,7 @@ import { findRichTextAttributeKey, START_OF_SELECTED_AREA, } from '../utils/selection'; +import { transformBlockAttributesWithBindingsValues } from '../utils/bindings'; import { __experimentalUpdateSettings, privateRemoveBlocks, @@ -823,10 +824,11 @@ export const __unstableDeleteSelection = /** * Split the current selection. - * @param {?Array} blocks + * @param {?Array} blocks + * @param {?Object} context */ export const __unstableSplitSelection = - ( blocks = [] ) => + ( blocks = [], context = {} ) => ( { registry, select, dispatch } ) => { const selectionAnchor = select.getSelectionStart(); const selectionFocus = select.getSelectionEnd(); @@ -918,9 +920,14 @@ export const __unstableSplitSelection = ); } - const length = select.getBlockAttributes( selectionA.clientId )[ - attributeKeyA - ].length; + const blockAttributes = + transformBlockAttributesWithBindingsValues( + selectionA.clientId, + context, + registry + ); + + const length = blockAttributes[ attributeKeyA ].length; if ( selectionA.offset === 0 && length ) { dispatch.insertBlocks( From 5b32ba0b473df86691bbd51056d94b441d571408 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:58 +0200 Subject: [PATCH 54/60] Pass context to in use-input --- .../block-editor/src/components/writing-flow/use-input.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/block-editor/src/components/writing-flow/use-input.js b/packages/block-editor/src/components/writing-flow/use-input.js index 0f10cc9c2d1c7..234cf12577870 100644 --- a/packages/block-editor/src/components/writing-flow/use-input.js +++ b/packages/block-editor/src/components/writing-flow/use-input.js @@ -3,6 +3,7 @@ */ import { useSelect, useDispatch } from '@wordpress/data'; import { useRefEffect } from '@wordpress/compose'; +import { useContext } from '@wordpress/element'; import { ENTER, BACKSPACE, DELETE } from '@wordpress/keycodes'; import { createBlock, @@ -16,6 +17,7 @@ import { * Internal dependencies */ import { store as blockEditorStore } from '../../store'; +import BlockContext from '../block-context'; /** * Handles input for selections across blocks. @@ -42,6 +44,7 @@ export default function useInput() { __unstableExpandSelection, __unstableMarkAutomaticChange, } = useDispatch( blockEditorStore ); + const blockContext = useContext( BlockContext ); return useRefEffect( ( node ) => { function onBeforeInput( event ) { @@ -115,7 +118,7 @@ export default function useInput() { getBlockRootClientId( clientId ) ) ) { - __unstableSplitSelection(); + __unstableSplitSelection( [], blockContext ); event.preventDefault(); } } @@ -131,7 +134,7 @@ export default function useInput() { createBlock( getDefaultBlockName() ) ); } else { - __unstableSplitSelection(); + __unstableSplitSelection( [], blockContext ); } } else if ( event.keyCode === BACKSPACE || From ca1296bc70f1a6b9867eeb7933ba78a71ca003b3 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:58 +0200 Subject: [PATCH 55/60] Change REST API check --- packages/editor/src/bindings/post-meta.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index 36582d8749463..aec890c5ceff8 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -43,8 +43,12 @@ export default { } // Check that the custom field is not protected and available in the REST API. - const isFieldExposed = - select( editorStore ).getEditedPostAttribute( 'meta' )[ args.key ]; + const isFieldExposed = !! select( coreDataStore ).getEntityRecord( + 'postType', + postType, + context?.postId + )?.meta?.[ args.key ]; + if ( ! isFieldExposed ) { return false; } From b5f2acf7252fadfe9c29efaf16a6984beb31d927 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:58 +0200 Subject: [PATCH 56/60] Use getBoundAttributesValues --- packages/block-editor/src/store/actions.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index 91e8ca384f92d..c74060b08fef6 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -29,7 +29,7 @@ import { findRichTextAttributeKey, START_OF_SELECTED_AREA, } from '../utils/selection'; -import { transformBlockAttributesWithBindingsValues } from '../utils/bindings'; +import { getBoundAttributesValues } from '../utils/bindings'; import { __experimentalUpdateSettings, privateRemoveBlocks, @@ -920,12 +920,16 @@ export const __unstableSplitSelection = ); } - const blockAttributes = - transformBlockAttributesWithBindingsValues( - selectionA.clientId, - context, - registry - ); + const boundAttributes = getBoundAttributesValues( + selectionA.clientId, + context, + registry + ); + + const blockAttributes = { + ...select.getBlockAttributes( selectionA.clientId ), + ...boundAttributes, + }; const length = blockAttributes[ attributeKeyA ].length; From a9da13e6f06cbb9279fbdb429355477e48c63799 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:58 +0200 Subject: [PATCH 57/60] Don't split when attribute is bound --- .../src/components/writing-flow/use-input.js | 7 ++--- packages/block-editor/src/store/actions.js | 27 +++++++++---------- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/packages/block-editor/src/components/writing-flow/use-input.js b/packages/block-editor/src/components/writing-flow/use-input.js index 234cf12577870..0f10cc9c2d1c7 100644 --- a/packages/block-editor/src/components/writing-flow/use-input.js +++ b/packages/block-editor/src/components/writing-flow/use-input.js @@ -3,7 +3,6 @@ */ import { useSelect, useDispatch } from '@wordpress/data'; import { useRefEffect } from '@wordpress/compose'; -import { useContext } from '@wordpress/element'; import { ENTER, BACKSPACE, DELETE } from '@wordpress/keycodes'; import { createBlock, @@ -17,7 +16,6 @@ import { * Internal dependencies */ import { store as blockEditorStore } from '../../store'; -import BlockContext from '../block-context'; /** * Handles input for selections across blocks. @@ -44,7 +42,6 @@ export default function useInput() { __unstableExpandSelection, __unstableMarkAutomaticChange, } = useDispatch( blockEditorStore ); - const blockContext = useContext( BlockContext ); return useRefEffect( ( node ) => { function onBeforeInput( event ) { @@ -118,7 +115,7 @@ export default function useInput() { getBlockRootClientId( clientId ) ) ) { - __unstableSplitSelection( [], blockContext ); + __unstableSplitSelection(); event.preventDefault(); } } @@ -134,7 +131,7 @@ export default function useInput() { createBlock( getDefaultBlockName() ) ); } else { - __unstableSplitSelection( [], blockContext ); + __unstableSplitSelection(); } } else if ( event.keyCode === BACKSPACE || diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index c74060b08fef6..e9f78add17244 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -29,7 +29,6 @@ import { findRichTextAttributeKey, START_OF_SELECTED_AREA, } from '../utils/selection'; -import { getBoundAttributesValues } from '../utils/bindings'; import { __experimentalUpdateSettings, privateRemoveBlocks, @@ -824,11 +823,10 @@ export const __unstableDeleteSelection = /** * Split the current selection. - * @param {?Array} blocks - * @param {?Object} context + * @param {?Array} blocks */ export const __unstableSplitSelection = - ( blocks = [], context = {} ) => + ( blocks = [] ) => ( { registry, select, dispatch } ) => { const selectionAnchor = select.getSelectionStart(); const selectionFocus = select.getSelectionEnd(); @@ -874,6 +872,16 @@ export const __unstableSplitSelection = typeof selectionB.attributeKey === 'string' ? selectionB.attributeKey : findRichTextAttributeKey( blockBType ); + const blockAttributes = select.getBlockAttributes( + selectionA.clientId + ); + const bindings = blockAttributes?.metadata?.bindings; + + // If the attribute is bound, don't split the selection and insert a new block instead. + if ( bindings?.[ attributeKeyA ] ) { + dispatch.insertAfterBlock( selectionA.clientId ); + return; + } // Can't split if the selection is not set. if ( @@ -920,17 +928,6 @@ export const __unstableSplitSelection = ); } - const boundAttributes = getBoundAttributesValues( - selectionA.clientId, - context, - registry - ); - - const blockAttributes = { - ...select.getBlockAttributes( selectionA.clientId ), - ...boundAttributes, - }; - const length = blockAttributes[ attributeKeyA ].length; if ( selectionA.offset === 0 && length ) { From 4bca30559e1b36cffa3aa1dc8fbd3dd54fc52134 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:58 +0200 Subject: [PATCH 58/60] Revert changes caused by rebase --- .../block-list/use-block-props/index.js | 2 +- .../src/components/block-toolbar/index.js | 2 +- .../src/components/rich-text/index.js | 2 +- .../src/hooks/use-bindings-attributes.js | 103 +++++++++++++---- packages/block-editor/src/utils/bindings.js | 107 ------------------ 5 files changed, 84 insertions(+), 132 deletions(-) delete mode 100644 packages/block-editor/src/utils/bindings.js 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 16ac0be3af36a..64e40559bb473 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 @@ -30,7 +30,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 '../../../utils/bindings'; +import { canBindBlock } from '../../../hooks/use-bindings-attributes'; /** * This hook is used to lightly mark an element as a block element. The element diff --git a/packages/block-editor/src/components/block-toolbar/index.js b/packages/block-editor/src/components/block-toolbar/index.js index 5dc91de198fc2..c3570c4a007f1 100644 --- a/packages/block-editor/src/components/block-toolbar/index.js +++ b/packages/block-editor/src/components/block-toolbar/index.js @@ -37,7 +37,7 @@ import NavigableToolbar from '../navigable-toolbar'; import Shuffle from './shuffle'; import BlockBindingsIndicator from '../block-bindings-toolbar-indicator'; import { useHasBlockToolbar } from './use-has-block-toolbar'; -import { canBindBlock } from '../../utils/bindings'; +import { canBindBlock } from '../../hooks/use-bindings-attributes'; /** * Renders the block toolbar. * diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 31bc4f891c694..cc22c9b804130 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 { getAllowedFormats } from './utils'; import { Content, valueToHTMLString } from './content'; import { withDeprecations } from './with-deprecations'; import { unlock } from '../../lock-unlock'; -import { canBindBlock } from '../../utils/bindings'; +import { canBindBlock } from '../../hooks/use-bindings-attributes'; import BlockContext from '../block-context'; export const keyboardShortcutContext = createContext(); diff --git a/packages/block-editor/src/hooks/use-bindings-attributes.js b/packages/block-editor/src/hooks/use-bindings-attributes.js index 4be372eec27c3..b7a4ca0379dd1 100644 --- a/packages/block-editor/src/hooks/use-bindings-attributes.js +++ b/packages/block-editor/src/hooks/use-bindings-attributes.js @@ -11,11 +11,6 @@ import { addFilter } from '@wordpress/hooks'; * Internal dependencies */ import { unlock } from '../lock-unlock'; -import { - canBindBlock, - canBindAttribute, - getBoundAttributesValues, -} from '../utils/bindings'; /** @typedef {import('@wordpress/compose').WPHigherOrderComponent} WPHigherOrderComponent */ /** @typedef {import('@wordpress/blocks').WPBlockSettings} WPBlockSettings */ @@ -27,26 +22,90 @@ import { * @return {WPHigherOrderComponent} Higher-order component. */ +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 const withBlockBindingSupport = createHigherOrderComponent( ( BlockEdit ) => ( props ) => { const registry = useRegistry(); - const { - attributes: blockAttributes, - name, - clientId, - context, - setAttributes, - } = props; - const bindings = blockAttributes?.metadata?.bindings; - - // It seems if I don't wrap this in a useSelect, the reset in pattern overrides doesn't work as expected. + const sources = useSelect( ( select ) => + unlock( select( blocksStore ) ).getAllBlockBindingsSources() + ); + const bindings = props.attributes.metadata?.bindings; + const { name, clientId, context } = props; const boundAttributes = useSelect( () => { - return getBoundAttributesValues( clientId, context, registry ); - }, [ clientId, context, registry ] ); + if ( ! bindings ) { + return; + } + + const attributes = {}; + + for ( const [ attributeName, boundAttribute ] of Object.entries( + bindings + ) ) { + const source = sources[ boundAttribute.source ]; + if ( + ! source?.getValue || + ! canBindAttribute( name, attributeName ) + ) { + continue; + } + + const args = { + registry, + context, + clientId, + attributeName, + args: boundAttribute.args, + }; + + attributes[ attributeName ] = source.getValue( args ); + + if ( attributes[ attributeName ] === undefined ) { + if ( attributeName === 'url' ) { + attributes[ attributeName ] = null; + } else { + attributes[ attributeName ] = + source.getPlaceholder?.( args ); + } + } + } + + return attributes; + }, [ bindings, name, clientId, context, registry, sources ] ); + + const { setAttributes } = props; - const sources = unlock( - registry.select( blocksStore ) - ).getAllBlockBindingsSources(); const _setAttributes = useCallback( ( nextAttributes ) => { registry.batch( () => { @@ -131,7 +190,7 @@ export const withBlockBindingSupport = createHigherOrderComponent( <> @@ -161,6 +220,6 @@ function shimAttributeSource( settings, name ) { addFilter( 'blocks.registerBlockType', - 'core/editor/use-bindings-attributes', + 'core/editor/custom-sources-backwards-compatibility/shim-attribute-source', shimAttributeSource ); diff --git a/packages/block-editor/src/utils/bindings.js b/packages/block-editor/src/utils/bindings.js deleted file mode 100644 index 28281b4be75c8..0000000000000 --- a/packages/block-editor/src/utils/bindings.js +++ /dev/null @@ -1,107 +0,0 @@ -/** - * WordPress dependencies - */ -import { store as blocksStore } from '@wordpress/blocks'; - -/** - * Internal dependencies - */ -import { unlock } from '../lock-unlock'; -import { store as blockEditorStore } from '../store'; - -/** - * List of blocks and block attributes that can be bound. - */ -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 ) - ); -} - -/** - * Process the bound block attributes and return the values obtained from the bindings. - * - * @param {string} clientId - The block clientId. - * @param {Object} blockContext - The block context, which is needed for the binding sources. - * @param {Object} registry - The data registry. - * - * @return {Object} Object with the value obtained from the bindings of each bound attribute. - */ -export function getBoundAttributesValues( clientId, blockContext, registry ) { - const attributes = registry - .select( blockEditorStore ) - .getBlockAttributes( clientId ); - const bindings = attributes?.metadata?.bindings; - const boundAttributes = {}; - - if ( ! bindings ) { - return boundAttributes; - } - - const blockName = registry - .select( blockEditorStore ) - .getBlockName( clientId ); - const sources = unlock( - registry.select( blocksStore ) - ).getAllBlockBindingsSources(); - - for ( const [ attributeName, boundAttribute ] of Object.entries( - bindings - ) ) { - const source = sources[ boundAttribute.source ]; - if ( - ! source?.getValue || - ! canBindAttribute( blockName, attributeName ) - ) { - continue; - } - - const args = { - registry, - context: blockContext, - clientId, - attributeName, - args: boundAttribute.args, - }; - - boundAttributes[ attributeName ] = source.getValue( args ); - - if ( boundAttributes[ attributeName ] === undefined ) { - if ( attributeName === 'url' ) { - boundAttributes[ attributeName ] = null; - } else { - boundAttributes[ attributeName ] = - source.getPlaceholder?.( args ); - } - } - } - - return boundAttributes; -} From f10aa25b3139c9b2c2bc0d767e31d33ea06a6e76 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:58 +0200 Subject: [PATCH 59/60] Cover more blocks when editing custom fields --- .../editor/various/block-bindings.spec.js | 244 +++++++++++++++--- 1 file changed, 202 insertions(+), 42 deletions(-) diff --git a/test/e2e/specs/editor/various/block-bindings.spec.js b/test/e2e/specs/editor/various/block-bindings.spec.js index 7c7256b367566..87e5b2f2e10b1 100644 --- a/test/e2e/specs/editor/various/block-bindings.spec.js +++ b/test/e2e/specs/editor/various/block-bindings.spec.js @@ -1344,48 +1344,6 @@ test.describe( 'Block bindings', () => { await expect( newEmptyParagraph ).toBeEditable(); } ); - test( 'should be possible to edit the value of the custom field from the paragraph', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { - anchor: 'paragraph-binding', - content: 'paragraph default content', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const paragraphBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Paragraph', - } ); - - await expect( paragraphBlock ).toHaveText( - 'Value of the text_custom_field' - ); - await expect( paragraphBlock ).toHaveAttribute( - 'contenteditable', - 'true' - ); - await paragraphBlock.fill( 'new value' ); - // Check that the paragraph content attribute didn't change. - const [ paragraphBlockObject ] = await editor.getBlocks(); - expect( paragraphBlockObject.attributes.content ).toBe( - 'paragraph default content' - ); - // Check the value of the custom field is being updated by visiting the frontend. - const previewPage = await editor.openPreviewPage(); - await expect( - previewPage.locator( '#paragraph-binding' ) - ).toHaveText( 'new value' ); - } ); - test( 'should NOT be possible to edit the value of the custom field when it is protected', async ( { editor, } ) => { @@ -2006,6 +1964,208 @@ test.describe( 'Block bindings', () => { ); } ); } ); + + test.describe( 'Edit custom fields', () => { + test( 'should be possible to edit the value of the custom field from the paragraph', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + anchor: 'paragraph-binding', + content: 'paragraph default content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + + await expect( paragraphBlock ).toHaveAttribute( + 'contenteditable', + 'true' + ); + await paragraphBlock.fill( 'new value' ); + // Check that the paragraph content attribute didn't change. + const [ paragraphBlockObject ] = await editor.getBlocks(); + expect( paragraphBlockObject.attributes.content ).toBe( + 'paragraph default content' + ); + // Check the value of the custom field is being updated by visiting the frontend. + const previewPage = await editor.openPreviewPage(); + await expect( + previewPage.locator( '#paragraph-binding' ) + ).toHaveText( 'new value' ); + } ); + + test( 'should be possible to edit the value of the url custom field from the button', async ( { + editor, + page, + pageUtils, + } ) => { + await editor.insertBlock( { + name: 'core/buttons', + innerBlocks: [ + { + name: 'core/button', + attributes: { + anchor: 'button-url-binding', + text: 'button default text', + url: '#default-url', + metadata: { + bindings: { + url: { + source: 'core/post-meta', + args: { key: 'url_custom_field' }, + }, + }, + }, + }, + }, + ], + } ); + + // Edit the url. + const buttonBlock = editor.canvas + .getByRole( 'document', { + name: 'Block: Button', + exact: true, + } ) + .getByRole( 'textbox' ); + await buttonBlock.click(); + await page + .getByRole( 'button', { name: 'Edit link', exact: true } ) + .click(); + await page + .getByPlaceholder( 'Search or type url' ) + .fill( '#url-custom-field-modified' ); + await pageUtils.pressKeys( 'Enter' ); + + // Check that the button url attribute didn't change. + const [ buttonsObject ] = await editor.getBlocks(); + expect( buttonsObject.innerBlocks[ 0 ].attributes.url ).toBe( + '#default-url' + ); + // Check the value of the custom field is being updated by visiting the frontend. + const previewPage = await editor.openPreviewPage(); + await expect( + previewPage.locator( '#button-url-binding a' ) + ).toHaveAttribute( 'href', '#url-custom-field-modified' ); + } ); + + test( 'should be possible to edit the value of the url custom field from the image', async ( { + editor, + page, + pageUtils, + requestUtils, + } ) => { + const customFieldMedia = await requestUtils.uploadMedia( + path.join( + './test/e2e/assets', + '1024x768_e2e_test_image_size.jpeg' + ) + ); + imageCustomFieldSrc = customFieldMedia.source_url; + + await editor.insertBlock( { + name: 'core/image', + attributes: { + anchor: 'image-url-binding', + url: imagePlaceholderSrc, + alt: 'default alt value', + title: 'default title value', + metadata: { + bindings: { + url: { + source: 'core/post-meta', + args: { key: 'url_custom_field' }, + }, + }, + }, + }, + } ); + + // Edit image url. + await page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { + name: 'Replace', + } ) + .click(); + await page + .getByRole( 'button', { name: 'Edit link', exact: true } ) + .click(); + await page + .getByPlaceholder( 'Search or type url' ) + .fill( imageCustomFieldSrc ); + await pageUtils.pressKeys( 'Enter' ); + + // Check that the image url attribute didn't change and still uses the placeholder. + const [ imageBlockObject ] = await editor.getBlocks(); + expect( imageBlockObject.attributes.url ).toBe( + imagePlaceholderSrc + ); + + // Check the value of the custom field is being updated by visiting the frontend. + const previewPage = await editor.openPreviewPage(); + await expect( + previewPage.locator( '#image-url-binding img' ) + ).toHaveAttribute( 'src', imageCustomFieldSrc ); + } ); + + test( 'should be possible to edit the value of the text custom field from the image alt', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/image', + attributes: { + anchor: 'image-alt-binding', + url: imagePlaceholderSrc, + alt: 'default alt value', + metadata: { + bindings: { + alt: { + source: 'core/post-meta', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + } ); + const imageBlockImg = editor.canvas + .getByRole( 'document', { + name: 'Block: Image', + } ) + .locator( 'img' ); + await imageBlockImg.click(); + + // Edit the custom field value in the alt textarea. + const altInputArea = page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Alternative text' ); + await expect( altInputArea ).not.toHaveAttribute( 'readonly' ); + await altInputArea.fill( 'new value' ); + + // Check that the image alt attribute didn't change. + const [ imageBlockObject ] = await editor.getBlocks(); + expect( imageBlockObject.attributes.alt ).toBe( + 'default alt value' + ); + // Check the value of the custom field is being updated by visiting the frontend. + const previewPage = await editor.openPreviewPage(); + await expect( + previewPage.locator( '#image-alt-binding img' ) + ).toHaveAttribute( 'alt', 'new value' ); + } ); + } ); } ); } ); From c72afdabe993565196aee28b317d001a805f1702 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 May 2024 10:44:58 +0200 Subject: [PATCH 60/60] Add a warning when pasting blocks --- packages/block-editor/src/store/actions.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index e9f78add17244..db2c615dd5d6c 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -18,6 +18,7 @@ import { } from '@wordpress/blocks'; import { speak } from '@wordpress/a11y'; import { __, _n, sprintf } from '@wordpress/i18n'; +import { store as noticesStore } from '@wordpress/notices'; import { create, insert, remove, toHTMLString } from '@wordpress/rich-text'; import deprecated from '@wordpress/deprecated'; @@ -879,6 +880,20 @@ export const __unstableSplitSelection = // If the attribute is bound, don't split the selection and insert a new block instead. if ( bindings?.[ attributeKeyA ] ) { + // Show warning if user tries to insert a block into another block with bindings. + if ( blocks.length ) { + const { createWarningNotice } = + registry.dispatch( noticesStore ); + createWarningNotice( + __( + "Blocks can't be inserted into other blocks with bindings" + ), + { + type: 'snackbar', + } + ); + return; + } dispatch.insertAfterBlock( selectionA.clientId ); return; }