diff --git a/backport-changelog/6.7/7020.md b/backport-changelog/6.7/7020.md new file mode 100644 index 00000000000000..8eacb220d340e5 --- /dev/null +++ b/backport-changelog/6.7/7020.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/7020 + +* https://github.com/WordPress/gutenberg/pull/63470 diff --git a/lib/compat/wordpress-6.7/block-bindings.php b/lib/compat/wordpress-6.7/block-bindings.php new file mode 100644 index 00000000000000..4c82dc6683f370 --- /dev/null +++ b/lib/compat/wordpress-6.7/block-bindings.php @@ -0,0 +1,40 @@ + $source_properties ) { + // Add source with the label to editor settings. + $editor_settings['blockBindingsSources'][ $source_name ] = array( + 'label' => $source_properties->label, + ); + // Add `usesContext` property if exists. + if ( ! empty( $source_properties->uses_context ) ) { + $editor_settings['blockBindingsSources'][ $source_name ]['usesContext'] = $source_properties->uses_context; + } + } + } + return $editor_settings; +} + +add_filter( 'block_editor_settings_all', 'gutenberg_add_server_block_bindings_sources_to_editor_settings', 10 ); diff --git a/lib/load.php b/lib/load.php index 47ce1011012970..312d95acea270f 100644 --- a/lib/load.php +++ b/lib/load.php @@ -154,6 +154,7 @@ function gutenberg_is_experiment_enabled( $name ) { // WordPress 6.7 compat. require __DIR__ . '/compat/wordpress-6.7/blocks.php'; +require __DIR__ . '/compat/wordpress-6.7/block-bindings.php'; // Experimental features. require __DIR__ . '/experimental/block-editor-settings-mobile.php'; diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index 78aab99b11b617..886309d8bd8f3e 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -769,7 +769,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 {string} [source.label] Human-readable label. * @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. @@ -800,11 +800,15 @@ export const registerBlockBindingsSource = ( source ) => { canUserEditValue, } = source; - // Check if the source is already registered. const existingSource = unlock( select( blocksStore ) ).getBlockBindingsSource( name ); - if ( existingSource ) { + + /* + * Check if the source has been already registered on the client. + * If the `getValues` property is defined, it could be assumed the source is already registered. + */ + if ( existingSource?.getValues ) { warning( 'Block bindings source "' + name + '" is already registered.' ); @@ -844,12 +848,21 @@ export const registerBlockBindingsSource = ( source ) => { } // Check the `label` property is correct. - if ( ! label ) { + if ( label && existingSource?.label ) { + warning( + 'Block bindings "' + + name + + '" source label is already defined in the server.' + ); + return; + } + + if ( ! label && ! existingSource?.label ) { warning( 'Block bindings source must contain a label.' ); return; } - if ( typeof label !== 'string' ) { + if ( label && typeof label !== 'string' ) { warning( 'Block bindings source label must be a string.' ); return; } diff --git a/packages/blocks/src/api/test/registration.js b/packages/blocks/src/api/test/registration.js index bc1057597bcd7a..d36abee2930bfa 100644 --- a/packages/blocks/src/api/test/registration.js +++ b/packages/blocks/src/api/test/registration.js @@ -1512,6 +1512,22 @@ describe( 'blocks', () => { expect( getBlockBindingsSource( 'core/testing' ) ).toBeUndefined(); } ); + it( 'should not override label from the server', () => { + // Simulate bootstrapping a source from the server registration. + registerBlockBindingsSource( { + name: 'core/server', + label: 'Server label', + } ); + // Override the source with a different label in the client. + registerBlockBindingsSource( { + name: 'core/server', + label: 'Client label', + } ); + expect( console ).toHaveWarnedWith( + 'Block bindings "core/server" source label is already defined in the server.' + ); + } ); + // Check the `getValues` callback is correct. it( 'should reject invalid getValues callback', () => { registerBlockBindingsSource( { @@ -1600,6 +1616,7 @@ describe( 'blocks', () => { const source = { name: 'core/test-source', label: 'Test Source', + getValues: () => 'value', }; registerBlockBindingsSource( source ); registerBlockBindingsSource( source ); diff --git a/packages/blocks/src/store/private-actions.js b/packages/blocks/src/store/private-actions.js index 55cdb2128895f5..6f7581da53de36 100644 --- a/packages/blocks/src/store/private-actions.js +++ b/packages/blocks/src/store/private-actions.js @@ -69,3 +69,17 @@ export function removeBlockBindingsSource( name ) { name, }; } + +/** + * Add bootstrapped block bindings sources, usually initialized from the server. + * + * @param {string} source Name of the source to bootstrap. + */ +export function addBootstrappedBlockBindingsSource( source ) { + return { + type: 'ADD_BOOTSTRAPPED_BLOCK_BINDINGS_SOURCE', + name: source.name, + label: source.label, + usesContext: source.usesContext, + }; +} diff --git a/packages/blocks/src/store/reducer.js b/packages/blocks/src/store/reducer.js index fc386e7ea9f557..5cffb0abc91973 100644 --- a/packages/blocks/src/store/reducer.js +++ b/packages/blocks/src/store/reducer.js @@ -377,13 +377,22 @@ export function blockBindingsSources( state = {}, action ) { return { ...state, [ action.name ]: { - label: action.label, + // Don't override the label if it's already set. + label: state[ action.name ]?.label || action.label, getValues: action.getValues, setValues: action.setValues, getPlaceholder: action.getPlaceholder, canUserEditValue: action.canUserEditValue, }, }; + case 'ADD_BOOTSTRAPPED_BLOCK_BINDINGS_SOURCE': + return { + ...state, + [ action.name ]: { + label: action.label, + usesContext: action.usesContext, + }, + }; case 'REMOVE_BLOCK_BINDINGS_SOURCE': return omit( state, action.name ); } diff --git a/packages/e2e-tests/plugins/block-bindings.php b/packages/e2e-tests/plugins/block-bindings.php index 74aec2adb500fb..b2eb9d797610d5 100644 --- a/packages/e2e-tests/plugins/block-bindings.php +++ b/packages/e2e-tests/plugins/block-bindings.php @@ -8,9 +8,19 @@ */ /** -* Register custom fields. +* Register custom fields and custom block bindings sources. */ -function gutenberg_test_block_bindings_register_custom_fields() { +function gutenberg_test_block_bindings_registration() { + // Register custom block bindings sources. + register_block_bindings_source( + 'core/server-source', + array( + 'label' => 'Server Source', + 'get_value_callback' => function () {}, + ) + ); + + // Register custom fields. register_meta( 'post', 'text_custom_field', @@ -51,4 +61,4 @@ function gutenberg_test_block_bindings_register_custom_fields() { ) ); } -add_action( 'init', 'gutenberg_test_block_bindings_register_custom_fields' ); +add_action( 'init', 'gutenberg_test_block_bindings_registration' ); diff --git a/packages/edit-post/src/index.js b/packages/edit-post/src/index.js index 2cbf1958719c68..aa3473e6e55d45 100644 --- a/packages/edit-post/src/index.js +++ b/packages/edit-post/src/index.js @@ -29,6 +29,7 @@ const { BackButton: __experimentalMainDashboardButton, registerDefaultActions, registerCoreBlockBindingsSources, + bootstrapBlockBindingsSourcesFromServer, } = unlock( editorPrivateApis ); /** @@ -87,6 +88,7 @@ export function initializeEditor( } registerCoreBlocks(); + bootstrapBlockBindingsSourcesFromServer( settings?.blockBindingsSources ); registerCoreBlockBindingsSources(); registerLegacyWidgetBlock( { inserter: false } ); registerWidgetGroupBlock( { inserter: false } ); diff --git a/packages/edit-site/src/index.js b/packages/edit-site/src/index.js index 02d974e037c045..922e2f6ab933ab 100644 --- a/packages/edit-site/src/index.js +++ b/packages/edit-site/src/index.js @@ -28,8 +28,11 @@ import { store as editSiteStore } from './store'; import { unlock } from './lock-unlock'; import App from './components/app'; -const { registerDefaultActions, registerCoreBlockBindingsSources } = - unlock( editorPrivateApis ); +const { + registerDefaultActions, + registerCoreBlockBindingsSources, + bootstrapBlockBindingsSourcesFromServer, +} = unlock( editorPrivateApis ); /** * Initializes the site editor screen. @@ -46,6 +49,7 @@ export function initializeEditor( id, settings ) { ( { name } ) => name !== 'core/freeform' ); registerCoreBlocks( coreBlocks ); + bootstrapBlockBindingsSourcesFromServer( settings?.blockBindingsSources ); registerCoreBlockBindingsSources(); dispatch( blocksStore ).setFreeformFallbackBlockName( 'core/html' ); registerLegacyWidgetBlock( { inserter: false } ); 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, + } ); + } + } +} diff --git a/packages/editor/src/bindings/pattern-overrides.js b/packages/editor/src/bindings/pattern-overrides.js index b299211900095d..88c6c73bdc61c1 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' ), getValues( { registry, clientId, context, bindings } ) { 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 f8161dd47b5c41..aafc784a21bd4a 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; }, diff --git a/packages/editor/src/private-apis.js b/packages/editor/src/private-apis.js index 58688a9099e879..f949be8e9321fb 100644 --- a/packages/editor/src/private-apis.js +++ b/packages/editor/src/private-apis.js @@ -24,7 +24,10 @@ import { GlobalStylesProvider, } from './components/global-styles-provider'; import registerDefaultActions from './dataviews/actions'; -import { registerCoreBlockBindingsSources } from './bindings/api'; +import { + registerCoreBlockBindingsSources, + bootstrapBlockBindingsSourcesFromServer, +} from './bindings/api'; const { store: interfaceStore, ...remainingInterfaceApis } = interfaceApis; @@ -45,6 +48,7 @@ lock( privateApis, { ResizableEditor, registerDefaultActions, registerCoreBlockBindingsSources, + bootstrapBlockBindingsSourcesFromServer, // This is a temporary private API while we're updating the site editor to use EditorProvider. useBlockEditorSettings, diff --git a/test/e2e/specs/editor/various/block-bindings.spec.js b/test/e2e/specs/editor/various/block-bindings.spec.js index 222004c7c1bccc..1499e255482c63 100644 --- a/test/e2e/specs/editor/various/block-bindings.spec.js +++ b/test/e2e/specs/editor/various/block-bindings.spec.js @@ -2172,4 +2172,33 @@ test.describe( 'Block bindings', () => { } ); } ); } ); + + test.describe( 'Sources registration', () => { + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost( { title: 'Test bindings' } ); + } ); + + test( 'should show the label of a source only registered in the server', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + metadata: { + bindings: { + content: { + source: 'core/server-source', + }, + }, + }, + }, + } ); + + const bindingLabel = page.locator( + '.components-item__block-bindings-source' + ); + await expect( bindingLabel ).toHaveText( 'Server Source' ); + } ); + } ); } );