diff --git a/packages/e2e-tests/plugins/meta-attribute-block.php b/packages/e2e-tests/plugins/meta-attribute-block.php index 45fbcc661e1083..58eec7c04244af 100644 --- a/packages/e2e-tests/plugins/meta-attribute-block.php +++ b/packages/e2e-tests/plugins/meta-attribute-block.php @@ -8,20 +8,9 @@ */ /** - * Registers a custom script and a custom meta for the plugin. + * Registers a custom meta for use by the test block. */ function init_test_meta_attribute_block_plugin() { - wp_enqueue_script( - 'gutenberg-test-meta-attribute-block', - plugins_url( 'meta-attribute-block/index.js', __FILE__ ), - array( - 'wp-blocks', - 'wp-element', - ), - filemtime( plugin_dir_path( __FILE__ ) . 'meta-attribute-block/index.js' ), - true - ); - register_meta( 'post', 'my_meta', @@ -34,3 +23,20 @@ function init_test_meta_attribute_block_plugin() { } add_action( 'init', 'init_test_meta_attribute_block_plugin' ); + +/** + * Enqueues block assets for the custom meta test block. + */ +function enqueue_test_meta_attribute_block() { + wp_enqueue_script( + 'gutenberg-test-meta-attribute-block', + plugins_url( 'meta-attribute-block/index.js', __FILE__ ), + array( + 'wp-blocks', + 'wp-element', + ), + filemtime( plugin_dir_path( __FILE__ ) . 'meta-attribute-block/index.js' ), + true + ); +} +add_action( 'enqueue_block_assets', 'enqueue_test_meta_attribute_block' ); diff --git a/packages/editor/src/hooks/custom-sources-backwards-compatibility.js b/packages/editor/src/hooks/custom-sources-backwards-compatibility.js index 6b56281feade72..ff52b478f105f0 100644 --- a/packages/editor/src/hooks/custom-sources-backwards-compatibility.js +++ b/packages/editor/src/hooks/custom-sources-backwards-compatibility.js @@ -1,67 +1,71 @@ +/** + * External dependencies + */ +import { pickBy, mapValues, isEmpty, mapKeys } from 'lodash'; + /** * WordPress dependencies */ -import { getBlockType } from '@wordpress/blocks'; import { useSelect } from '@wordpress/data'; import { useEntityProp } from '@wordpress/core-data'; -import { useMemo, useCallback } from '@wordpress/element'; +import { useMemo } from '@wordpress/element'; import { createHigherOrderComponent } from '@wordpress/compose'; import { addFilter } from '@wordpress/hooks'; -const EMPTY_OBJECT = {}; -function useMetaAttributeSource( name, _attributes, _setAttributes ) { - const { attributes: attributeTypes = EMPTY_OBJECT } = - getBlockType( name ) || EMPTY_OBJECT; - let [ attributes, setAttributes ] = [ _attributes, _setAttributes ]; +/** @typedef {import('@wordpress/compose').WPHigherOrderComponent} WPHigherOrderComponent */ +/** @typedef {import('@wordpress/blocks').WPBlockSettings} WPBlockSettings */ + +/** + * Object whose keys are the names of block attributes, where each value + * represents the meta key to which the block attribute is intended to save. + * + * @see https://developer.wordpress.org/reference/functions/register_meta/ + * + * @typedef {Object} WPMetaAttributeMapping + */ - if ( Object.values( attributeTypes ).some( ( type ) => type.source === 'meta' ) ) { - // eslint-disable-next-line react-hooks/rules-of-hooks - const type = useSelect( ( select ) => select( 'core/editor' ).getCurrentPostType(), [] ); - // eslint-disable-next-line react-hooks/rules-of-hooks - const [ meta, setMeta ] = useEntityProp( 'postType', type, 'meta' ); +/** + * Given a mapping of attribute names (meta source attributes) to their + * associated meta key, returns a higher order component that overrides its + * `attributes` and `setAttributes` props to sync any changes with the edited + * post's meta keys. + * + * @param {WPMetaAttributeMapping} metaAttributes Meta attribute mapping. + * + * @return {WPHigherOrderComponent} Higher-order component. + */ +const createWithMetaAttributeSource = ( metaAttributes ) => createHigherOrderComponent( + ( BlockEdit ) => ( { attributes, setAttributes, ...props } ) => { + const postType = useSelect( ( select ) => select( 'core/editor' ).getCurrentPostType(), [] ); + const [ meta, setMeta ] = useEntityProp( 'postType', postType, 'meta' ); - // eslint-disable-next-line react-hooks/rules-of-hooks - attributes = useMemo( + const mergedAttributes = useMemo( () => ( { - ..._attributes, - ...Object.keys( attributeTypes ).reduce( ( acc, key ) => { - if ( attributeTypes[ key ].source === 'meta' ) { - acc[ key ] = meta[ attributeTypes[ key ].meta ]; - } - return acc; - }, {} ), + ...attributes, + ...mapValues( metaAttributes, ( metaKey ) => meta[ metaKey ] ), } ), - [ attributeTypes, meta, _attributes ] + [ attributes, meta ] ); - // eslint-disable-next-line react-hooks/rules-of-hooks - setAttributes = useCallback( - ( ...args ) => { - Object.keys( args[ 0 ] ).forEach( ( key ) => { - if ( attributeTypes[ key ].source === 'meta' ) { - setMeta( { [ attributeTypes[ key ].meta ]: args[ 0 ][ key ] } ); + return ( + { + const nextMeta = mapKeys( + // Filter to intersection of keys between the updated + // attributes and those with an associated meta key. + pickBy( nextAttributes, ( value, key ) => metaAttributes[ key ] ), + + // Rename the keys to the expected meta key name. + ( value, attributeKey ) => metaAttributes[ attributeKey ], + ); + + if ( ! isEmpty( nextMeta ) ) { + setMeta( nextMeta ); } - } ); - return _setAttributes( ...args ); - }, - [ attributeTypes, setMeta, _setAttributes ] - ); - } - return [ attributes, setAttributes ]; -} -const withMetaAttributeSource = createHigherOrderComponent( - ( BlockListBlock ) => ( { attributes, setAttributes, name, ...props } ) => { - [ attributes, setAttributes ] = useMetaAttributeSource( - name, - attributes, - setAttributes - ); - return ( - ); @@ -69,8 +73,26 @@ const withMetaAttributeSource = createHigherOrderComponent( 'withMetaAttributeSource' ); +/** + * Filters a registered block's settings to enhance a block's `edit` component + * to upgrade meta-sourced attributes to use the post's meta entity property. + * + * @param {WPBlockSettings} settings Registered block settings. + * + * @return {WPBlockSettings} Filtered block settings. + */ +function shimAttributeSource( settings ) { + /** @type {WPMetaAttributeMapping} */ + const metaAttributes = mapValues( pickBy( settings.attributes, { source: 'meta' } ), 'meta' ); + if ( ! isEmpty( metaAttributes ) ) { + settings.edit = createWithMetaAttributeSource( metaAttributes )( settings.edit ); + } + + return settings; +} + addFilter( - 'editor.BlockListBlock', - 'core/editor/custom-sources-backwards-compatibility/with-meta-attribute-source', - withMetaAttributeSource + 'blocks.registerBlockType', + 'core/editor/custom-sources-backwards-compatibility/shim-attribute-source', + shimAttributeSource );