From bd630b72c3404431f7714478ea71827613c00c36 Mon Sep 17 00:00:00 2001 From: Jarda Snajdr Date: Wed, 30 Aug 2023 15:13:08 +0200 Subject: [PATCH] Blocks: move bootstrapped block types to Redux state (#53807) * Blocks: move bootstrapped block types to Redux state * Fix the ADD_UNPROCESSED_BLOCK_TYPE reducer * Unit test for reapplyBlockFilters, fixing block unregistration * Fixup unprocessedBlockTypes reducer tests * Remove the apiVersion and ancestor polyfills * Improve action/selector documentation * Add -Type suffix to block registration actions * Stabilize reapplyBlockTypeFilters * Revert back to boostrapping metadata only when provided --- .../reference-guides/data/data-core-blocks.md | 6 +- packages/blocks/src/api/registration.js | 98 +-------- packages/blocks/src/api/test/registration.js | 118 +++------- packages/blocks/src/store/actions.js | 202 +++--------------- packages/blocks/src/store/index.js | 2 + packages/blocks/src/store/private-actions.js | 42 ++++ .../blocks/src/store/private-selectors.js | 24 +++ .../blocks/src/store/process-block-type.js | 159 ++++++++++++++ packages/blocks/src/store/reducer.js | 74 ++++++- packages/blocks/src/store/selectors.js | 11 - packages/blocks/src/store/test/reducer.js | 15 +- packages/customize-widgets/src/index.js | 2 +- packages/edit-post/src/index.js | 2 +- packages/edit-site/src/index.js | 2 +- packages/edit-widgets/src/index.js | 2 +- 15 files changed, 387 insertions(+), 372 deletions(-) create mode 100644 packages/blocks/src/store/private-actions.js create mode 100644 packages/blocks/src/store/process-block-type.js diff --git a/docs/reference-guides/data/data-core-blocks.md b/docs/reference-guides/data/data-core-blocks.md index ba047160f047de..084c9c1d7a5fbc 100644 --- a/docs/reference-guides/data/data-core-blocks.md +++ b/docs/reference-guides/data/data-core-blocks.md @@ -712,6 +712,10 @@ The actions in this package shouldn't be used directly. Instead, use the functio -Nothing to document. +### reapplyBlockTypeFilters + +Signals that all block types should be computed again. It uses stored unprocessed block types and all the most recent list of registered filters. + +It addresses the issue where third party block filters get registered after third party blocks. A sample sequence: 1. Filter A. 2. Block B. 3. Block C. 4. Filter D. 5. Filter E. 6. Block F. 7. Filter G. In this scenario some filters would not get applied for all blocks because they are registered too late. diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index 702582ff6489e8..72c0a30db02059 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -1,10 +1,5 @@ /* eslint no-console: [ 'error', { allow: [ 'error', 'warn' ] } ] */ -/** - * External dependencies - */ -import { camelCase } from 'change-case'; - /** * WordPress dependencies */ @@ -15,8 +10,8 @@ import { _x } from '@wordpress/i18n'; * Internal dependencies */ import i18nBlockSchema from './i18n-block.json'; -import { BLOCK_ICON_DEFAULT } from './constants'; import { store as blocksStore } from '../store'; +import { unlock } from '../lock-unlock'; /** * An icon type definition. One of a Dashicon slug, an element, @@ -129,8 +124,6 @@ import { store as blocksStore } from '../store'; * then no preview is shown. */ -const serverSideBlockDefinitions = {}; - function isObject( object ) { return object !== null && typeof object === 'object'; } @@ -142,65 +135,9 @@ function isObject( object ) { */ // eslint-disable-next-line camelcase export function unstable__bootstrapServerSideBlockDefinitions( definitions ) { - for ( const blockName of Object.keys( definitions ) ) { - // Don't overwrite if already set. It covers the case when metadata - // was initialized from the server. - if ( serverSideBlockDefinitions[ blockName ] ) { - // We still need to polyfill `apiVersion` for WordPress version - // lower than 5.7. If it isn't present in the definition shared - // from the server, we try to fallback to the definition passed. - // @see https://github.com/WordPress/gutenberg/pull/29279 - if ( - serverSideBlockDefinitions[ blockName ].apiVersion === - undefined && - definitions[ blockName ].apiVersion - ) { - serverSideBlockDefinitions[ blockName ].apiVersion = - definitions[ blockName ].apiVersion; - } - // The `ancestor` prop is not included in the definitions shared - // from the server yet, so it needs to be polyfilled as well. - // @see https://github.com/WordPress/gutenberg/pull/39894 - if ( - serverSideBlockDefinitions[ blockName ].ancestor === - undefined && - definitions[ blockName ].ancestor - ) { - serverSideBlockDefinitions[ blockName ].ancestor = - definitions[ blockName ].ancestor; - } - // The `selectors` prop is not yet included in the server provided - // definitions. Polyfill it as well. This can be removed when the - // minimum supported WordPress is >= 6.3. - if ( - serverSideBlockDefinitions[ blockName ].selectors === - undefined && - definitions[ blockName ].selectors - ) { - serverSideBlockDefinitions[ blockName ].selectors = - definitions[ blockName ].selectors; - } - - if ( - serverSideBlockDefinitions[ blockName ] - .__experimentalAutoInsert === undefined && - definitions[ blockName ].__experimentalAutoInsert - ) { - serverSideBlockDefinitions[ - blockName - ].__experimentalAutoInsert = - definitions[ blockName ].__experimentalAutoInsert; - } - continue; - } - - serverSideBlockDefinitions[ blockName ] = Object.fromEntries( - Object.entries( definitions[ blockName ] ) - .filter( - ( [ , value ] ) => value !== null && value !== undefined - ) - .map( ( [ key, value ] ) => [ camelCase( key ), value ] ) - ); + const { addBootstrappedBlockType } = unlock( dispatch( blocksStore ) ); + for ( const [ name, blockType ] of Object.entries( definitions ) ) { + addBootstrappedBlockType( name, blockType ); } } @@ -302,29 +239,16 @@ export function registerBlockType( blockNameOrMetadata, settings ) { return; } + const { addBootstrappedBlockType, addUnprocessedBlockType } = unlock( + dispatch( blocksStore ) + ); + if ( isObject( blockNameOrMetadata ) ) { - unstable__bootstrapServerSideBlockDefinitions( { - [ name ]: getBlockSettingsFromMetadata( blockNameOrMetadata ), - } ); + const metadata = getBlockSettingsFromMetadata( blockNameOrMetadata ); + addBootstrappedBlockType( name, metadata ); } - const blockType = { - name, - icon: BLOCK_ICON_DEFAULT, - keywords: [], - attributes: {}, - providesContext: {}, - usesContext: [], - selectors: {}, - supports: {}, - styles: [], - variations: [], - save: () => null, - ...serverSideBlockDefinitions?.[ name ], - ...settings, - }; - - dispatch( blocksStore ).__experimentalRegisterBlockType( blockType ); + addUnprocessedBlockType( name, settings ); return select( blocksStore ).getBlockType( name ); } diff --git a/packages/blocks/src/api/test/registration.js b/packages/blocks/src/api/test/registration.js index 42f4dcfbf0e48f..877c9fdc4a038a 100644 --- a/packages/blocks/src/api/test/registration.js +++ b/packages/blocks/src/api/test/registration.js @@ -5,7 +5,7 @@ */ import { addFilter, removeAllFilters, removeFilter } from '@wordpress/hooks'; import { logged } from '@wordpress/deprecated'; -import { select } from '@wordpress/data'; +import { select, dispatch } from '@wordpress/data'; /** * Internal dependencies @@ -33,6 +33,7 @@ import { import { BLOCK_ICON_DEFAULT, DEPRECATED_ENTRY_KEYS } from '../constants'; import { omit } from '../utils'; import { store as blocksStore } from '../../store'; +import { unlock } from '../../lock-unlock'; const noop = () => {}; @@ -48,19 +49,14 @@ describe( 'blocks', () => { title: 'block title', }; - beforeAll( () => { - // Initialize the block store. - require( '../../store' ); - } ); - afterEach( () => { - getBlockTypes().forEach( ( block ) => { - unregisterBlockType( block.name ); - } ); + const registeredNames = Object.keys( + unlock( select( blocksStore ) ).getUnprocessedBlockTypes() + ); + dispatch( blocksStore ).removeBlockTypes( registeredNames ); setFreeformContentHandlerName( undefined ); setUnregisteredTypeHandlerName( undefined ); setDefaultBlockName( undefined ); - unstable__bootstrapServerSideBlockDefinitions( {} ); // Reset deprecation logging to ensure we properly track warnings. for ( const key in logged ) { @@ -392,80 +388,6 @@ describe( 'blocks', () => { } ); } ); - // This test can be removed once the polyfill for apiVersion gets removed. - it( 'should apply apiVersion on the client when not set on the server', () => { - const blockName = 'core/test-block-back-compat'; - unstable__bootstrapServerSideBlockDefinitions( { - [ blockName ]: { - category: 'widgets', - }, - } ); - unstable__bootstrapServerSideBlockDefinitions( { - [ blockName ]: { - apiVersion: 3, - category: 'ignored', - }, - } ); - - const blockType = { - title: 'block title', - }; - registerBlockType( blockName, blockType ); - expect( getBlockType( blockName ) ).toEqual( { - apiVersion: 3, - name: blockName, - save: expect.any( Function ), - title: 'block title', - category: 'widgets', - icon: { src: BLOCK_ICON_DEFAULT }, - attributes: {}, - providesContext: {}, - usesContext: [], - keywords: [], - selectors: {}, - supports: {}, - styles: [], - variations: [], - } ); - } ); - - // This test can be removed once the polyfill for ancestor gets removed. - it( 'should apply ancestor on the client when not set on the server', () => { - const blockName = 'core/test-block-with-ancestor'; - unstable__bootstrapServerSideBlockDefinitions( { - [ blockName ]: { - category: 'widgets', - }, - } ); - unstable__bootstrapServerSideBlockDefinitions( { - [ blockName ]: { - ancestor: 'core/test-block-ancestor', - category: 'ignored', - }, - } ); - - const blockType = { - title: 'block title', - }; - registerBlockType( blockName, blockType ); - expect( getBlockType( blockName ) ).toEqual( { - ancestor: 'core/test-block-ancestor', - name: blockName, - save: expect.any( Function ), - title: 'block title', - category: 'widgets', - icon: { src: BLOCK_ICON_DEFAULT }, - attributes: {}, - providesContext: {}, - usesContext: [], - keywords: [], - selectors: {}, - supports: {}, - styles: [], - variations: [], - } ); - } ); - // This can be removed once polyfill adding selectors has been removed. it( 'should apply selectors on the client when not set on the server', () => { const blockName = 'core/test-block-with-selectors'; @@ -920,6 +842,34 @@ describe( 'blocks', () => { 'Declaring non-string block descriptions is deprecated since version 6.2.' ); } ); + + it( 're-applies block filters', () => { + // register block + registerBlockType( 'test/block', defaultBlockSettings ); + + // register a filter after registering a block + addFilter( + 'blocks.registerBlockType', + 'core/blocks/reapply', + ( settings ) => ( { + ...settings, + title: settings.title + ' filtered', + } ) + ); + + // check that block type has unfiltered values + expect( getBlockType( 'test/block' ).title ).toBe( + 'block title' + ); + + // reapply the block filters + dispatch( blocksStore ).reapplyBlockTypeFilters(); + + // check that block type has filtered values + expect( getBlockType( 'test/block' ).title ).toBe( + 'block title filtered' + ); + } ); } ); test( 'registers block from metadata', () => { diff --git a/packages/blocks/src/store/actions.js b/packages/blocks/src/store/actions.js index 2c02fb73b03527..d3bd71c067ebe3 100644 --- a/packages/blocks/src/store/actions.js +++ b/packages/blocks/src/store/actions.js @@ -1,154 +1,17 @@ -/** - * External dependencies - */ -import { isPlainObject } from 'is-plain-object'; - /** * WordPress dependencies */ import deprecated from '@wordpress/deprecated'; -import { applyFilters } from '@wordpress/hooks'; /** * Internal dependencies */ -import { isValidIcon, normalizeIconObject, omit } from '../api/utils'; -import { DEPRECATED_ENTRY_KEYS } from '../api/constants'; +import { processBlockType } from './process-block-type'; /** @typedef {import('../api/registration').WPBlockVariation} WPBlockVariation */ /** @typedef {import('../api/registration').WPBlockType} WPBlockType */ /** @typedef {import('./reducer').WPBlockCategory} WPBlockCategory */ -const { error, warn } = window.console; - -/** - * Mapping of legacy category slugs to their latest normal values, used to - * accommodate updates of the default set of block categories. - * - * @type {Record} - */ -const LEGACY_CATEGORY_MAPPING = { - common: 'text', - formatting: 'text', - layout: 'design', -}; - -/** - * Whether the argument is a function. - * - * @param {*} maybeFunc The argument to check. - * @return {boolean} True if the argument is a function, false otherwise. - */ -function isFunction( maybeFunc ) { - return typeof maybeFunc === 'function'; -} - -/** - * Takes the unprocessed block type data and applies all the existing filters for the registered block type. - * Next, it validates all the settings and performs additional processing to the block type definition. - * - * @param {WPBlockType} blockType Unprocessed block type settings. - * @param {Object} thunkArgs Argument object for the thunk middleware. - * @param {Function} thunkArgs.select Function to select from the store. - * - * @return {WPBlockType | undefined} The block, if it has been successfully registered; otherwise `undefined`. - */ -const processBlockType = ( blockType, { select } ) => { - const { name } = blockType; - - const settings = applyFilters( - 'blocks.registerBlockType', - { ...blockType }, - name, - null - ); - - if ( settings.description && typeof settings.description !== 'string' ) { - deprecated( 'Declaring non-string block descriptions', { - since: '6.2', - } ); - } - - if ( settings.deprecated ) { - settings.deprecated = settings.deprecated.map( ( deprecation ) => - Object.fromEntries( - Object.entries( - // Only keep valid deprecation keys. - applyFilters( - 'blocks.registerBlockType', - // Merge deprecation keys with pre-filter settings - // so that filters that depend on specific keys being - // present don't fail. - { - // Omit deprecation keys here so that deprecations - // can opt out of specific keys like "supports". - ...omit( blockType, DEPRECATED_ENTRY_KEYS ), - ...deprecation, - }, - name, - deprecation - ) - ).filter( ( [ key ] ) => DEPRECATED_ENTRY_KEYS.includes( key ) ) - ) - ); - } - - if ( ! isPlainObject( settings ) ) { - error( 'Block settings must be a valid object.' ); - return; - } - - if ( ! isFunction( settings.save ) ) { - error( 'The "save" property must be a valid function.' ); - return; - } - if ( 'edit' in settings && ! isFunction( settings.edit ) ) { - error( 'The "edit" property must be a valid function.' ); - return; - } - - // Canonicalize legacy categories to equivalent fallback. - if ( LEGACY_CATEGORY_MAPPING.hasOwnProperty( settings.category ) ) { - settings.category = LEGACY_CATEGORY_MAPPING[ settings.category ]; - } - - if ( - 'category' in settings && - ! select - .getCategories() - .some( ( { slug } ) => slug === settings.category ) - ) { - warn( - 'The block "' + - name + - '" is registered with an invalid category "' + - settings.category + - '".' - ); - delete settings.category; - } - - if ( ! ( 'title' in settings ) || settings.title === '' ) { - error( 'The block "' + name + '" must have a title.' ); - return; - } - if ( typeof settings.title !== 'string' ) { - error( 'Block titles must be strings.' ); - return; - } - - settings.icon = normalizeIconObject( settings.icon ); - if ( ! isValidIcon( settings.icon.src ) ) { - error( - 'The icon passed is invalid. ' + - 'The icon should be a string, an element, a function, or an object following the specifications documented in https://developer.wordpress.org/block-editor/developers/block-api/block-registration/#icon-optional' - ); - return; - } - - return settings; -}; - /** * Returns an action object used in signalling that block types have been added. * Ignored from documentation as the recommended usage for this action through registerBlockType from @wordpress/blocks. @@ -167,26 +30,6 @@ export function addBlockTypes( blockTypes ) { }; } -/** - * Signals that the passed block type's settings should be stored in the state. - * - * @param {WPBlockType} blockType Unprocessed block type settings. - */ -export const __experimentalRegisterBlockType = - ( blockType ) => - ( { dispatch, select } ) => { - dispatch( { - type: 'ADD_UNPROCESSED_BLOCK_TYPE', - blockType, - } ); - - const processedBlockType = processBlockType( blockType, { select } ); - if ( ! processedBlockType ) { - return; - } - dispatch.addBlockTypes( processedBlockType ); - }; - /** * Signals that all block types should be computed again. * It uses stored unprocessed block types and all the most recent list of registered filters. @@ -201,25 +44,17 @@ export const __experimentalRegisterBlockType = * 7. Filter G. * In this scenario some filters would not get applied for all blocks because they are registered too late. */ -export const __experimentalReapplyBlockTypeFilters = - () => - ( { dispatch, select } ) => { - const unprocessedBlockTypes = - select.__experimentalGetUnprocessedBlockTypes(); - - const processedBlockTypes = Object.keys( unprocessedBlockTypes ).reduce( - ( accumulator, blockName ) => { - const result = processBlockType( - unprocessedBlockTypes[ blockName ], - { select } - ); - if ( result ) { - accumulator.push( result ); - } - return accumulator; - }, - [] - ); +export function reapplyBlockTypeFilters() { + return ( { dispatch, select } ) => { + const processedBlockTypes = []; + for ( const [ name, settings ] of Object.entries( + select.getUnprocessedBlockTypes() + ) ) { + const result = dispatch( processBlockType( name, settings ) ); + if ( result ) { + processedBlockTypes.push( result ); + } + } if ( ! processedBlockTypes.length ) { return; @@ -227,6 +62,19 @@ export const __experimentalReapplyBlockTypeFilters = dispatch.addBlockTypes( processedBlockTypes ); }; +} + +export function __experimentalReapplyBlockFilters() { + deprecated( + 'wp.data.dispatch( "core/blocks" ).__experimentalReapplyBlockFilters', + { + since: '6.4', + alternative: 'reapplyBlockFilters', + } + ); + + return reapplyBlockTypeFilters(); +} /** * Returns an action object used to remove a registered block type. diff --git a/packages/blocks/src/store/index.js b/packages/blocks/src/store/index.js index ce69fd83d4e6c0..ffda3ffe000261 100644 --- a/packages/blocks/src/store/index.js +++ b/packages/blocks/src/store/index.js @@ -10,6 +10,7 @@ import reducer from './reducer'; import * as selectors from './selectors'; import * as privateSelectors from './private-selectors'; import * as actions from './actions'; +import * as privateActions from './private-actions'; import { STORE_NAME } from './constants'; import { unlock } from '../lock-unlock'; @@ -28,3 +29,4 @@ export const store = createReduxStore( STORE_NAME, { register( store ); unlock( store ).registerPrivateSelectors( privateSelectors ); +unlock( store ).registerPrivateActions( privateActions ); diff --git a/packages/blocks/src/store/private-actions.js b/packages/blocks/src/store/private-actions.js new file mode 100644 index 00000000000000..bc06e231b17222 --- /dev/null +++ b/packages/blocks/src/store/private-actions.js @@ -0,0 +1,42 @@ +/** + * Internal dependencies + */ +import { processBlockType } from './process-block-type'; + +/** @typedef {import('../api/registration').WPBlockType} WPBlockType */ + +/** + * Add bootstrapped block type metadata to the store. These metadata usually come from + * the `block.json` file and are either statically boostrapped from the server, or + * passed as the `metadata` parameter to the `registerBlockType` function. + * + * @param {string} name Block name. + * @param {WPBlockType} blockType Block type metadata. + */ +export function addBootstrappedBlockType( name, blockType ) { + return { + type: 'ADD_BOOTSTRAPPED_BLOCK_TYPE', + name, + blockType, + }; +} + +/** + * Add unprocessed block type settings to the store. These data are passed as the + * `settings` parameter to the client-side `registerBlockType` function. + * + * @param {string} name Block name. + * @param {WPBlockType} blockType Unprocessed block type settings. + */ +export function addUnprocessedBlockType( name, blockType ) { + return ( { dispatch } ) => { + dispatch( { type: 'ADD_UNPROCESSED_BLOCK_TYPE', name, blockType } ); + const processedBlockType = dispatch( + processBlockType( name, blockType ) + ); + if ( ! processedBlockType ) { + return; + } + dispatch.addBlockTypes( processedBlockType ); + }; +} diff --git a/packages/blocks/src/store/private-selectors.js b/packages/blocks/src/store/private-selectors.js index c9f5002b2219a3..7e4311658c8694 100644 --- a/packages/blocks/src/store/private-selectors.js +++ b/packages/blocks/src/store/private-selectors.js @@ -152,3 +152,27 @@ export const getSupportedStyles = createSelector( }, ( state, name ) => [ state.blockTypes[ name ] ] ); + +/** + * Returns the bootstrapped block type metadata for a give block name. + * + * @param {Object} state Data state. + * @param {string} name Block name. + * + * @return {Object} Bootstrapped block type metadata for a block. + */ +export function getBootstrappedBlockType( state, name ) { + return state.bootstrappedBlockTypes[ name ]; +} + +/** + * Returns all the unprocessed (before applying the `registerBlockType` filter) + * block type settings as passed during block registration. + * + * @param {Object} state Data state. + * + * @return {Array} Unprocessed block type settings for all blocks. + */ +export function getUnprocessedBlockTypes( state ) { + return state.unprocessedBlockTypes; +} diff --git a/packages/blocks/src/store/process-block-type.js b/packages/blocks/src/store/process-block-type.js new file mode 100644 index 00000000000000..aab198af6c66fb --- /dev/null +++ b/packages/blocks/src/store/process-block-type.js @@ -0,0 +1,159 @@ +/** + * External dependencies + */ +import { isPlainObject } from 'is-plain-object'; + +/** + * WordPress dependencies + */ +import deprecated from '@wordpress/deprecated'; +import { applyFilters } from '@wordpress/hooks'; + +/** + * Internal dependencies + */ +import { isValidIcon, normalizeIconObject, omit } from '../api/utils'; +import { BLOCK_ICON_DEFAULT, DEPRECATED_ENTRY_KEYS } from '../api/constants'; + +/** @typedef {import('../api/registration').WPBlockType} WPBlockType */ + +const { error, warn } = window.console; + +/** + * Mapping of legacy category slugs to their latest normal values, used to + * accommodate updates of the default set of block categories. + * + * @type {Record} + */ +const LEGACY_CATEGORY_MAPPING = { + common: 'text', + formatting: 'text', + layout: 'design', +}; + +/** + * Takes the unprocessed block type settings, merges them with block type metadata + * and applies all the existing filters for the registered block type. + * Next, it validates all the settings and performs additional processing to the block type definition. + * + * @param {string} name Block name. + * @param {WPBlockType} blockSettings Unprocessed block type settings. + * + * @return {WPBlockType | undefined} The block, if it has been processed and can be registered; otherwise `undefined`. + */ +export const processBlockType = + ( name, blockSettings ) => + ( { select } ) => { + const blockType = { + name, + icon: BLOCK_ICON_DEFAULT, + keywords: [], + attributes: {}, + providesContext: {}, + usesContext: [], + selectors: {}, + supports: {}, + styles: [], + variations: [], + save: () => null, + ...select.getBootstrappedBlockType( name ), + ...blockSettings, + }; + + const settings = applyFilters( + 'blocks.registerBlockType', + blockType, + name, + null + ); + + if ( + settings.description && + typeof settings.description !== 'string' + ) { + deprecated( 'Declaring non-string block descriptions', { + since: '6.2', + } ); + } + + if ( settings.deprecated ) { + settings.deprecated = settings.deprecated.map( ( deprecation ) => + Object.fromEntries( + Object.entries( + // Only keep valid deprecation keys. + applyFilters( + 'blocks.registerBlockType', + // Merge deprecation keys with pre-filter settings + // so that filters that depend on specific keys being + // present don't fail. + { + // Omit deprecation keys here so that deprecations + // can opt out of specific keys like "supports". + ...omit( blockType, DEPRECATED_ENTRY_KEYS ), + ...deprecation, + }, + blockType.name, + deprecation + ) + ).filter( ( [ key ] ) => + DEPRECATED_ENTRY_KEYS.includes( key ) + ) + ) + ); + } + + if ( ! isPlainObject( settings ) ) { + error( 'Block settings must be a valid object.' ); + return; + } + + if ( typeof settings.save !== 'function' ) { + error( 'The "save" property must be a valid function.' ); + return; + } + if ( 'edit' in settings && typeof settings.edit !== 'function' ) { + error( 'The "edit" property must be a valid function.' ); + return; + } + + // Canonicalize legacy categories to equivalent fallback. + if ( LEGACY_CATEGORY_MAPPING.hasOwnProperty( settings.category ) ) { + settings.category = LEGACY_CATEGORY_MAPPING[ settings.category ]; + } + + if ( + 'category' in settings && + ! select + .getCategories() + .some( ( { slug } ) => slug === settings.category ) + ) { + warn( + 'The block "' + + name + + '" is registered with an invalid category "' + + settings.category + + '".' + ); + delete settings.category; + } + + if ( ! ( 'title' in settings ) || settings.title === '' ) { + error( 'The block "' + name + '" must have a title.' ); + return; + } + if ( typeof settings.title !== 'string' ) { + error( 'Block titles must be strings.' ); + return; + } + + settings.icon = normalizeIconObject( settings.icon ); + if ( ! isValidIcon( settings.icon.src ) ) { + error( + 'The icon passed is invalid. ' + + 'The icon should be a string, an element, a function, or an object following the specifications documented in https://developer.wordpress.org/block-editor/developers/block-api/block-registration/#icon-optional' + ); + return; + } + + return settings; + }; diff --git a/packages/blocks/src/store/reducer.js b/packages/blocks/src/store/reducer.js index d8f76e00fc71d5..a8f114fea79c70 100644 --- a/packages/blocks/src/store/reducer.js +++ b/packages/blocks/src/store/reducer.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import { camelCase } from 'change-case'; + /** * WordPress dependencies */ @@ -52,6 +57,72 @@ function getUniqueItemsByName( items ) { }, [] ); } +function bootstrappedBlockTypes( state = {}, action ) { + switch ( action.type ) { + case 'ADD_BOOTSTRAPPED_BLOCK_TYPE': + const { name, blockType } = action; + const serverDefinition = state[ name ]; + let newDefinition; + // Don't overwrite if already set. It covers the case when metadata + // was initialized from the server. + if ( serverDefinition ) { + // The `selectors` prop is not yet included in the server provided + // definitions and needs to be polyfilled. This can be removed when the + // minimum supported WordPress is >= 6.3. + if ( + serverDefinition.selectors === undefined && + blockType.selectors + ) { + newDefinition = { + ...serverDefinition, + selectors: blockType.selectors, + }; + } + + // The `autoInsert` prop is not yet included in the server provided + // definitions and needs to be polyfilled. This can be removed when the + // minimum supported WordPress is >= 6.4. + if ( + serverDefinition.__experimentalAutoInsert === undefined && + blockType.__experimentalAutoInsert + ) { + newDefinition = { + ...serverDefinition, + ...newDefinition, + __experimentalAutoInsert: + blockType.__experimentalAutoInsert, + }; + } + } else { + newDefinition = Object.fromEntries( + Object.entries( blockType ) + .filter( + ( [ , value ] ) => + value !== null && value !== undefined + ) + .map( ( [ key, value ] ) => [ + camelCase( key ), + value, + ] ) + ); + newDefinition.name = name; + } + + if ( newDefinition ) { + return { + ...state, + [ name ]: newDefinition, + }; + } + + return state; + case 'REMOVE_BLOCK_TYPES': + return omit( state, action.names ); + } + + return state; +} + /** * Reducer managing the unprocessed block types in a form passed when registering the by block. * It's for internal use only. It allows recomputing the processed block types on-demand after block type filters @@ -67,7 +138,7 @@ export function unprocessedBlockTypes( state = {}, action ) { case 'ADD_UNPROCESSED_BLOCK_TYPE': return { ...state, - [ action.blockType.name ]: action.blockType, + [ action.name ]: action.blockType, }; case 'REMOVE_BLOCK_TYPES': return omit( state, action.names ); @@ -300,6 +371,7 @@ export function collections( state = {}, action ) { } export default combineReducers( { + bootstrappedBlockTypes, unprocessedBlockTypes, blockTypes, blockStyles, diff --git a/packages/blocks/src/store/selectors.js b/packages/blocks/src/store/selectors.js index cf577c695c9c5a..b2b8ab8106f097 100644 --- a/packages/blocks/src/store/selectors.js +++ b/packages/blocks/src/store/selectors.js @@ -32,17 +32,6 @@ const getNormalizedBlockType = ( state, nameOrType ) => ? getBlockType( state, nameOrType ) : nameOrType; -/** - * Returns all the unprocessed block types as passed during the registration. - * - * @param {Object} state Data state. - * - * @return {Array} Unprocessed block types. - */ -export function __experimentalGetUnprocessedBlockTypes( state ) { - return state.unprocessedBlockTypes; -} - /** * Returns all the available block types. * diff --git a/packages/blocks/src/store/test/reducer.js b/packages/blocks/src/store/test/reducer.js index b4312d0fd7df22..5664f9d876cb6e 100644 --- a/packages/blocks/src/store/test/reducer.js +++ b/packages/blocks/src/store/test/reducer.js @@ -31,24 +31,25 @@ describe( 'unprocessedBlockTypes', () => { it( 'should add a new block type', () => { const original = deepFreeze( { - 'core/paragraph': { name: 'core/paragraph' }, + 'core/paragraph': { title: 'Paragraph' }, } ); const state = unprocessedBlockTypes( original, { type: 'ADD_UNPROCESSED_BLOCK_TYPE', - blockType: { name: 'core/code' }, + name: 'core/code', + blockType: { title: 'Code' }, } ); expect( state ).toEqual( { - 'core/paragraph': { name: 'core/paragraph' }, - 'core/code': { name: 'core/code' }, + 'core/paragraph': { title: 'Paragraph' }, + 'core/code': { title: 'Code' }, } ); } ); it( 'should remove unprocessed block types', () => { const original = deepFreeze( { - 'core/paragraph': { name: 'core/paragraph' }, - 'core/code': { name: 'core/code' }, + 'core/paragraph': { title: 'Paragraph' }, + 'core/code': { title: 'Code' }, } ); const state = blockTypes( original, { @@ -57,7 +58,7 @@ describe( 'unprocessedBlockTypes', () => { } ); expect( state ).toEqual( { - 'core/paragraph': { name: 'core/paragraph' }, + 'core/paragraph': { title: 'Paragraph' }, } ); } ); } ); diff --git a/packages/customize-widgets/src/index.js b/packages/customize-widgets/src/index.js index 10ca00ff783090..5b438cac86f49b 100644 --- a/packages/customize-widgets/src/index.js +++ b/packages/customize-widgets/src/index.js @@ -49,7 +49,7 @@ export function initialize( editorName, blockEditorSettings ) { welcomeGuide: true, } ); - dispatch( blocksStore ).__experimentalReapplyBlockTypeFilters(); + dispatch( blocksStore ).reapplyBlockTypeFilters(); const coreBlocks = __experimentalGetCoreBlocks().filter( ( block ) => { return ! ( DISABLED_BLOCKS.includes( block.name ) || diff --git a/packages/edit-post/src/index.js b/packages/edit-post/src/index.js index fc5ce8c948e2ab..9dec0c6cd5e357 100644 --- a/packages/edit-post/src/index.js +++ b/packages/edit-post/src/index.js @@ -62,7 +62,7 @@ export function initializeEditor( welcomeGuideTemplate: true, } ); - dispatch( blocksStore ).__experimentalReapplyBlockTypeFilters(); + dispatch( blocksStore ).reapplyBlockTypeFilters(); // Check if the block list view should be open by default. // If `distractionFree` mode is enabled, the block list view should not be open. diff --git a/packages/edit-site/src/index.js b/packages/edit-site/src/index.js index ea2899a8abc105..18cb0d5e5db69b 100644 --- a/packages/edit-site/src/index.js +++ b/packages/edit-site/src/index.js @@ -43,7 +43,7 @@ export function initializeEditor( id, settings ) { fetchLinkSuggestions( search, searchOptions, settings ); settings.__experimentalFetchRichUrlData = fetchUrlData; - dispatch( blocksStore ).__experimentalReapplyBlockTypeFilters(); + dispatch( blocksStore ).reapplyBlockTypeFilters(); const coreBlocks = __experimentalGetCoreBlocks().filter( ( { name } ) => name !== 'core/freeform' ); diff --git a/packages/edit-widgets/src/index.js b/packages/edit-widgets/src/index.js index 56421ebe357bac..eb87d22fefef9e 100644 --- a/packages/edit-widgets/src/index.js +++ b/packages/edit-widgets/src/index.js @@ -70,7 +70,7 @@ export function initializeEditor( id, settings ) { themeStyles: true, } ); - dispatch( blocksStore ).__experimentalReapplyBlockTypeFilters(); + dispatch( blocksStore ).reapplyBlockTypeFilters(); registerCoreBlocks( coreBlocks ); registerLegacyWidgetBlock(); if ( process.env.IS_GUTENBERG_PLUGIN ) {