From 13e788ab39be1d543447a3d83bfe236e254ee7de Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 22 Jul 2024 09:56:24 +0200 Subject: [PATCH 01/20] Move bindings registration to editor provider --- .../src/{ => components/provider}/bindings/api.js | 0 .../provider}/bindings/pattern-overrides.js | 0 .../provider}/bindings/post-meta.js | 2 +- packages/editor/src/components/provider/index.js | 14 +++++++++++++- 4 files changed, 14 insertions(+), 2 deletions(-) rename packages/editor/src/{ => components/provider}/bindings/api.js (100%) rename packages/editor/src/{ => components/provider}/bindings/pattern-overrides.js (100%) rename packages/editor/src/{ => components/provider}/bindings/post-meta.js (97%) diff --git a/packages/editor/src/bindings/api.js b/packages/editor/src/components/provider/bindings/api.js similarity index 100% rename from packages/editor/src/bindings/api.js rename to packages/editor/src/components/provider/bindings/api.js diff --git a/packages/editor/src/bindings/pattern-overrides.js b/packages/editor/src/components/provider/bindings/pattern-overrides.js similarity index 100% rename from packages/editor/src/bindings/pattern-overrides.js rename to packages/editor/src/components/provider/bindings/pattern-overrides.js diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/components/provider/bindings/post-meta.js similarity index 97% rename from packages/editor/src/bindings/post-meta.js rename to packages/editor/src/components/provider/bindings/post-meta.js index aafc784a21bd4a..f28084555d5e2f 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/components/provider/bindings/post-meta.js @@ -6,7 +6,7 @@ import { store as coreDataStore } from '@wordpress/core-data'; /** * Internal dependencies */ -import { store as editorStore } from '../store'; +import { store as editorStore } from '../../../store'; export default { name: 'core/post-meta', diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index aaf25621d3324b..88bdc85b55cc47 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -12,7 +12,7 @@ import { } from '@wordpress/block-editor'; import { store as noticesStore } from '@wordpress/notices'; import { privateApis as editPatternsPrivateApis } from '@wordpress/patterns'; -import { createBlock } from '@wordpress/blocks'; +import { createBlock, store as blocksStore } from '@wordpress/blocks'; /** * Internal dependencies @@ -34,6 +34,8 @@ import EditorKeyboardShortcuts from '../global-keyboard-shortcuts'; import PatternRenameModal from '../pattern-rename-modal'; import PatternDuplicateModal from '../pattern-duplicate-modal'; import TemplatePartMenuItems from '../template-part-menu-items'; +import patternOverrides from './bindings/pattern-overrides'; +import postMeta from './bindings/post-meta'; const { ExperimentalBlockEditorProvider } = unlock( blockEditorPrivateApis ); const { PatternsMenuItems } = unlock( editPatternsPrivateApis ); @@ -223,6 +225,9 @@ export const ExperimentalEditorProvider = withRegistryProvider( setRenderingMode, } = unlock( useDispatch( editorStore ) ); const { createWarningNotice } = useDispatch( noticesStore ); + const { registerBlockBindingsSource } = unlock( + useDispatch( blocksStore ) + ); // Ideally this should be synced on each change and not just something you do once. useLayoutEffect( () => { @@ -271,6 +276,13 @@ export const ExperimentalEditorProvider = withRegistryProvider( setRenderingMode( settings.defaultRenderingMode ?? 'post-only' ); }, [ settings.defaultRenderingMode, setRenderingMode ] ); + // Register block bindings sources. + useEffect( () => { + // Initialize core sources. + registerBlockBindingsSource( postMeta ); + registerBlockBindingsSource( patternOverrides ); + }, [ postMeta, patternOverrides, registerBlockBindingsSource ] ); + useHideBlocksFromInserter( post.type, mode ); // Register the editor commands. From 01f79e5e9fe072d0daaed1c3fd0f529c8a4d81c1 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 22 Jul 2024 09:56:40 +0200 Subject: [PATCH 02/20] Remove sources from editor provider --- packages/editor/src/components/provider/index.js | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index 88bdc85b55cc47..aaf25621d3324b 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -12,7 +12,7 @@ import { } from '@wordpress/block-editor'; import { store as noticesStore } from '@wordpress/notices'; import { privateApis as editPatternsPrivateApis } from '@wordpress/patterns'; -import { createBlock, store as blocksStore } from '@wordpress/blocks'; +import { createBlock } from '@wordpress/blocks'; /** * Internal dependencies @@ -34,8 +34,6 @@ import EditorKeyboardShortcuts from '../global-keyboard-shortcuts'; import PatternRenameModal from '../pattern-rename-modal'; import PatternDuplicateModal from '../pattern-duplicate-modal'; import TemplatePartMenuItems from '../template-part-menu-items'; -import patternOverrides from './bindings/pattern-overrides'; -import postMeta from './bindings/post-meta'; const { ExperimentalBlockEditorProvider } = unlock( blockEditorPrivateApis ); const { PatternsMenuItems } = unlock( editPatternsPrivateApis ); @@ -225,9 +223,6 @@ export const ExperimentalEditorProvider = withRegistryProvider( setRenderingMode, } = unlock( useDispatch( editorStore ) ); const { createWarningNotice } = useDispatch( noticesStore ); - const { registerBlockBindingsSource } = unlock( - useDispatch( blocksStore ) - ); // Ideally this should be synced on each change and not just something you do once. useLayoutEffect( () => { @@ -276,13 +271,6 @@ export const ExperimentalEditorProvider = withRegistryProvider( setRenderingMode( settings.defaultRenderingMode ?? 'post-only' ); }, [ settings.defaultRenderingMode, setRenderingMode ] ); - // Register block bindings sources. - useEffect( () => { - // Initialize core sources. - registerBlockBindingsSource( postMeta ); - registerBlockBindingsSource( patternOverrides ); - }, [ postMeta, patternOverrides, registerBlockBindingsSource ] ); - useHideBlocksFromInserter( post.type, mode ); // Register the editor commands. From 43fe46f7b3600bf51c617162117c217551c45ca8 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 22 Jul 2024 09:59:05 +0200 Subject: [PATCH 03/20] Create `registerCoreBlockBindingsSources` function --- packages/editor/src/bindings/api.js | 27 ++++++ .../editor/src/bindings/pattern-overrides.js | 92 +++++++++++++++++++ packages/editor/src/bindings/post-meta.js | 68 ++++++++++++++ 3 files changed, 187 insertions(+) create mode 100644 packages/editor/src/bindings/api.js create mode 100644 packages/editor/src/bindings/pattern-overrides.js create mode 100644 packages/editor/src/bindings/post-meta.js diff --git a/packages/editor/src/bindings/api.js b/packages/editor/src/bindings/api.js new file mode 100644 index 00000000000000..0037f3334215b8 --- /dev/null +++ b/packages/editor/src/bindings/api.js @@ -0,0 +1,27 @@ +/** + * WordPress dependencies + */ +import { privateApis as blocksPrivateApis } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import patternOverrides from './pattern-overrides'; +import postMeta from './post-meta'; +import { unlock } from '../lock-unlock'; + +/** + * Function to register core block bindings sources provided by the editor. + * + * @example + * ```js + * import { registerCoreBlockBindingsSources } from '@wordpress/editor'; + * + * registerCoreBlockBindingsSources(); + * ``` + */ +export function registerCoreBlockBindingsSources() { + const { registerBlockBindingsSource } = unlock( blocksPrivateApis ); + registerBlockBindingsSource( patternOverrides ); + registerBlockBindingsSource( postMeta ); +} diff --git a/packages/editor/src/bindings/pattern-overrides.js b/packages/editor/src/bindings/pattern-overrides.js new file mode 100644 index 00000000000000..54ca77650a5fe9 --- /dev/null +++ b/packages/editor/src/bindings/pattern-overrides.js @@ -0,0 +1,92 @@ +/** + * WordPress dependencies + */ +import { _x } from '@wordpress/i18n'; +import { store as blockEditorStore } from '@wordpress/block-editor'; + +const CONTENT = 'content'; + +export default { + name: 'core/pattern-overrides', + label: _x( 'Pattern Overrides', 'block bindings source' ), + getValue( { registry, clientId, context, attributeName } ) { + const patternOverridesContent = context[ 'pattern/overrides' ]; + const { getBlockAttributes } = registry.select( blockEditorStore ); + const currentBlockAttributes = getBlockAttributes( clientId ); + + if ( ! patternOverridesContent ) { + return currentBlockAttributes[ attributeName ]; + } + + const overridableValue = + patternOverridesContent?.[ + currentBlockAttributes?.metadata?.name + ]?.[ attributeName ]; + + // If there is no pattern client ID, or it is not overwritten, return the default value. + if ( overridableValue === undefined ) { + return currentBlockAttributes[ attributeName ]; + } + + return overridableValue === '' ? undefined : overridableValue; + }, + setValues( { registry, clientId, attributes } ) { + const { getBlockAttributes, getBlockParentsByBlockName, getBlocks } = + registry.select( blockEditorStore ); + const currentBlockAttributes = getBlockAttributes( clientId ); + const blockName = currentBlockAttributes?.metadata?.name; + if ( ! blockName ) { + return; + } + + const [ patternClientId ] = getBlockParentsByBlockName( + clientId, + 'core/block', + true + ); + + // If there is no pattern client ID, sync blocks with the same name and same attributes. + if ( ! patternClientId ) { + const syncBlocksWithSameName = ( blocks ) => { + for ( const block of blocks ) { + if ( block.attributes?.metadata?.name === blockName ) { + registry + .dispatch( blockEditorStore ) + .updateBlockAttributes( + block.clientId, + attributes + ); + } + syncBlocksWithSameName( block.innerBlocks ); + } + }; + + syncBlocksWithSameName( getBlocks() ); + return; + } + const currentBindingValue = + getBlockAttributes( patternClientId )?.[ CONTENT ]; + registry + .dispatch( blockEditorStore ) + .updateBlockAttributes( patternClientId, { + [ CONTENT ]: { + ...currentBindingValue, + [ blockName ]: { + ...currentBindingValue?.[ blockName ], + ...Object.entries( attributes ).reduce( + ( acc, [ key, value ] ) => { + // TODO: We need a way to represent `undefined` in the serialized overrides. + // Also see: https://github.com/WordPress/gutenberg/pull/57249#discussion_r1452987871 + // We use an empty string to represent undefined for now until + // we support a richer format for overrides and the block bindings API. + acc[ key ] = value === undefined ? '' : value; + return acc; + }, + {} + ), + }, + }, + } ); + }, + canUserEditValue: () => true, +}; diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js new file mode 100644 index 00000000000000..aec890c5ceff87 --- /dev/null +++ b/packages/editor/src/bindings/post-meta.js @@ -0,0 +1,68 @@ +/** + * WordPress dependencies + */ +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' ), + getPlaceholder( { args } ) { + return 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 + .dispatch( coreDataStore ) + .editEntityRecord( 'postType', context?.postType, context?.postId, { + meta: { + [ args.key ]: value, + }, + } ); + }, + 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 false; + } + + // Check that the custom field is not protected and available in the REST API. + const isFieldExposed = !! select( coreDataStore ).getEntityRecord( + 'postType', + postType, + context?.postId + )?.meta?.[ args.key ]; + + if ( ! isFieldExposed ) { + return false; + } + + // Check that the user has the capability to edit post meta. + const canUserEdit = select( coreDataStore ).canUserEditEntityRecord( + 'postType', + context?.postType, + context?.postId + ); + if ( ! canUserEdit ) { + return false; + } + + return true; + }, +}; From f6050cf5b438c82ddc5d102351735302e3019af4 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 22 Jul 2024 10:02:09 +0200 Subject: [PATCH 04/20] Add `updateBlockBindingsSource` action --- packages/blocks/src/store/reducer.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/blocks/src/store/reducer.js b/packages/blocks/src/store/reducer.js index 5cffb0abc91973..03c623ee28f53e 100644 --- a/packages/blocks/src/store/reducer.js +++ b/packages/blocks/src/store/reducer.js @@ -396,6 +396,24 @@ export function blockBindingsSources( state = {}, action ) { case 'REMOVE_BLOCK_BINDINGS_SOURCE': return omit( state, action.name ); } + if ( action.type === 'UPDATE_BLOCK_BINDINGS_SOURCE' ) { + // Filter the name property and the undefined values. + const updatedProperties = Object.fromEntries( + Object.entries( action ).filter( + ( [ key, value ] ) => value !== undefined && key !== 'name' + ) + ); + + return { + ...state, + [ action.name ]: { + // Keep the existing properties. + ...state[ action.name ], + // Update with the new properties. + ...updatedProperties, + }, + }; + } return state; } From 235c59221fcfcaae00e9a1769b17ded0951998ca Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 22 Jul 2024 10:02:24 +0200 Subject: [PATCH 05/20] Bootstrap sources defined in the server --- packages/edit-post/src/index.js | 5 ++++- packages/edit-site/src/index.js | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/edit-post/src/index.js b/packages/edit-post/src/index.js index aa3473e6e55d45..4e62f9b67785b1 100644 --- a/packages/edit-post/src/index.js +++ b/packages/edit-post/src/index.js @@ -1,7 +1,10 @@ /** * WordPress dependencies */ -import { store as blocksStore } from '@wordpress/blocks'; +import { + store as blocksStore, + privateApis as blocksPrivateApis, +} from '@wordpress/blocks'; import { registerCoreBlocks, __experimentalRegisterExperimentalCoreBlocks, diff --git a/packages/edit-site/src/index.js b/packages/edit-site/src/index.js index 922e2f6ab933ab..582af78c8441e7 100644 --- a/packages/edit-site/src/index.js +++ b/packages/edit-site/src/index.js @@ -1,7 +1,10 @@ /** * WordPress dependencies */ -import { store as blocksStore } from '@wordpress/blocks'; +import { + store as blocksStore, + privateApis as blocksPrivateApis, +} from '@wordpress/blocks'; import { registerCoreBlocks, __experimentalGetCoreBlocks, From 6df5458edaf129a865621c5f079725aea8fe11eb Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 22 Jul 2024 10:02:47 +0200 Subject: [PATCH 06/20] Change registration to allow server bootstrap --- packages/blocks/src/api/registration.js | 31 ++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index 886309d8bd8f3e..d21e774ae1ae02 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -815,6 +815,31 @@ export const registerBlockBindingsSource = ( source ) => { return; } + // Check the properties from the server aren't overriden. + if ( existingSource ) { + /* + * It is not possible to just check the properties with a value because + * in some of them, like `canUserEditValue`, a default one could be used. + */ + const serverProperties = [ 'label', 'usesContext' ]; + let shouldReturn = false; + serverProperties.forEach( ( property ) => { + if ( existingSource[ property ] && source[ property ] ) { + console.error( + 'Block bindings "' + + name + + '" source "' + + property + + '" is already defined in the server.' + ); + shouldReturn = true; + } + } ); + if ( shouldReturn ) { + return; + } + } + // Check the `name` property is correct. if ( ! name ) { warning( 'Block bindings source must contain a name.' ); @@ -891,7 +916,11 @@ export const registerBlockBindingsSource = ( source ) => { return; } - return unlock( dispatch( blocksStore ) ).addBlockBindingsSource( source ); + if ( existingSource ) { + unlock( dispatch( blocksStore ) ).updateBlockBindingsSource( source ); + } else { + unlock( dispatch( blocksStore ) ).addBlockBindingsSource( source ); + } }; /** From b48c91fac757998352c2ba3b2498e66af516fc89 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 22 Jul 2024 10:02:47 +0200 Subject: [PATCH 07/20] Remove label from post meta and pattern overrides --- packages/editor/src/bindings/pattern-overrides.js | 2 -- packages/editor/src/bindings/post-meta.js | 2 -- 2 files changed, 4 deletions(-) diff --git a/packages/editor/src/bindings/pattern-overrides.js b/packages/editor/src/bindings/pattern-overrides.js index 54ca77650a5fe9..492406a9f8eb1a 100644 --- a/packages/editor/src/bindings/pattern-overrides.js +++ b/packages/editor/src/bindings/pattern-overrides.js @@ -1,14 +1,12 @@ /** * WordPress dependencies */ -import { _x } from '@wordpress/i18n'; import { store as blockEditorStore } from '@wordpress/block-editor'; const CONTENT = 'content'; export default { name: 'core/pattern-overrides', - label: _x( 'Pattern Overrides', 'block bindings source' ), getValue( { registry, clientId, context, attributeName } ) { const patternOverridesContent = context[ 'pattern/overrides' ]; const { getBlockAttributes } = registry.select( blockEditorStore ); diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index aec890c5ceff87..0173e5b5f63020 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -2,7 +2,6 @@ * WordPress dependencies */ import { store as coreDataStore } from '@wordpress/core-data'; -import { _x } from '@wordpress/i18n'; /** * Internal dependencies @@ -11,7 +10,6 @@ import { store as editorStore } from '../store'; export default { name: 'core/post-meta', - label: _x( 'Post Meta', 'block bindings source' ), getPlaceholder( { args } ) { return args.key; }, From 717913c16a8755a276cad83f4a1dc8ca7856d496 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 22 Jul 2024 10:02:57 +0200 Subject: [PATCH 08/20] Remove `updateBlockBindingsSource` --- packages/blocks/src/store/reducer.js | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/packages/blocks/src/store/reducer.js b/packages/blocks/src/store/reducer.js index 03c623ee28f53e..5cffb0abc91973 100644 --- a/packages/blocks/src/store/reducer.js +++ b/packages/blocks/src/store/reducer.js @@ -396,24 +396,6 @@ export function blockBindingsSources( state = {}, action ) { case 'REMOVE_BLOCK_BINDINGS_SOURCE': return omit( state, action.name ); } - if ( action.type === 'UPDATE_BLOCK_BINDINGS_SOURCE' ) { - // Filter the name property and the undefined values. - const updatedProperties = Object.fromEntries( - Object.entries( action ).filter( - ( [ key, value ] ) => value !== undefined && key !== 'name' - ) - ); - - return { - ...state, - [ action.name ]: { - // Keep the existing properties. - ...state[ action.name ], - // Update with the new properties. - ...updatedProperties, - }, - }; - } return state; } From 1f3be1da3db69455562c31282a1472f070b1fc55 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 22 Jul 2024 10:03:53 +0200 Subject: [PATCH 09/20] Use `bootstrapBlockBindingsSource` instead --- packages/blocks/src/api/registration.js | 33 +++++++++++++++++++++---- packages/blocks/src/store/reducer.js | 19 ++++++++++++++ 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index d21e774ae1ae02..e7d6fe2222d146 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -916,11 +916,34 @@ export const registerBlockBindingsSource = ( source ) => { return; } - if ( existingSource ) { - unlock( dispatch( blocksStore ) ).updateBlockBindingsSource( source ); - } else { - unlock( dispatch( blocksStore ) ).addBlockBindingsSource( source ); - } + unlock( dispatch( blocksStore ) ).addBlockBindingsSource( source ); +}; + +/** + * Bootstrap a new block bindings source with an object with initial properties. + * This function is meant to initialize block bindings sources from the server. + * + * @param {Object} source Properties of the source to be bootstrapped. + * @param {string} source.name The unique and machine-readable name. + * @param {string} source.label Human-readable label. + * @param {Array} [source.usesContext] Array of context needed by the source. + * + * @example + * ```js + * import { bootstrapBlockBindingsSource } from '@wordpress/blocks' + * + * bootstrapBlockBindingsSource( { + * name: 'plugin/my-custom-source', + * label: 'Label', + * usesContext: [ 'postId', 'postType' ], + * } ); + * ``` + */ +export const bootstrapBlockBindingsSource = ( source ) => { + // No need for validation as it usually happens in the server. + unlock( dispatch( blocksStore ) ).addBootstrappedBlockBindingsSource( + source + ); }; /** diff --git a/packages/blocks/src/store/reducer.js b/packages/blocks/src/store/reducer.js index 5cffb0abc91973..19979ca421e1b5 100644 --- a/packages/blocks/src/store/reducer.js +++ b/packages/blocks/src/store/reducer.js @@ -374,6 +374,25 @@ export function collections( state = {}, action ) { export function blockBindingsSources( state = {}, action ) { switch ( action.type ) { case 'ADD_BLOCK_BINDINGS_SOURCE': + // Filter the name property and the undefined values. + const newProperties = Object.fromEntries( + Object.entries( action ).filter( + ( [ key, value ] ) => value !== undefined && key !== 'name' + ) + ); + + return { + ...state, + [ action.name ]: { + // Keep the existing properties if it has been bootstrapped. + ...state[ action.name ], + // Update with the new properties. + ...newProperties, + canUserEditValue: + action.canUserEditValue || ( () => false ), + }, + }; + case 'ADD_BOOTSTRAPPED_BLOCK_BINDINGS_SOURCE': return { ...state, [ action.name ]: { From 19b2f8787c99aef8c9d4c72a08ff71538ecf0b0c Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 22 Jul 2024 10:04:08 +0200 Subject: [PATCH 10/20] Wrap server registration in the same function --- packages/blocks/src/api/registration.js | 27 --------------------- packages/edit-post/src/index.js | 5 +--- packages/edit-site/src/index.js | 5 +--- packages/editor/src/bindings/api.js | 32 ++++++++++++++++++++++++- 4 files changed, 33 insertions(+), 36 deletions(-) diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index e7d6fe2222d146..99a276da3504a5 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -919,33 +919,6 @@ export const registerBlockBindingsSource = ( source ) => { unlock( dispatch( blocksStore ) ).addBlockBindingsSource( source ); }; -/** - * Bootstrap a new block bindings source with an object with initial properties. - * This function is meant to initialize block bindings sources from the server. - * - * @param {Object} source Properties of the source to be bootstrapped. - * @param {string} source.name The unique and machine-readable name. - * @param {string} source.label Human-readable label. - * @param {Array} [source.usesContext] Array of context needed by the source. - * - * @example - * ```js - * import { bootstrapBlockBindingsSource } from '@wordpress/blocks' - * - * bootstrapBlockBindingsSource( { - * name: 'plugin/my-custom-source', - * label: 'Label', - * usesContext: [ 'postId', 'postType' ], - * } ); - * ``` - */ -export const bootstrapBlockBindingsSource = ( source ) => { - // No need for validation as it usually happens in the server. - unlock( dispatch( blocksStore ) ).addBootstrappedBlockBindingsSource( - source - ); -}; - /** * Unregisters a block bindings source * diff --git a/packages/edit-post/src/index.js b/packages/edit-post/src/index.js index 4e62f9b67785b1..aa3473e6e55d45 100644 --- a/packages/edit-post/src/index.js +++ b/packages/edit-post/src/index.js @@ -1,10 +1,7 @@ /** * WordPress dependencies */ -import { - store as blocksStore, - privateApis as blocksPrivateApis, -} from '@wordpress/blocks'; +import { store as blocksStore } from '@wordpress/blocks'; import { registerCoreBlocks, __experimentalRegisterExperimentalCoreBlocks, diff --git a/packages/edit-site/src/index.js b/packages/edit-site/src/index.js index 582af78c8441e7..922e2f6ab933ab 100644 --- a/packages/edit-site/src/index.js +++ b/packages/edit-site/src/index.js @@ -1,10 +1,7 @@ /** * WordPress dependencies */ -import { - store as blocksStore, - privateApis as blocksPrivateApis, -} from '@wordpress/blocks'; +import { store as blocksStore } from '@wordpress/blocks'; import { registerCoreBlocks, __experimentalGetCoreBlocks, diff --git a/packages/editor/src/bindings/api.js b/packages/editor/src/bindings/api.js index 0037f3334215b8..2cfed5168a143e 100644 --- a/packages/editor/src/bindings/api.js +++ b/packages/editor/src/bindings/api.js @@ -1,7 +1,11 @@ /** * WordPress dependencies */ -import { privateApis as blocksPrivateApis } from '@wordpress/blocks'; +import { + privateApis as blocksPrivateApis, + store as blocksStore, +} from '@wordpress/blocks'; +import { dispatch } from '@wordpress/data'; /** * Internal dependencies @@ -25,3 +29,29 @@ export function registerCoreBlockBindingsSources() { registerBlockBindingsSource( patternOverrides ); registerBlockBindingsSource( postMeta ); } + +/** + * Function to bootstrap core block bindings sources defined in the server. + * + * @param {Object} sources Object containing the sources to bootstrap. + * + * @example + * ```js + * import { bootstrapBlockBindingsSourcesFromServer } from '@wordpress/editor'; + * + * bootstrapBlockBindingsSourcesFromServer( sources ); + * ``` + */ +export function bootstrapBlockBindingsSourcesFromServer( sources ) { + if ( sources ) { + const { addBootstrappedBlockBindingsSource } = unlock( + dispatch( blocksStore ) + ); + for ( const [ name, args ] of Object.entries( sources ) ) { + addBootstrappedBlockBindingsSource( { + name, + ...args, + } ); + } + } +} From a81c1764ab353960dde0bf29f82a911c0f2687f6 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 22 Jul 2024 10:05:36 +0200 Subject: [PATCH 11/20] Change how label is managed --- packages/blocks/src/api/registration.js | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index 99a276da3504a5..7f56b9949cf2af 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -815,31 +815,6 @@ export const registerBlockBindingsSource = ( source ) => { return; } - // Check the properties from the server aren't overriden. - if ( existingSource ) { - /* - * It is not possible to just check the properties with a value because - * in some of them, like `canUserEditValue`, a default one could be used. - */ - const serverProperties = [ 'label', 'usesContext' ]; - let shouldReturn = false; - serverProperties.forEach( ( property ) => { - if ( existingSource[ property ] && source[ property ] ) { - console.error( - 'Block bindings "' + - name + - '" source "' + - property + - '" is already defined in the server.' - ); - shouldReturn = true; - } - } ); - if ( shouldReturn ) { - return; - } - } - // Check the `name` property is correct. if ( ! name ) { warning( 'Block bindings source must contain a name.' ); From a47e14bdd1da6abab34a61a17ccf048b7fa26e5f Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 22 Jul 2024 10:05:50 +0200 Subject: [PATCH 12/20] Remove type from object --- packages/blocks/src/store/reducer.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/blocks/src/store/reducer.js b/packages/blocks/src/store/reducer.js index 19979ca421e1b5..68b5fae37f4931 100644 --- a/packages/blocks/src/store/reducer.js +++ b/packages/blocks/src/store/reducer.js @@ -374,10 +374,11 @@ export function collections( state = {}, action ) { export function blockBindingsSources( state = {}, action ) { switch ( action.type ) { case 'ADD_BLOCK_BINDINGS_SOURCE': - // Filter the name property and the undefined values. + // Filter the name property, the type property, and the undefined values. const newProperties = Object.fromEntries( Object.entries( action ).filter( - ( [ key, value ] ) => value !== undefined && key !== 'name' + ( [ key, value ] ) => + value !== undefined && key !== 'name' && key !== 'type' ) ); From 0c624ccd0e6f55dbc543ef55a63e40ec8bb1d9ab Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 22 Jul 2024 10:06:23 +0200 Subject: [PATCH 13/20] Add usesContext to the store --- packages/blocks/src/store/private-actions.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/blocks/src/store/private-actions.js b/packages/blocks/src/store/private-actions.js index 6f7581da53de36..5440a74f39d8bb 100644 --- a/packages/blocks/src/store/private-actions.js +++ b/packages/blocks/src/store/private-actions.js @@ -52,6 +52,7 @@ export function addBlockBindingsSource( source ) { name: source.name, label: source.label, getValues: source.getValues, + usesContext: source.usesContext, setValues: source.setValues, getPlaceholder: source.getPlaceholder, canUserEditValue: source.canUserEditValue, From f1cc726715f5a6b2fbb9a4070dbb5c0b235cb6b7 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 22 Jul 2024 10:09:42 +0200 Subject: [PATCH 14/20] Handle usesContext validation --- packages/blocks/src/api/registration.js | 8 ++++++++ packages/blocks/src/api/test/registration.js | 14 ++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index 7f56b9949cf2af..add3c9684b24fa 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -770,6 +770,7 @@ export const unregisterBlockVariation = ( blockName, variationName ) => { * @param {Object} source Properties of the source to be registered. * @param {string} source.name The unique and machine-readable name. * @param {string} [source.label] Human-readable label. + * @param {Array} [source.usesContext] Array of context needed by the source only in the editor. * @param {Function} [source.getValues] Function to get the values from the source. * @param {Function} [source.setValues] Function to update multiple values connected to the source. * @param {Function} [source.getPlaceholder] Function to get the placeholder when the value is undefined. @@ -794,6 +795,7 @@ export const registerBlockBindingsSource = ( source ) => { const { name, label, + usesContext, getValues, setValues, getPlaceholder, @@ -867,6 +869,12 @@ export const registerBlockBindingsSource = ( source ) => { return; } + // Check the `usesContext` property is correct. + if ( usesContext && ! Array.isArray( usesContext ) ) { + console.error( 'Block bindings source usesContext must be an array.' ); + return; + } + // Check the `getValues` property is correct. if ( getValues && typeof getValues !== 'function' ) { warning( 'Block bindings source getValues must be a function.' ); diff --git a/packages/blocks/src/api/test/registration.js b/packages/blocks/src/api/test/registration.js index d36abee2930bfa..98cde8312d4428 100644 --- a/packages/blocks/src/api/test/registration.js +++ b/packages/blocks/src/api/test/registration.js @@ -1528,6 +1528,18 @@ describe( 'blocks', () => { ); } ); + // Check the `usesContext` array is correct. + it( 'should reject invalid usesContext property', () => { + registerBlockBindingsSource( { + name: 'core/testing', + label: 'testing', + usesContext: 'should be an array', + } ); + expect( console ).toHaveErroredWith( + 'Block bindings source usesContext must be an array.' + ); + } ); + // Check the `getValues` callback is correct. it( 'should reject invalid getValues callback', () => { registerBlockBindingsSource( { @@ -1584,6 +1596,7 @@ describe( 'blocks', () => { it( 'should register a valid source', () => { const sourceProperties = { label: 'Valid Source', + usesContext: [ 'postId' ], getValues: () => 'value', setValues: () => 'new values', getPlaceholder: () => 'placeholder', @@ -1605,6 +1618,7 @@ describe( 'blocks', () => { label: 'Valid Source', } ); const source = getBlockBindingsSource( 'core/valid-source' ); + expect( source.usesContext ).toBeUndefined(); expect( source.getValues ).toBeUndefined(); expect( source.setValues ).toBeUndefined(); expect( source.getPlaceholder ).toBeUndefined(); From 9a4d9519cd48551af4f84e8f485924085116c545 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 22 Jul 2024 10:11:17 +0200 Subject: [PATCH 15/20] Merge usesContext and add tests --- packages/blocks/src/api/registration.js | 16 +++++- packages/blocks/src/api/test/registration.js | 56 ++++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index add3c9684b24fa..a7fc048d9ada06 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -899,7 +899,21 @@ export const registerBlockBindingsSource = ( source ) => { return; } - unlock( dispatch( blocksStore ) ).addBlockBindingsSource( source ); + // Merge context from server and client. + let mergedUsesContext = [ + ...( existingSource?.usesContext || [] ), + ...( usesContext || [] ), + ]; + // Remove duplicates. + mergedUsesContext = + mergedUsesContext.length > 0 + ? [ ...new Set( mergedUsesContext ) ] + : undefined; + + unlock( dispatch( blocksStore ) ).addBlockBindingsSource( { + ...source, + usesContext: mergedUsesContext, + } ); }; /** diff --git a/packages/blocks/src/api/test/registration.js b/packages/blocks/src/api/test/registration.js index 98cde8312d4428..f45cd229eb05f7 100644 --- a/packages/blocks/src/api/test/registration.js +++ b/packages/blocks/src/api/test/registration.js @@ -1540,6 +1540,62 @@ describe( 'blocks', () => { ); } ); + it( 'should add usesContext when only defined in the client', () => { + // Simulate server bootstrap. + registerBlockBindingsSource( { + name: 'core/testing', + label: 'testing', + usesContext: [ 'postId', 'postType' ], + } ); + // Register source in the client without usesContext. + registerBlockBindingsSource( { + name: 'core/testing', + getValue: () => 'value', + } ); + const source = getBlockBindingsSource( 'core/testing' ); + unregisterBlockBindingsSource( 'core/testing' ); + expect( source.usesContext ).toEqual( [ 'postId', 'postType' ] ); + } ); + + it( 'should keep usesContext when it is not defined in the client', () => { + // Simulate server bootstrap. + registerBlockBindingsSource( { + name: 'core/testing', + label: 'testing', + } ); + // Register source in the client with usesContext. + registerBlockBindingsSource( { + name: 'core/testing', + usesContext: [ 'postId', 'postType' ], + getValue: () => 'value', + } ); + const source = getBlockBindingsSource( 'core/testing' ); + unregisterBlockBindingsSource( 'core/testing' ); + expect( source.usesContext ).toEqual( [ 'postId', 'postType' ] ); + } ); + + it( 'should merge usesContext from server and client without duplicates', () => { + // Simulate server bootstrap. + registerBlockBindingsSource( { + name: 'core/testing', + label: 'testing', + usesContext: [ 'postId', 'postType' ], + } ); + // Register source in the client with usesContext. + registerBlockBindingsSource( { + name: 'core/testing', + usesContext: [ 'postType', 'clientContext' ], + getValue: () => 'value', + } ); + const source = getBlockBindingsSource( 'core/testing' ); + unregisterBlockBindingsSource( 'core/testing' ); + expect( source.usesContext ).toEqual( [ + 'postId', + 'postType', + 'clientContext', + ] ); + } ); + // Check the `getValues` callback is correct. it( 'should reject invalid getValues callback', () => { registerBlockBindingsSource( { From 0cc18123992b17e9561cffc6645782b79364f232 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 22 Jul 2024 10:16:15 +0200 Subject: [PATCH 16/20] Read `usesContext` during bindings processing --- .../src/hooks/use-bindings-attributes.js | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/block-editor/src/hooks/use-bindings-attributes.js b/packages/block-editor/src/hooks/use-bindings-attributes.js index 410ad93bd971a7..e8a55e9fb42f04 100644 --- a/packages/block-editor/src/hooks/use-bindings-attributes.js +++ b/packages/block-editor/src/hooks/use-bindings-attributes.js @@ -4,13 +4,14 @@ import { store as blocksStore } from '@wordpress/blocks'; import { createHigherOrderComponent } from '@wordpress/compose'; import { useRegistry, useSelect } from '@wordpress/data'; -import { useCallback, useMemo } from '@wordpress/element'; +import { useCallback, useMemo, useContext } from '@wordpress/element'; import { addFilter } from '@wordpress/hooks'; /** * Internal dependencies */ import { unlock } from '../lock-unlock'; +import BlockContext from '../components/block-context'; /** @typedef {import('@wordpress/compose').WPHigherOrderComponent} WPHigherOrderComponent */ /** @typedef {import('@wordpress/blocks').WPBlockSettings} WPBlockSettings */ @@ -93,10 +94,11 @@ export function canBindAttribute( blockName, attributeName ) { export const withBlockBindingSupport = createHigherOrderComponent( ( BlockEdit ) => ( props ) => { const registry = useRegistry(); + const blockContext = useContext( BlockContext ); const sources = useSelect( ( select ) => unlock( select( blocksStore ) ).getAllBlockBindingsSources() ); - const { name, clientId, context } = props; + const { name, clientId } = props; const hasParentPattern = !! props.context[ 'pattern/overrides' ]; const hasPatternOverridesDefaultBinding = props.attributes.metadata?.bindings?.[ DEFAULT_ATTRIBUTE ] @@ -145,6 +147,15 @@ export const withBlockBindingSupport = createHigherOrderComponent( if ( blockBindingsBySource.size ) { for ( const [ source, bindings ] of blockBindingsBySource ) { + // Populate context. + const context = {}; + + if ( source.usesContext?.length ) { + for ( const key of source.usesContext ) { + context[ key ] = blockContext[ key ]; + } + } + // Get values in batch if the source supports it. const values = source.getValues( { registry, @@ -177,7 +188,7 @@ export const withBlockBindingSupport = createHigherOrderComponent( } return attributes; - }, [ blockBindings, name, clientId, context, registry, sources ] ); + }, [ bindings, name, clientId, blockContext, registry, sources ] ); const { setAttributes } = props; @@ -223,6 +234,15 @@ export const withBlockBindingSupport = createHigherOrderComponent( source, bindings, ] of blockBindingsBySource ) { + // Populate context. + const context = {}; + + if ( source.usesContext?.length ) { + for ( const key of source.usesContext ) { + context[ key ] = blockContext[ key ]; + } + } + source.setValues( { registry, context, @@ -255,7 +275,7 @@ export const withBlockBindingSupport = createHigherOrderComponent( blockBindings, name, clientId, - context, + blockContext, setAttributes, sources, hasPatternOverridesDefaultBinding, From 9fcf841a434624b221c198047c3a1be13d9286fe Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 22 Jul 2024 10:36:31 +0200 Subject: [PATCH 17/20] Adapt code after rebase --- .../src/hooks/use-bindings-attributes.js | 2 +- packages/blocks/src/api/registration.js | 18 +------- packages/blocks/src/store/private-actions.js | 2 +- packages/blocks/src/store/reducer.js | 30 +++++-------- .../editor/src/bindings/pattern-overrides.js | 43 ++++++++++++------- packages/editor/src/bindings/post-meta.js | 36 ++++++++++------ 6 files changed, 67 insertions(+), 64 deletions(-) diff --git a/packages/block-editor/src/hooks/use-bindings-attributes.js b/packages/block-editor/src/hooks/use-bindings-attributes.js index e8a55e9fb42f04..54b5425bfc44a5 100644 --- a/packages/block-editor/src/hooks/use-bindings-attributes.js +++ b/packages/block-editor/src/hooks/use-bindings-attributes.js @@ -188,7 +188,7 @@ export const withBlockBindingSupport = createHigherOrderComponent( } return attributes; - }, [ bindings, name, clientId, blockContext, registry, sources ] ); + }, [ blockBindings, name, clientId, blockContext, registry, sources ] ); const { setAttributes } = props; diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index a7fc048d9ada06..9b2f0f8bf5a3cd 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -871,7 +871,7 @@ export const registerBlockBindingsSource = ( source ) => { // Check the `usesContext` property is correct. if ( usesContext && ! Array.isArray( usesContext ) ) { - console.error( 'Block bindings source usesContext must be an array.' ); + warning( 'Block bindings source usesContext must be an array.' ); return; } @@ -899,21 +899,7 @@ export const registerBlockBindingsSource = ( source ) => { return; } - // Merge context from server and client. - let mergedUsesContext = [ - ...( existingSource?.usesContext || [] ), - ...( usesContext || [] ), - ]; - // Remove duplicates. - mergedUsesContext = - mergedUsesContext.length > 0 - ? [ ...new Set( mergedUsesContext ) ] - : undefined; - - unlock( dispatch( blocksStore ) ).addBlockBindingsSource( { - ...source, - usesContext: mergedUsesContext, - } ); + unlock( dispatch( blocksStore ) ).addBlockBindingsSource( source ); }; /** diff --git a/packages/blocks/src/store/private-actions.js b/packages/blocks/src/store/private-actions.js index 5440a74f39d8bb..977270cf1d0c97 100644 --- a/packages/blocks/src/store/private-actions.js +++ b/packages/blocks/src/store/private-actions.js @@ -51,8 +51,8 @@ export function addBlockBindingsSource( source ) { type: 'ADD_BLOCK_BINDINGS_SOURCE', name: source.name, label: source.label, - getValues: source.getValues, usesContext: source.usesContext, + getValues: source.getValues, setValues: source.setValues, getPlaceholder: source.getPlaceholder, canUserEditValue: source.canUserEditValue, diff --git a/packages/blocks/src/store/reducer.js b/packages/blocks/src/store/reducer.js index 68b5fae37f4931..5e0714b6064fde 100644 --- a/packages/blocks/src/store/reducer.js +++ b/packages/blocks/src/store/reducer.js @@ -374,31 +374,23 @@ export function collections( state = {}, action ) { export function blockBindingsSources( state = {}, action ) { switch ( action.type ) { case 'ADD_BLOCK_BINDINGS_SOURCE': - // Filter the name property, the type property, and the undefined values. - const newProperties = Object.fromEntries( - Object.entries( action ).filter( - ( [ key, value ] ) => - value !== undefined && key !== 'name' && key !== 'type' - ) - ); + // Merge usesContext with existing values, potentially defined in the server registration. + let mergedUsesContext = [ + ...( state[ action.name ]?.usesContext || [] ), + ...( action.usesContext || [] ), + ]; + // Remove duplicates. + mergedUsesContext = + mergedUsesContext.length > 0 + ? [ ...new Set( mergedUsesContext ) ] + : undefined; - return { - ...state, - [ action.name ]: { - // Keep the existing properties if it has been bootstrapped. - ...state[ action.name ], - // Update with the new properties. - ...newProperties, - canUserEditValue: - action.canUserEditValue || ( () => false ), - }, - }; - case 'ADD_BOOTSTRAPPED_BLOCK_BINDINGS_SOURCE': return { ...state, [ action.name ]: { // Don't override the label if it's already set. label: state[ action.name ]?.label || action.label, + usesContext: mergedUsesContext, getValues: action.getValues, setValues: action.setValues, getPlaceholder: action.getPlaceholder, diff --git a/packages/editor/src/bindings/pattern-overrides.js b/packages/editor/src/bindings/pattern-overrides.js index 492406a9f8eb1a..88c6c73bdc61c1 100644 --- a/packages/editor/src/bindings/pattern-overrides.js +++ b/packages/editor/src/bindings/pattern-overrides.js @@ -7,28 +7,32 @@ const CONTENT = 'content'; export default { name: 'core/pattern-overrides', - getValue( { registry, clientId, context, attributeName } ) { + getValues( { registry, clientId, context, bindings } ) { const patternOverridesContent = context[ 'pattern/overrides' ]; const { getBlockAttributes } = registry.select( blockEditorStore ); const currentBlockAttributes = getBlockAttributes( clientId ); - if ( ! patternOverridesContent ) { - return currentBlockAttributes[ attributeName ]; - } - - const overridableValue = - patternOverridesContent?.[ - currentBlockAttributes?.metadata?.name - ]?.[ attributeName ]; + const overridesValues = {}; + for ( const attributeName of Object.keys( bindings ) ) { + const overridableValue = + patternOverridesContent?.[ + currentBlockAttributes?.metadata?.name + ]?.[ attributeName ]; - // If there is no pattern client ID, or it is not overwritten, return the default value. - if ( overridableValue === undefined ) { - return currentBlockAttributes[ attributeName ]; + // If it has not been overriden, return the original value. + // Check undefined because empty string is a valid value. + if ( overridableValue === undefined ) { + overridesValues[ attributeName ] = + currentBlockAttributes[ attributeName ]; + continue; + } else { + overridesValues[ attributeName ] = + overridableValue === '' ? undefined : overridableValue; + } } - - return overridableValue === '' ? undefined : overridableValue; + return overridesValues; }, - setValues( { registry, clientId, attributes } ) { + setValues( { registry, clientId, bindings } ) { const { getBlockAttributes, getBlockParentsByBlockName, getBlocks } = registry.select( blockEditorStore ); const currentBlockAttributes = getBlockAttributes( clientId ); @@ -43,6 +47,15 @@ export default { true ); + // Extract the updated attributes from the source bindings. + const attributes = Object.entries( bindings ).reduce( + ( attrs, [ key, { newValue } ] ) => { + attrs[ key ] = newValue; + return attrs; + }, + {} + ); + // If there is no pattern client ID, sync blocks with the same name and same attributes. if ( ! patternClientId ) { const syncBlocksWithSameName = ( blocks ) => { diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index 0173e5b5f63020..aafc784a21bd4a 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -13,25 +13,37 @@ export default { getPlaceholder( { args } ) { return args.key; }, - getValue( { registry, context, args } ) { - return registry + getValues( { registry, context, bindings } ) { + const meta = registry .select( coreDataStore ) .getEditedEntityRecord( 'postType', context?.postType, context?.postId - ).meta?.[ args.key ]; + )?.meta; + const newValues = {}; + for ( const [ attributeName, source ] of Object.entries( bindings ) ) { + newValues[ attributeName ] = meta?.[ source.args.key ]; + } + return newValues; }, - setValue( { registry, context, args, value } ) { + setValues( { registry, context, bindings } ) { + const newMeta = {}; + Object.values( bindings ).forEach( ( { args, newValue } ) => { + newMeta[ args.key ] = newValue; + } ); registry .dispatch( coreDataStore ) .editEntityRecord( 'postType', context?.postType, context?.postId, { - meta: { - [ args.key ]: value, - }, + meta: newMeta, } ); }, canUserEditValue( { select, context, args } ) { + // Lock editing in query loop. + if ( context?.query || context?.queryId ) { + return false; + } + const postType = context?.postType || select( editorStore ).getCurrentPostType(); @@ -52,11 +64,11 @@ export default { } // Check that the user has the capability to edit post meta. - const canUserEdit = select( coreDataStore ).canUserEditEntityRecord( - 'postType', - context?.postType, - context?.postId - ); + const canUserEdit = select( coreDataStore ).canUser( 'update', { + kind: 'postType', + name: context?.postType, + id: context?.postId, + } ); if ( ! canUserEdit ) { return false; } From b898110aecaaac6b8535295dbcd5dfaab08878e6 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 22 Jul 2024 10:39:22 +0200 Subject: [PATCH 18/20] More changes after rebase --- packages/blocks/src/api/registration.js | 2 +- .../src/components/provider/bindings/api.js | 57 ---------- .../provider/bindings/pattern-overrides.js | 103 ------------------ .../components/provider/bindings/post-meta.js | 78 ------------- 4 files changed, 1 insertion(+), 239 deletions(-) delete mode 100644 packages/editor/src/components/provider/bindings/api.js delete mode 100644 packages/editor/src/components/provider/bindings/pattern-overrides.js delete mode 100644 packages/editor/src/components/provider/bindings/post-meta.js diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index 9b2f0f8bf5a3cd..7cce959c78cc80 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -899,7 +899,7 @@ export const registerBlockBindingsSource = ( source ) => { return; } - unlock( dispatch( blocksStore ) ).addBlockBindingsSource( source ); + return unlock( dispatch( blocksStore ) ).addBlockBindingsSource( source ); }; /** diff --git a/packages/editor/src/components/provider/bindings/api.js b/packages/editor/src/components/provider/bindings/api.js deleted file mode 100644 index 2cfed5168a143e..00000000000000 --- a/packages/editor/src/components/provider/bindings/api.js +++ /dev/null @@ -1,57 +0,0 @@ -/** - * WordPress dependencies - */ -import { - privateApis as blocksPrivateApis, - store as blocksStore, -} from '@wordpress/blocks'; -import { dispatch } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import patternOverrides from './pattern-overrides'; -import postMeta from './post-meta'; -import { unlock } from '../lock-unlock'; - -/** - * Function to register core block bindings sources provided by the editor. - * - * @example - * ```js - * import { registerCoreBlockBindingsSources } from '@wordpress/editor'; - * - * registerCoreBlockBindingsSources(); - * ``` - */ -export function registerCoreBlockBindingsSources() { - const { registerBlockBindingsSource } = unlock( blocksPrivateApis ); - registerBlockBindingsSource( patternOverrides ); - registerBlockBindingsSource( postMeta ); -} - -/** - * Function to bootstrap core block bindings sources defined in the server. - * - * @param {Object} sources Object containing the sources to bootstrap. - * - * @example - * ```js - * import { bootstrapBlockBindingsSourcesFromServer } from '@wordpress/editor'; - * - * bootstrapBlockBindingsSourcesFromServer( sources ); - * ``` - */ -export function bootstrapBlockBindingsSourcesFromServer( sources ) { - if ( sources ) { - const { addBootstrappedBlockBindingsSource } = unlock( - dispatch( blocksStore ) - ); - for ( const [ name, args ] of Object.entries( sources ) ) { - addBootstrappedBlockBindingsSource( { - name, - ...args, - } ); - } - } -} diff --git a/packages/editor/src/components/provider/bindings/pattern-overrides.js b/packages/editor/src/components/provider/bindings/pattern-overrides.js deleted file mode 100644 index 88c6c73bdc61c1..00000000000000 --- a/packages/editor/src/components/provider/bindings/pattern-overrides.js +++ /dev/null @@ -1,103 +0,0 @@ -/** - * WordPress dependencies - */ -import { store as blockEditorStore } from '@wordpress/block-editor'; - -const CONTENT = 'content'; - -export default { - name: 'core/pattern-overrides', - getValues( { registry, clientId, context, bindings } ) { - const patternOverridesContent = context[ 'pattern/overrides' ]; - const { getBlockAttributes } = registry.select( blockEditorStore ); - const currentBlockAttributes = getBlockAttributes( clientId ); - - const overridesValues = {}; - for ( const attributeName of Object.keys( bindings ) ) { - const overridableValue = - patternOverridesContent?.[ - currentBlockAttributes?.metadata?.name - ]?.[ attributeName ]; - - // If it has not been overriden, return the original value. - // Check undefined because empty string is a valid value. - if ( overridableValue === undefined ) { - overridesValues[ attributeName ] = - currentBlockAttributes[ attributeName ]; - continue; - } else { - overridesValues[ attributeName ] = - overridableValue === '' ? undefined : overridableValue; - } - } - return overridesValues; - }, - setValues( { registry, clientId, bindings } ) { - const { getBlockAttributes, getBlockParentsByBlockName, getBlocks } = - registry.select( blockEditorStore ); - const currentBlockAttributes = getBlockAttributes( clientId ); - const blockName = currentBlockAttributes?.metadata?.name; - if ( ! blockName ) { - return; - } - - const [ patternClientId ] = getBlockParentsByBlockName( - clientId, - 'core/block', - true - ); - - // Extract the updated attributes from the source bindings. - const attributes = Object.entries( bindings ).reduce( - ( attrs, [ key, { newValue } ] ) => { - attrs[ key ] = newValue; - return attrs; - }, - {} - ); - - // If there is no pattern client ID, sync blocks with the same name and same attributes. - if ( ! patternClientId ) { - const syncBlocksWithSameName = ( blocks ) => { - for ( const block of blocks ) { - if ( block.attributes?.metadata?.name === blockName ) { - registry - .dispatch( blockEditorStore ) - .updateBlockAttributes( - block.clientId, - attributes - ); - } - syncBlocksWithSameName( block.innerBlocks ); - } - }; - - syncBlocksWithSameName( getBlocks() ); - return; - } - const currentBindingValue = - getBlockAttributes( patternClientId )?.[ CONTENT ]; - registry - .dispatch( blockEditorStore ) - .updateBlockAttributes( patternClientId, { - [ CONTENT ]: { - ...currentBindingValue, - [ blockName ]: { - ...currentBindingValue?.[ blockName ], - ...Object.entries( attributes ).reduce( - ( acc, [ key, value ] ) => { - // TODO: We need a way to represent `undefined` in the serialized overrides. - // Also see: https://github.com/WordPress/gutenberg/pull/57249#discussion_r1452987871 - // We use an empty string to represent undefined for now until - // we support a richer format for overrides and the block bindings API. - acc[ key ] = value === undefined ? '' : value; - return acc; - }, - {} - ), - }, - }, - } ); - }, - canUserEditValue: () => true, -}; diff --git a/packages/editor/src/components/provider/bindings/post-meta.js b/packages/editor/src/components/provider/bindings/post-meta.js deleted file mode 100644 index f28084555d5e2f..00000000000000 --- a/packages/editor/src/components/provider/bindings/post-meta.js +++ /dev/null @@ -1,78 +0,0 @@ -/** - * WordPress dependencies - */ -import { store as coreDataStore } from '@wordpress/core-data'; - -/** - * Internal dependencies - */ -import { store as editorStore } from '../../../store'; - -export default { - name: 'core/post-meta', - getPlaceholder( { args } ) { - return args.key; - }, - getValues( { registry, context, bindings } ) { - const meta = registry - .select( coreDataStore ) - .getEditedEntityRecord( - 'postType', - context?.postType, - context?.postId - )?.meta; - const newValues = {}; - for ( const [ attributeName, source ] of Object.entries( bindings ) ) { - newValues[ attributeName ] = meta?.[ source.args.key ]; - } - return newValues; - }, - setValues( { registry, context, bindings } ) { - const newMeta = {}; - Object.values( bindings ).forEach( ( { args, newValue } ) => { - newMeta[ args.key ] = newValue; - } ); - registry - .dispatch( coreDataStore ) - .editEntityRecord( 'postType', context?.postType, context?.postId, { - meta: newMeta, - } ); - }, - canUserEditValue( { select, context, args } ) { - // Lock editing in query loop. - if ( context?.query || context?.queryId ) { - return false; - } - - 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 false; - } - - // Check that the custom field is not protected and available in the REST API. - const isFieldExposed = !! select( coreDataStore ).getEntityRecord( - 'postType', - postType, - context?.postId - )?.meta?.[ args.key ]; - - if ( ! isFieldExposed ) { - return false; - } - - // Check that the user has the capability to edit post meta. - const canUserEdit = select( coreDataStore ).canUser( 'update', { - kind: 'postType', - name: context?.postType, - id: context?.postId, - } ); - if ( ! canUserEdit ) { - return false; - } - - return true; - }, -}; From 5ba135a43336f0036045ea3678d993fa87a1680a Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 22 Jul 2024 10:43:48 +0200 Subject: [PATCH 19/20] Adapt unit test --- packages/blocks/src/api/test/registration.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/blocks/src/api/test/registration.js b/packages/blocks/src/api/test/registration.js index f45cd229eb05f7..e6ea9909d8763c 100644 --- a/packages/blocks/src/api/test/registration.js +++ b/packages/blocks/src/api/test/registration.js @@ -1535,7 +1535,7 @@ describe( 'blocks', () => { label: 'testing', usesContext: 'should be an array', } ); - expect( console ).toHaveErroredWith( + expect( console ).toHaveWarnedWith( 'Block bindings source usesContext must be an array.' ); } ); From 1d7160491a65c402a9b2ca5da6d4c1a87ee0b8c7 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 23 Jul 2024 14:39:45 +0200 Subject: [PATCH 20/20] Adapt unit tests --- packages/blocks/src/api/test/registration.js | 28 +++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/blocks/src/api/test/registration.js b/packages/blocks/src/api/test/registration.js index e6ea9909d8763c..f4df432976bc12 100644 --- a/packages/blocks/src/api/test/registration.js +++ b/packages/blocks/src/api/test/registration.js @@ -1513,8 +1513,10 @@ describe( 'blocks', () => { } ); it( 'should not override label from the server', () => { - // Simulate bootstrapping a source from the server registration. - registerBlockBindingsSource( { + // Bootstrap source from the server. + unlock( + dispatch( blocksStore ) + ).addBootstrappedBlockBindingsSource( { name: 'core/server', label: 'Server label', } ); @@ -1540,9 +1542,11 @@ describe( 'blocks', () => { ); } ); - it( 'should add usesContext when only defined in the client', () => { - // Simulate server bootstrap. - registerBlockBindingsSource( { + it( 'should add usesContext when only defined in the server', () => { + // Bootstrap source from the server. + unlock( + dispatch( blocksStore ) + ).addBootstrappedBlockBindingsSource( { name: 'core/testing', label: 'testing', usesContext: [ 'postId', 'postType' ], @@ -1557,9 +1561,11 @@ describe( 'blocks', () => { expect( source.usesContext ).toEqual( [ 'postId', 'postType' ] ); } ); - it( 'should keep usesContext when it is not defined in the client', () => { - // Simulate server bootstrap. - registerBlockBindingsSource( { + it( 'should add usesContext when only defined in the client', () => { + // Bootstrap source from the server. + unlock( + dispatch( blocksStore ) + ).addBootstrappedBlockBindingsSource( { name: 'core/testing', label: 'testing', } ); @@ -1575,8 +1581,10 @@ describe( 'blocks', () => { } ); it( 'should merge usesContext from server and client without duplicates', () => { - // Simulate server bootstrap. - registerBlockBindingsSource( { + // Bootstrap source from the server. + unlock( + dispatch( blocksStore ) + ).addBootstrappedBlockBindingsSource( { name: 'core/testing', label: 'testing', usesContext: [ 'postId', 'postType' ],