From 5536a2328d05f0a3f04ad2df4f7fd710c783c5eb Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Wed, 25 Aug 2021 15:35:02 +0200 Subject: [PATCH 01/17] Blocks: Try to split block registration from filtering --- packages/blocks/src/api/registration.js | 35 ++++++++++++++----------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index e1ff1c2a23f0e2..7f6bb8afde99f6 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -256,13 +256,24 @@ export function registerBlockType( blockNameOrMetadata, settings ) { return; } + if ( ! /^[a-z][a-z0-9-]*\/[a-z][a-z0-9-]*$/.test( name ) ) { + console.error( + 'Block names must contain a namespace prefix, include only lowercase alphanumeric characters or dashes, and start with a letter. Example: my-plugin/my-custom-block' + ); + return; + } + if ( select( blocksStore ).getBlockType( name ) ) { + console.error( 'Block "' + name + '" is already registered.' ); + return; + } + if ( isObject( blockNameOrMetadata ) ) { unstable__bootstrapServerSideBlockDefinitions( { [ name ]: getBlockSettingsFromMetadata( blockNameOrMetadata ), } ); } - settings = { + const blockType = { name, icon: BLOCK_ICON_DEFAULT, keywords: [], @@ -276,19 +287,11 @@ export function registerBlockType( blockNameOrMetadata, settings ) { ...settings, }; - if ( ! /^[a-z][a-z0-9-]*\/[a-z][a-z0-9-]*$/.test( name ) ) { - console.error( - 'Block names must contain a namespace prefix, include only lowercase alphanumeric characters or dashes, and start with a letter. Example: my-plugin/my-custom-block' - ); - return; - } - if ( select( blocksStore ).getBlockType( name ) ) { - console.error( 'Block "' + name + '" is already registered.' ); - return; - } - - const preFilterSettings = { ...settings }; - settings = applyFilters( 'blocks.registerBlockType', settings, name ); + settings = applyFilters( + 'blocks.registerBlockType', + { ...blockType }, + name + ); if ( settings.deprecated ) { settings.deprecated = settings.deprecated.map( ( deprecation ) => @@ -302,7 +305,7 @@ export function registerBlockType( blockNameOrMetadata, settings ) { { // Omit deprecation keys here so that deprecations // can opt out of specific keys like "supports". - ...omit( preFilterSettings, DEPRECATED_ENTRY_KEYS ), + ...omit( blockType, DEPRECATED_ENTRY_KEYS ), ...deprecation, }, name @@ -367,7 +370,7 @@ export function registerBlockType( blockNameOrMetadata, settings ) { dispatch( blocksStore ).addBlockTypes( settings ); - return settings; + return select( blocksStore ).getBlockType( name ); } /** From 5181bfa5f0a313ad88552238f50b76d9bde5654a Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Thu, 26 Aug 2021 10:35:39 +0200 Subject: [PATCH 02/17] Experiment with data control for processing block settings --- .../blocks/src/api/raw-handling/test/utils.js | 3 + packages/blocks/src/api/registration.js | 103 +------------- packages/blocks/src/store/actions.js | 7 + packages/blocks/src/store/controls.js | 132 ++++++++++++++++++ packages/blocks/src/store/index.js | 2 + 5 files changed, 146 insertions(+), 101 deletions(-) create mode 100644 packages/blocks/src/store/controls.js diff --git a/packages/blocks/src/api/raw-handling/test/utils.js b/packages/blocks/src/api/raw-handling/test/utils.js index c414f3ae24eeeb..56c38bbb38c965 100644 --- a/packages/blocks/src/api/raw-handling/test/utils.js +++ b/packages/blocks/src/api/raw-handling/test/utils.js @@ -39,6 +39,9 @@ jest.mock( '@wordpress/data', () => { const mock = jest.fn(); return mock; }, + createRegistryControl() { + return jest.fn(); + }, }; } ); diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index 7f6bb8afde99f6..c7b91b5c4c728b 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -7,22 +7,17 @@ import { camelCase, isArray, isEmpty, - isFunction, isNil, isObject, - isPlainObject, isString, mapKeys, - omit, pick, pickBy, - some, } from 'lodash'; /** * WordPress dependencies */ -import { applyFilters } from '@wordpress/hooks'; import { select, dispatch } from '@wordpress/data'; import { _x } from '@wordpress/i18n'; @@ -30,8 +25,7 @@ import { _x } from '@wordpress/i18n'; * Internal dependencies */ import i18nBlockSchema from './i18n-block.json'; -import { isValidIcon, normalizeIconObject } from './utils'; -import { BLOCK_ICON_DEFAULT, DEPRECATED_ENTRY_KEYS } from './constants'; +import { BLOCK_ICON_DEFAULT } from './constants'; import { store as blocksStore } from '../store'; /** @@ -145,18 +139,6 @@ import { store as blocksStore } from '../store'; * then no preview is shown. */ -/** - * 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', -}; - export const serverSideBlockDefinitions = {}; /** @@ -287,88 +269,7 @@ export function registerBlockType( blockNameOrMetadata, settings ) { ...settings, }; - settings = applyFilters( - 'blocks.registerBlockType', - { ...blockType }, - name - ); - - if ( settings.deprecated ) { - settings.deprecated = settings.deprecated.map( ( deprecation ) => - pick( - // 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 - ), - DEPRECATED_ENTRY_KEYS - ) - ); - } - - if ( ! isPlainObject( settings ) ) { - console.error( 'Block settings must be a valid object.' ); - return; - } - - if ( ! isFunction( settings.save ) ) { - console.error( 'The "save" property must be a valid function.' ); - return; - } - if ( 'edit' in settings && ! isFunction( settings.edit ) ) { - console.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 && - ! some( select( blocksStore ).getCategories(), { - slug: settings.category, - } ) - ) { - console.warn( - 'The block "' + - name + - '" is registered with an invalid category "' + - settings.category + - '".' - ); - delete settings.category; - } - - if ( ! ( 'title' in settings ) || settings.title === '' ) { - console.error( 'The block "' + name + '" must have a title.' ); - return; - } - if ( typeof settings.title !== 'string' ) { - console.error( 'Block titles must be strings.' ); - return; - } - - settings.icon = normalizeIconObject( settings.icon ); - if ( ! isValidIcon( settings.icon.src ) ) { - console.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; - } - - dispatch( blocksStore ).addBlockTypes( settings ); + dispatch( blocksStore ).__experimentalAddBlockType( blockType ); return select( blocksStore ).getBlockType( name ); } diff --git a/packages/blocks/src/store/actions.js b/packages/blocks/src/store/actions.js index ff24ac6b1a4148..9248b53f1929fb 100644 --- a/packages/blocks/src/store/actions.js +++ b/packages/blocks/src/store/actions.js @@ -19,6 +19,13 @@ export function addBlockTypes( blockTypes ) { }; } +export function* __experimentalAddBlockType( blockType ) { + yield { + type: 'ADD_PROCESSED_BLOCK_TYPE', + blockType, + }; +} + /** * Returns an action object used to remove a registered block type. * diff --git a/packages/blocks/src/store/controls.js b/packages/blocks/src/store/controls.js new file mode 100644 index 00000000000000..89f8ff700e9bbe --- /dev/null +++ b/packages/blocks/src/store/controls.js @@ -0,0 +1,132 @@ +/** + * External dependencies + */ +import { isFunction, isPlainObject, omit, pick, some } from 'lodash'; + +/** + * WordPress dependencies + */ +import { createRegistryControl } from '@wordpress/data'; +import { applyFilters } from '@wordpress/hooks'; + +/** + * Internal dependencies + */ +import { isValidIcon, normalizeIconObject } from '../api/utils'; +import { DEPRECATED_ENTRY_KEYS } from '../api/constants'; + +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', +}; + +function processBlockType( blockType, select ) { + const { name } = blockType; + + const settings = applyFilters( + 'blocks.registerBlockType', + { ...blockType }, + name + ); + + if ( settings.deprecated ) { + settings.deprecated = settings.deprecated.map( ( deprecation ) => + pick( + // 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 + ), + DEPRECATED_ENTRY_KEYS + ) + ); + } + + 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 && + ! some( select( 'core/blocks' ).getCategories(), { + 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; +} + +const controls = { + ADD_PROCESSED_BLOCK_TYPE: createRegistryControl( + ( { dispatch, select } ) => ( { blockType } ) => { + const settings = processBlockType( blockType, select ); + if ( ! settings ) { + return; + } + + dispatch( 'core/blocks' ).addBlockTypes( settings ); + } + ), +}; + +export default controls; diff --git a/packages/blocks/src/store/index.js b/packages/blocks/src/store/index.js index 6d2dc7c822dbde..d791880c4bdab4 100644 --- a/packages/blocks/src/store/index.js +++ b/packages/blocks/src/store/index.js @@ -9,6 +9,7 @@ import { createReduxStore, register } from '@wordpress/data'; import reducer from './reducer'; import * as selectors from './selectors'; import * as actions from './actions'; +import controls from './controls'; import { STORE_NAME } from './constants'; /** @@ -22,6 +23,7 @@ export const store = createReduxStore( STORE_NAME, { reducer, selectors, actions, + controls, } ); register( store ); From b0132b72ab8432ac1566da1d7e753e6ce1fdb344 Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Thu, 26 Aug 2021 13:36:08 +0200 Subject: [PATCH 03/17] Add store controls that reapplies block type filters --- packages/blocks/src/store/actions.js | 12 ++++++- packages/blocks/src/store/controls.js | 28 ++++++++++++--- packages/blocks/src/store/reducer.js | 25 ++++++++++++- packages/blocks/src/store/selectors.js | 11 ++++++ packages/blocks/src/store/test/reducer.js | 43 +++++++++++++++++++++-- packages/edit-post/src/index.js | 3 ++ 6 files changed, 113 insertions(+), 9 deletions(-) diff --git a/packages/blocks/src/store/actions.js b/packages/blocks/src/store/actions.js index 9248b53f1929fb..6734420307f7b1 100644 --- a/packages/blocks/src/store/actions.js +++ b/packages/blocks/src/store/actions.js @@ -21,9 +21,19 @@ export function addBlockTypes( blockTypes ) { export function* __experimentalAddBlockType( blockType ) { yield { - type: 'ADD_PROCESSED_BLOCK_TYPE', + type: 'ADD_UNPROCESSED_BLOCK_TYPE', blockType, }; + yield { + type: 'APPLY_BLOCK_TYPE_FILTERS', + blockType, + }; +} + +export function* __experimentalReapplyBlockTypeFilters() { + yield { + type: 'REAPPLY_BLOCK_TYPE_FILTERS', + }; } /** diff --git a/packages/blocks/src/store/controls.js b/packages/blocks/src/store/controls.js index 89f8ff700e9bbe..d52b47a7fa60de 100644 --- a/packages/blocks/src/store/controls.js +++ b/packages/blocks/src/store/controls.js @@ -12,6 +12,7 @@ import { applyFilters } from '@wordpress/hooks'; /** * Internal dependencies */ +import { STORE_NAME } from './constants'; import { isValidIcon, normalizeIconObject } from '../api/utils'; import { DEPRECATED_ENTRY_KEYS } from '../api/constants'; @@ -81,7 +82,7 @@ function processBlockType( blockType, select ) { if ( 'category' in settings && - ! some( select( 'core/blocks' ).getCategories(), { + ! some( select( STORE_NAME ).getCategories(), { slug: settings.category, } ) ) { @@ -117,14 +118,31 @@ function processBlockType( blockType, select ) { } const controls = { - ADD_PROCESSED_BLOCK_TYPE: createRegistryControl( + APPLY_BLOCK_TYPE_FILTERS: createRegistryControl( ( { dispatch, select } ) => ( { blockType } ) => { - const settings = processBlockType( blockType, select ); - if ( ! settings ) { + const processedBlockType = processBlockType( blockType, select ); + if ( ! processedBlockType ) { return; } - dispatch( 'core/blocks' ).addBlockTypes( settings ); + dispatch( STORE_NAME ).addBlockTypes( processedBlockType ); + } + ), + REAPPLY_BLOCK_TYPE_FILTERS: createRegistryControl( + ( { dispatch, select } ) => () => { + const unprocessedBlockTypes = select( + STORE_NAME + ).__experimentalGetUnprocessedBlockTypes(); + + const processedBlockTypes = unprocessedBlockTypes + .map( ( blockType ) => processBlockType( blockType, select ) ) + .filter( Boolean ); + + if ( ! processedBlockTypes.length ) { + return; + } + + dispatch( STORE_NAME ).addBlockTypes( processedBlockTypes ); } ), }; diff --git a/packages/blocks/src/store/reducer.js b/packages/blocks/src/store/reducer.js index 6eb859369b320d..e050461640c65f 100644 --- a/packages/blocks/src/store/reducer.js +++ b/packages/blocks/src/store/reducer.js @@ -42,7 +42,29 @@ export const DEFAULT_CATEGORIES = [ ]; /** - * Reducer managing the block types + * Reducer managing the unprocessed block types registered by block authors. + * + * @param {Object} state Current state. + * @param {Object} action Dispatched action. + * + * @return {Object} Updated state. + */ +export function unprocessedBlockTypes( state = {}, action ) { + switch ( action.type ) { + case 'ADD_UNPROCESSED_BLOCK_TYPE': + return { + ...state, + [ action.blockType.name ]: action.blockType, + }; + case 'REMOVE_BLOCK_TYPES': + return omit( state, action.names ); + } + + return state; +} + +/** + * Reducer managing the processed block types with all filters applied. * * @param {Object} state Current state. * @param {Object} action Dispatched action. @@ -256,6 +278,7 @@ export function collections( state = {}, action ) { } export default combineReducers( { + unprocessedBlockTypes, blockTypes, blockStyles, blockVariations, diff --git a/packages/blocks/src/store/selectors.js b/packages/blocks/src/store/selectors.js index f680db154ec015..83a9d2eb177a90 100644 --- a/packages/blocks/src/store/selectors.js +++ b/packages/blocks/src/store/selectors.js @@ -32,6 +32,17 @@ const getNormalizedBlockType = ( state, nameOrType ) => ? getBlockType( state, nameOrType ) : nameOrType; +/** + * Returns all the unprocessed block types. + * + * @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 55d77ec74f4217..7f9623e2138bd9 100644 --- a/packages/blocks/src/store/test/reducer.js +++ b/packages/blocks/src/store/test/reducer.js @@ -12,9 +12,10 @@ import { removeBlockVariations, } from '../actions'; import { - blockVariations, - blockStyles, + unprocessedBlockTypes, blockTypes, + blockStyles, + blockVariations, categories, defaultBlockName, freeformFallbackBlockName, @@ -23,6 +24,44 @@ import { DEFAULT_CATEGORIES, } from '../reducer'; +describe( 'unprocessedBlockTypes', () => { + it( 'should return an empty object as default state', () => { + expect( unprocessedBlockTypes( undefined, {} ) ).toEqual( {} ); + } ); + + it( 'should add a new block type', () => { + const original = deepFreeze( { + 'core/paragraph': { name: 'core/paragraph' }, + } ); + + const state = unprocessedBlockTypes( original, { + type: 'ADD_UNPROCESSED_BLOCK_TYPE', + blockType: { name: 'core/code' }, + } ); + + expect( state ).toEqual( { + 'core/paragraph': { name: 'core/paragraph' }, + 'core/code': { name: 'core/code' }, + } ); + } ); + + it( 'should remove unprocessed block types', () => { + const original = deepFreeze( { + 'core/paragraph': { name: 'core/paragraph' }, + 'core/code': { name: 'core/code' }, + } ); + + const state = blockTypes( original, { + type: 'REMOVE_BLOCK_TYPES', + names: [ 'core/code' ], + } ); + + expect( state ).toEqual( { + 'core/paragraph': { name: 'core/paragraph' }, + } ); + } ); +} ); + describe( 'blockTypes', () => { it( 'should return an empty object as default state', () => { expect( blockTypes( undefined, {} ) ).toEqual( {} ); diff --git a/packages/edit-post/src/index.js b/packages/edit-post/src/index.js index c20cff1441222b..42bb95f10c8d80 100644 --- a/packages/edit-post/src/index.js +++ b/packages/edit-post/src/index.js @@ -1,6 +1,7 @@ /** * WordPress dependencies */ +import { store as blocksStore } from '@wordpress/blocks'; import { registerCoreBlocks, __experimentalRegisterExperimentalCoreBlocks, @@ -97,6 +98,8 @@ export function initializeEditor( welcomeGuideTemplate: true, } ); + dispatch( blocksStore ).__experimentalReapplyBlockTypeFilters(); + registerCoreBlocks(); if ( process.env.GUTENBERG_PHASE === 2 ) { __experimentalRegisterExperimentalCoreBlocks( { From d87f4b03a4259424e5215abd04a17302a40a144f Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Thu, 26 Aug 2021 14:25:46 +0200 Subject: [PATCH 04/17] Add test block and filter to validate if it works --- packages/blocks/src/store/controls.js | 15 +++++++++--- packages/edit-post/src/index.js | 33 ++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/packages/blocks/src/store/controls.js b/packages/blocks/src/store/controls.js index d52b47a7fa60de..9030abf01ca418 100644 --- a/packages/blocks/src/store/controls.js +++ b/packages/blocks/src/store/controls.js @@ -134,9 +134,18 @@ const controls = { STORE_NAME ).__experimentalGetUnprocessedBlockTypes(); - const processedBlockTypes = unprocessedBlockTypes - .map( ( blockType ) => processBlockType( blockType, select ) ) - .filter( Boolean ); + const processedBlockTypes = Object.keys( + unprocessedBlockTypes + ).reduce( ( accumulator, blockName ) => { + const result = processBlockType( + unprocessedBlockTypes[ blockName ], + select + ); + if ( result ) { + accumulator.push( result ); + } + return accumulator; + }, [] ); if ( ! processedBlockTypes.length ) { return; diff --git a/packages/edit-post/src/index.js b/packages/edit-post/src/index.js index 42bb95f10c8d80..57dd555da52827 100644 --- a/packages/edit-post/src/index.js +++ b/packages/edit-post/src/index.js @@ -1,13 +1,14 @@ /** * WordPress dependencies */ -import { store as blocksStore } from '@wordpress/blocks'; +import { store as blocksStore, registerBlockType } from '@wordpress/blocks'; import { registerCoreBlocks, __experimentalRegisterExperimentalCoreBlocks, } from '@wordpress/block-library'; import { render, unmountComponentAtNode } from '@wordpress/element'; import { dispatch } from '@wordpress/data'; +import { addFilter } from '@wordpress/hooks'; import { store as interfaceStore } from '@wordpress/interface'; /** @@ -98,6 +99,36 @@ export function initializeEditor( welcomeGuideTemplate: true, } ); + // Temporary test block. + registerBlockType( 'wp-js-plugin-starter/hello-world', { + title: 'Hello World', + description: 'Hello World block', + icon: 'admin-site', + category: 'widgets', + edit() { + return 'Hello Editor'; + }, + save() { + return 'Hello Frontend'; + }, + } ); + + // Temporary test filter registered after the test block. + addFilter( + 'blocks.registerBlockType', + 'wp-js-plugin-starter/hello-world/filter-name', + ( blockType, name ) => { + if ( name === 'wp-js-plugin-starter/hello-world' ) { + return { + ...blockType, + category: 'common', + }; + } + + return blockType; + } + ); + dispatch( blocksStore ).__experimentalReapplyBlockTypeFilters(); registerCoreBlocks(); From 78fc9882183f3324e267ca1f7f5aefd9e8aa008d Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Thu, 26 Aug 2021 16:58:26 +0200 Subject: [PATCH 05/17] Meassure impact on the loading time when 100 custom blocks and filters registerd --- packages/edit-post/src/index.js | 56 +++++++++++++++++---------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/packages/edit-post/src/index.js b/packages/edit-post/src/index.js index 57dd555da52827..3ba1bca62728db 100644 --- a/packages/edit-post/src/index.js +++ b/packages/edit-post/src/index.js @@ -99,35 +99,37 @@ export function initializeEditor( welcomeGuideTemplate: true, } ); - // Temporary test block. - registerBlockType( 'wp-js-plugin-starter/hello-world', { - title: 'Hello World', - description: 'Hello World block', - icon: 'admin-site', - category: 'widgets', - edit() { - return 'Hello Editor'; - }, - save() { - return 'Hello Frontend'; - }, - } ); + for ( let i = 1; i <= 100; i++ ) { + // Temporary test block. + registerBlockType( `wp-js-plugin-starter/hello-world-${ i }`, { + title: `Hello World ${ i }`, + description: `Hello World block ${ i }`, + icon: 'admin-site', + category: 'widgets', + edit() { + return `Hello Editor ${ i }`; + }, + save() { + return `Hello Frontend ${ i }`; + }, + } ); - // Temporary test filter registered after the test block. - addFilter( - 'blocks.registerBlockType', - 'wp-js-plugin-starter/hello-world/filter-name', - ( blockType, name ) => { - if ( name === 'wp-js-plugin-starter/hello-world' ) { - return { - ...blockType, - category: 'common', - }; - } + // Temporary test filter registered after the test block. + addFilter( + 'blocks.registerBlockType', + `wp-js-plugin-starter/hello-world/filter-name-${ i }`, + ( blockType, name ) => { + if ( name.startsWith( 'wp-js-plugin-starter/hello-world' ) ) { + return { + ...blockType, + category: 'common', + }; + } - return blockType; - } - ); + return blockType; + } + ); + } dispatch( blocksStore ).__experimentalReapplyBlockTypeFilters(); From bc1e788264e3d80d844d161516e6e95bff1a0729 Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Fri, 27 Aug 2021 09:59:33 +0200 Subject: [PATCH 06/17] Reapply block type filters for all editor screens --- packages/customize-widgets/src/index.js | 6 +++++- packages/edit-navigation/src/index.js | 6 ++++-- packages/edit-post/src/index.js | 5 ++--- packages/edit-site/src/index.js | 3 +++ packages/edit-widgets/src/index.js | 4 +++- 5 files changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/customize-widgets/src/index.js b/packages/customize-widgets/src/index.js index ef93ba1264c8b7..fe79a434cda2fc 100644 --- a/packages/customize-widgets/src/index.js +++ b/packages/customize-widgets/src/index.js @@ -12,7 +12,10 @@ import { registerLegacyWidgetVariations, registerWidgetGroupBlock, } from '@wordpress/widgets'; -import { setFreeformContentHandlerName } from '@wordpress/blocks'; +import { + setFreeformContentHandlerName, + store as blocksStore, +} from '@wordpress/blocks'; import { dispatch } from '@wordpress/data'; import { store as interfaceStore } from '@wordpress/interface'; @@ -41,6 +44,7 @@ export function initialize( editorName, blockEditorSettings ) { welcomeGuide: true, } ); + dispatch( blocksStore ).__experimentalReapplyBlockTypeFilters(); const coreBlocks = __experimentalGetCoreBlocks().filter( ( block ) => { return ! ( DISABLED_BLOCKS.includes( block.name ) || diff --git a/packages/edit-navigation/src/index.js b/packages/edit-navigation/src/index.js index fe3629e2829494..68a2328f714866 100644 --- a/packages/edit-navigation/src/index.js +++ b/packages/edit-navigation/src/index.js @@ -1,16 +1,17 @@ /** * WordPress dependencies */ +import { store as blocksStore } from '@wordpress/blocks'; import { registerCoreBlocks, __experimentalRegisterExperimentalCoreBlocks, } from '@wordpress/block-library'; +import { dispatch, useDispatch } from '@wordpress/data'; import { render, useMemo } from '@wordpress/element'; import { __experimentalFetchLinkSuggestions as fetchLinkSuggestions, store as coreStore, } from '@wordpress/core-data'; -import { dispatch, useDispatch } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; /** @@ -58,7 +59,6 @@ function NavEditor( { settings } ) { */ function setUpEditor( settings ) { addFilters( ! settings.blockNavMenus ); - registerCoreBlocks(); // Set up the navigation post entity. dispatch( coreStore ).addEntities( [ @@ -71,6 +71,8 @@ function setUpEditor( settings ) { }, ] ); + dispatch( blocksStore ).__experimentalReapplyBlockTypeFilters(); + registerCoreBlocks(); if ( process.env.GUTENBERG_PHASE === 2 ) { __experimentalRegisterExperimentalCoreBlocks(); } diff --git a/packages/edit-post/src/index.js b/packages/edit-post/src/index.js index 3ba1bca62728db..6ec3eb233c3527 100644 --- a/packages/edit-post/src/index.js +++ b/packages/edit-post/src/index.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { store as blocksStore, registerBlockType } from '@wordpress/blocks'; +import { registerBlockType } from '@wordpress/blocks'; import { registerCoreBlocks, __experimentalRegisterExperimentalCoreBlocks, @@ -131,8 +131,7 @@ export function initializeEditor( ); } - dispatch( blocksStore ).__experimentalReapplyBlockTypeFilters(); - + // dispatch( blocksStore ).__experimentalReapplyBlockTypeFilters(); registerCoreBlocks(); if ( process.env.GUTENBERG_PHASE === 2 ) { __experimentalRegisterExperimentalCoreBlocks( { diff --git a/packages/edit-site/src/index.js b/packages/edit-site/src/index.js index ec81f5c3ccb193..c1706b7f468821 100644 --- a/packages/edit-site/src/index.js +++ b/packages/edit-site/src/index.js @@ -1,10 +1,12 @@ /** * WordPress dependencies */ +import { store as blocksStore } from '@wordpress/blocks'; import { registerCoreBlocks, __experimentalRegisterExperimentalCoreBlocks, } from '@wordpress/block-library'; +import { dispatch } from '@wordpress/data'; import { render, unmountComponentAtNode } from '@wordpress/element'; import { __experimentalFetchLinkSuggestions as fetchLinkSuggestions } from '@wordpress/core-data'; @@ -47,6 +49,7 @@ export function initialize( id, settings ) { const target = document.getElementById( id ); const reboot = reinitializeEditor.bind( null, target, settings ); + dispatch( blocksStore ).__experimentalReapplyBlockTypeFilters(); registerCoreBlocks(); if ( process.env.GUTENBERG_PHASE === 2 ) { __experimentalRegisterExperimentalCoreBlocks( { diff --git a/packages/edit-widgets/src/index.js b/packages/edit-widgets/src/index.js index 0c00388e7c0f2f..7a5eba3532de79 100644 --- a/packages/edit-widgets/src/index.js +++ b/packages/edit-widgets/src/index.js @@ -5,7 +5,9 @@ import { registerBlockType, unstable__bootstrapServerSideBlockDefinitions, // eslint-disable-line camelcase setFreeformContentHandlerName, + store as blocksStore, } from '@wordpress/blocks'; +import { dispatch } from '@wordpress/data'; import { render, unmountComponentAtNode } from '@wordpress/element'; import { registerCoreBlocks, @@ -18,7 +20,6 @@ import { registerLegacyWidgetVariations, registerWidgetGroupBlock, } from '@wordpress/widgets'; -import { dispatch } from '@wordpress/data'; import { store as interfaceStore } from '@wordpress/interface'; /** @@ -82,6 +83,7 @@ export function initialize( id, settings ) { themeStyles: true, } ); + dispatch( blocksStore ).__experimentalReapplyBlockTypeFilters(); registerCoreBlocks( coreBlocks ); registerLegacyWidgetBlock(); if ( process.env.GUTENBERG_PHASE === 2 ) { From b42a4b659328599dfb9910d103708cd5dd67e1b0 Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Fri, 27 Aug 2021 12:44:36 +0200 Subject: [PATCH 07/17] Remove test code examples --- packages/edit-post/src/index.js | 37 ++------------------------------- 1 file changed, 2 insertions(+), 35 deletions(-) diff --git a/packages/edit-post/src/index.js b/packages/edit-post/src/index.js index 6ec3eb233c3527..1b206b1c0dd411 100644 --- a/packages/edit-post/src/index.js +++ b/packages/edit-post/src/index.js @@ -1,14 +1,13 @@ /** * WordPress dependencies */ -import { registerBlockType } from '@wordpress/blocks'; +import { store as blocksStore } from '@wordpress/blocks'; import { registerCoreBlocks, __experimentalRegisterExperimentalCoreBlocks, } from '@wordpress/block-library'; import { render, unmountComponentAtNode } from '@wordpress/element'; import { dispatch } from '@wordpress/data'; -import { addFilter } from '@wordpress/hooks'; import { store as interfaceStore } from '@wordpress/interface'; /** @@ -99,39 +98,7 @@ export function initializeEditor( welcomeGuideTemplate: true, } ); - for ( let i = 1; i <= 100; i++ ) { - // Temporary test block. - registerBlockType( `wp-js-plugin-starter/hello-world-${ i }`, { - title: `Hello World ${ i }`, - description: `Hello World block ${ i }`, - icon: 'admin-site', - category: 'widgets', - edit() { - return `Hello Editor ${ i }`; - }, - save() { - return `Hello Frontend ${ i }`; - }, - } ); - - // Temporary test filter registered after the test block. - addFilter( - 'blocks.registerBlockType', - `wp-js-plugin-starter/hello-world/filter-name-${ i }`, - ( blockType, name ) => { - if ( name.startsWith( 'wp-js-plugin-starter/hello-world' ) ) { - return { - ...blockType, - category: 'common', - }; - } - - return blockType; - } - ); - } - - // dispatch( blocksStore ).__experimentalReapplyBlockTypeFilters(); + dispatch( blocksStore ).__experimentalReapplyBlockTypeFilters(); registerCoreBlocks(); if ( process.env.GUTENBERG_PHASE === 2 ) { __experimentalRegisterExperimentalCoreBlocks( { From 8496a17e653e55c488505cb7733afe43c6cd9f2a Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Thu, 9 Sep 2021 12:53:38 +0200 Subject: [PATCH 08/17] Improve code documentation for new functionality --- packages/blocks/src/store/actions.js | 13 +++++++++++++ packages/blocks/src/store/controls.js | 9 +++++++++ packages/blocks/src/store/reducer.js | 5 ++++- packages/blocks/src/store/selectors.js | 2 +- 4 files changed, 27 insertions(+), 2 deletions(-) diff --git a/packages/blocks/src/store/actions.js b/packages/blocks/src/store/actions.js index 6734420307f7b1..219fc9a06460f4 100644 --- a/packages/blocks/src/store/actions.js +++ b/packages/blocks/src/store/actions.js @@ -19,6 +19,13 @@ export function addBlockTypes( blockTypes ) { }; } +/** + * Yields action objects signalling that the passed block type's settings should be stored in the state. + * + * @param {WPBlockType} blockType Unprocessed block type settings. + * + * @yield {Object} Action object. + */ export function* __experimentalAddBlockType( blockType ) { yield { type: 'ADD_UNPROCESSED_BLOCK_TYPE', @@ -30,6 +37,12 @@ export function* __experimentalAddBlockType( blockType ) { }; } +/** + * Yields an action object signalling that all block types should be computed again. + * It uses stored unprocessed block types and all the most recent list of registration filters. + * + * @yield {Object} Action object. + */ export function* __experimentalReapplyBlockTypeFilters() { yield { type: 'REAPPLY_BLOCK_TYPE_FILTERS', diff --git a/packages/blocks/src/store/controls.js b/packages/blocks/src/store/controls.js index 9030abf01ca418..d20ecb6c7f20ec 100644 --- a/packages/blocks/src/store/controls.js +++ b/packages/blocks/src/store/controls.js @@ -30,6 +30,15 @@ const LEGACY_CATEGORY_MAPPING = { layout: 'design', }; +/** + * 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 {Function} select Calls a selector given the current state and extra arguments. + * + * @return {?WPBlockType} The block, if it has been successfully registered; otherwise `undefined`. + */ function processBlockType( blockType, select ) { const { name } = blockType; diff --git a/packages/blocks/src/store/reducer.js b/packages/blocks/src/store/reducer.js index e050461640c65f..9dfaef708890c3 100644 --- a/packages/blocks/src/store/reducer.js +++ b/packages/blocks/src/store/reducer.js @@ -42,7 +42,9 @@ export const DEFAULT_CATEGORIES = [ ]; /** - * Reducer managing the unprocessed block types registered by block authors. + * Reducer managing the unprocessed block types in a form passed when registering the by block. + * It's for internal use only. It allows to recompute the processed block types on-demand after block type filters + * get added or removed. * * @param {Object} state Current state. * @param {Object} action Dispatched action. @@ -65,6 +67,7 @@ export function unprocessedBlockTypes( state = {}, action ) { /** * Reducer managing the processed block types with all filters applied. + * The state is derived from the `unprocessedBlockTypes` reducer. * * @param {Object} state Current state. * @param {Object} action Dispatched action. diff --git a/packages/blocks/src/store/selectors.js b/packages/blocks/src/store/selectors.js index 83a9d2eb177a90..ec7ba43da2c715 100644 --- a/packages/blocks/src/store/selectors.js +++ b/packages/blocks/src/store/selectors.js @@ -33,7 +33,7 @@ const getNormalizedBlockType = ( state, nameOrType ) => : nameOrType; /** - * Returns all the unprocessed block types. + * Returns all the unprocessed block types as passed during the registration. * * @param {Object} state Data state. * From 11b6b59dc9424d36e12081141a8e48c7ed78d419 Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Thu, 9 Sep 2021 13:23:29 +0200 Subject: [PATCH 09/17] Remove broken functionality in the block types reducer --- packages/blocks/src/store/reducer.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/blocks/src/store/reducer.js b/packages/blocks/src/store/reducer.js index 9dfaef708890c3..799023e4cddf27 100644 --- a/packages/blocks/src/store/reducer.js +++ b/packages/blocks/src/store/reducer.js @@ -79,12 +79,7 @@ export function blockTypes( state = {}, action ) { case 'ADD_BLOCK_TYPES': return { ...state, - ...keyBy( - map( action.blockTypes, ( blockType ) => - omit( blockType, 'styles ' ) - ), - 'name' - ), + ...keyBy( action.blockTypes, 'name' ), }; case 'REMOVE_BLOCK_TYPES': return omit( state, action.names ); From 097b240a24ac81b57e71584e3ee214047245778f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Zi=C3=B3=C5=82kowski?= Date: Thu, 9 Sep 2021 13:27:25 +0200 Subject: [PATCH 10/17] Apply suggestions from code review --- packages/blocks/src/store/actions.js | 4 ++-- packages/blocks/src/store/reducer.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/blocks/src/store/actions.js b/packages/blocks/src/store/actions.js index 219fc9a06460f4..db9d2cd19e2f88 100644 --- a/packages/blocks/src/store/actions.js +++ b/packages/blocks/src/store/actions.js @@ -20,7 +20,7 @@ export function addBlockTypes( blockTypes ) { } /** - * Yields action objects signalling that the passed block type's settings should be stored in the state. + * Yields action objects signaling that the passed block type's settings should be stored in the state. * * @param {WPBlockType} blockType Unprocessed block type settings. * @@ -38,7 +38,7 @@ export function* __experimentalAddBlockType( blockType ) { } /** - * Yields an action object signalling that all block types should be computed again. + * Yields an action object signaling that all block types should be computed again. * It uses stored unprocessed block types and all the most recent list of registration filters. * * @yield {Object} Action object. diff --git a/packages/blocks/src/store/reducer.js b/packages/blocks/src/store/reducer.js index 799023e4cddf27..2b56dcdc2a6a6d 100644 --- a/packages/blocks/src/store/reducer.js +++ b/packages/blocks/src/store/reducer.js @@ -43,7 +43,7 @@ export const DEFAULT_CATEGORIES = [ /** * Reducer managing the unprocessed block types in a form passed when registering the by block. - * It's for internal use only. It allows to recompute the processed block types on-demand after block type filters + * It's for internal use only. It allows recomputing the processed block types on-demand after block type filters * get added or removed. * * @param {Object} state Current state. From 43a763d3eec0699c8f2ed03dda48d112046c23fc Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Thu, 9 Sep 2021 14:06:49 +0200 Subject: [PATCH 11/17] Provide the default value for variations iin block type to align with WordPress core --- packages/blocks/src/api/registration.js | 1 + packages/blocks/src/api/test/registration.js | 15 +++++++++++++++ packages/blocks/src/store/selectors.js | 11 ++--------- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index c7b91b5c4c728b..8e248a5e425549 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -264,6 +264,7 @@ export function registerBlockType( blockNameOrMetadata, settings ) { usesContext: [], supports: {}, styles: [], + variations: [], save: () => null, ...serverSideBlockDefinitions?.[ name ], ...settings, diff --git a/packages/blocks/src/api/test/registration.js b/packages/blocks/src/api/test/registration.js index 4b3d2864a0b229..35a17137ee473d 100644 --- a/packages/blocks/src/api/test/registration.js +++ b/packages/blocks/src/api/test/registration.js @@ -127,6 +127,7 @@ describe( 'blocks', () => { keywords: [], supports: {}, styles: [], + variations: [], save: noop, category: 'text', title: 'block title', @@ -272,6 +273,7 @@ describe( 'blocks', () => { keywords: [], supports: {}, styles: [], + variations: [], save: expect.any( Function ), } ); } ); @@ -307,6 +309,7 @@ describe( 'blocks', () => { keywords: [], supports: {}, styles: [], + variations: [], } ); } ); @@ -338,6 +341,7 @@ describe( 'blocks', () => { keywords: [], supports: {}, styles: [], + variations: [], } ); } ); @@ -371,6 +375,7 @@ describe( 'blocks', () => { keywords: [], supports: {}, styles: [], + variations: [], } ); } ); @@ -406,6 +411,7 @@ describe( 'blocks', () => { keywords: [], supports: {}, styles: [], + variations: [], } ); } ); @@ -473,6 +479,7 @@ describe( 'blocks', () => { keywords: [], supports: {}, styles: [], + variations: [], } ); } ); @@ -503,6 +510,7 @@ describe( 'blocks', () => { keywords: [], supports: {}, styles: [], + variations: [], } ); } ); @@ -547,6 +555,7 @@ describe( 'blocks', () => { keywords: [], supports: {}, styles: [], + variations: [], } ); } ); @@ -605,6 +614,7 @@ describe( 'blocks', () => { keywords: [], supports: {}, styles: [], + variations: [], } ); } ); @@ -630,6 +640,7 @@ describe( 'blocks', () => { keywords: [], supports: {}, styles: [], + variations: [], } ); } ); @@ -715,6 +726,7 @@ describe( 'blocks', () => { keywords: [], supports: {}, styles: [], + variations: [], save: () => null, ...get( serverSideBlockDefinitions, @@ -969,6 +981,7 @@ describe( 'blocks', () => { keywords: [], supports: {}, styles: [], + variations: [], } ); expect( getBlockTypes() ).toEqual( [] ); } ); @@ -1047,6 +1060,7 @@ describe( 'blocks', () => { keywords: [], supports: {}, styles: [], + variations: [], } ); } ); @@ -1071,6 +1085,7 @@ describe( 'blocks', () => { keywords: [], supports: {}, styles: [], + variations: [], } ); } ); } ); diff --git a/packages/blocks/src/store/selectors.js b/packages/blocks/src/store/selectors.js index ec7ba43da2c715..1afae5f0de0877 100644 --- a/packages/blocks/src/store/selectors.js +++ b/packages/blocks/src/store/selectors.js @@ -51,15 +51,8 @@ export function __experimentalGetUnprocessedBlockTypes( state ) { * @return {Array} Block Types. */ export const getBlockTypes = createSelector( - ( state ) => { - return Object.values( state.blockTypes ).map( ( blockType ) => { - return { - ...blockType, - variations: getBlockVariations( state, blockType.name ), - }; - } ); - }, - ( state ) => [ state.blockTypes, state.blockVariations ] + ( state ) => Object.values( state.blockTypes ), + ( state ) => [ state.blockTypes ] ); /** From c2456faa549e3f4963d3631b994ba78bcf6f0e89 Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Thu, 9 Sep 2021 15:27:50 +0200 Subject: [PATCH 12/17] Improve handling for block styles and variations when `ADD_BLOCK_TYPES` called again --- packages/blocks/src/store/reducer.js | 22 +++++++-- packages/blocks/src/store/test/reducer.js | 54 ++++++++++++++++++----- 2 files changed, 60 insertions(+), 16 deletions(-) diff --git a/packages/blocks/src/store/reducer.js b/packages/blocks/src/store/reducer.js index 2b56dcdc2a6a6d..79169f5e415f9a 100644 --- a/packages/blocks/src/store/reducer.js +++ b/packages/blocks/src/store/reducer.js @@ -106,8 +106,15 @@ export function blockStyles( state = {}, action ) { ( blockType ) => { return uniqBy( [ - ...get( blockType, [ 'styles' ], [] ), - ...get( state, [ blockType.name ], [] ), + ...get( blockType, [ 'styles' ], [] ).map( + ( style ) => ( { + ...style, + source: 'block', + } ) + ), + ...get( state, [ blockType.name ], [] ).filter( + ( { source } ) => 'block' !== source + ), ], ( style ) => style.name ); @@ -156,8 +163,15 @@ export function blockVariations( state = {}, action ) { ( blockType ) => { return uniqBy( [ - ...get( blockType, [ 'variations' ], [] ), - ...get( state, [ blockType.name ], [] ), + ...get( blockType, [ 'variations' ], [] ).map( + ( variation ) => ( { + ...variation, + source: 'block', + } ) + ), + ...get( state, [ blockType.name ], [] ).filter( + ( { source } ) => 'block' !== source + ), ], ( variation ) => variation.name ); diff --git a/packages/blocks/src/store/test/reducer.js b/packages/blocks/src/store/test/reducer.js index 7f9623e2138bd9..b4312d0fd7df22 100644 --- a/packages/blocks/src/store/test/reducer.js +++ b/packages/blocks/src/store/test/reducer.js @@ -101,6 +101,8 @@ describe( 'blockTypes', () => { } ); describe( 'blockStyles', () => { + const blockName = 'core/image'; + it( 'should return an empty object as default state', () => { expect( blockStyles( undefined, {} ) ).toEqual( {} ); } ); @@ -110,58 +112,83 @@ describe( 'blockStyles', () => { let state = blockStyles( original, { type: 'ADD_BLOCK_STYLES', - blockName: 'core/image', + blockName, styles: [ { name: 'fancy' } ], } ); expect( state ).toEqual( { - 'core/image': [ { name: 'fancy' } ], + [ blockName ]: [ { name: 'fancy' } ], } ); state = blockStyles( state, { type: 'ADD_BLOCK_STYLES', - blockName: 'core/image', + blockName, styles: [ { name: 'lightbox' } ], } ); expect( state ).toEqual( { - 'core/image': [ { name: 'fancy' }, { name: 'lightbox' } ], + [ blockName ]: [ { name: 'fancy' }, { name: 'lightbox' } ], } ); } ); - it( 'should add block styles when adding a block', () => { + it( 'should prepend block styles when adding a block', () => { const original = deepFreeze( { - 'core/image': [ { name: 'fancy' } ], + [ blockName ]: [ { name: 'fancy' } ], } ); const state = blockStyles( original, { type: 'ADD_BLOCK_TYPES', blockTypes: [ { - name: 'core/image', + name: blockName, styles: [ { name: 'original' } ], }, ], } ); expect( state ).toEqual( { - 'core/image': [ { name: 'original' }, { name: 'fancy' } ], + [ blockName ]: [ + { name: 'original', source: 'block' }, + { name: 'fancy' }, + ], + } ); + } ); + + it( 'should replace block styles if block type added again', () => { + const original = deepFreeze( { + [ blockName ]: [ { name: 'original', source: 'block' } ], + } ); + + const state = blockStyles( original, { + type: 'ADD_BLOCK_TYPES', + blockTypes: [ + { + name: blockName, + styles: [ { name: 'original', value: 'replace' } ], + }, + ], + } ); + + expect( state ).toEqual( { + [ blockName ]: [ + { name: 'original', value: 'replace', source: 'block' }, + ], } ); } ); it( 'should remove block styles', () => { const original = deepFreeze( { - 'core/image': [ { name: 'fancy' }, { name: 'lightbox' } ], + [ blockName ]: [ { name: 'fancy' }, { name: 'lightbox' } ], } ); const state = blockStyles( original, { type: 'REMOVE_BLOCK_STYLES', - blockName: 'core/image', + blockName, styleNames: [ 'fancy' ], } ); expect( state ).toEqual( { - 'core/image': [ { name: 'lightbox' } ], + [ blockName ]: [ { name: 'lightbox' } ], } ); } ); } ); @@ -229,7 +256,10 @@ describe( 'blockVariations', () => { ); expect( state ).toEqual( { - [ blockName ]: [ blockVariation, secondBlockVariation ], + [ blockName ]: [ + { ...blockVariation, source: 'block' }, + secondBlockVariation, + ], } ); } ); From 5fc04e9f4738b99a506b400b2794ba435075a1f7 Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Fri, 10 Sep 2021 14:42:42 +0200 Subject: [PATCH 13/17] Update buildBlockTypeItem to correctly fetch block type variations --- packages/block-editor/src/store/selectors.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 69d63048403cae..366a2b5e4678c7 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -24,6 +24,7 @@ import createSelector from 'rememo'; import { getBlockType, getBlockTypes, + getBlockVariations, hasBlockSupport, getPossibleBlockTransformations, parse, @@ -1514,9 +1515,7 @@ const buildBlockTypeItem = ( state, { buildScope = 'inserter' } ) => ( }; if ( buildScope === 'transform' ) return blockItemBase; - const inserterVariations = blockType.variations.filter( - ( { scope } ) => ! scope || scope.includes( 'inserter' ) - ); + const inserterVariations = getBlockVariations( blockType.name, 'inserter' ); return { ...blockItemBase, initialAttributes: {}, From 98036d7f7ca4286d0394adb289be3ca9562b27ec Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Fri, 17 Sep 2021 11:28:29 +0200 Subject: [PATCH 14/17] Test: Add e2e test that ensures that filters can be added after block registration --- packages/e2e-tests/plugins/block-api.php | 26 +++++++++++++++ packages/e2e-tests/plugins/block-api/index.js | 31 +++++++++++++++++ .../specs/editor/plugins/block-api.test.js | 33 +++++++++++++++++++ 3 files changed, 90 insertions(+) create mode 100644 packages/e2e-tests/plugins/block-api.php create mode 100644 packages/e2e-tests/plugins/block-api/index.js create mode 100644 packages/e2e-tests/specs/editor/plugins/block-api.test.js diff --git a/packages/e2e-tests/plugins/block-api.php b/packages/e2e-tests/plugins/block-api.php new file mode 100644 index 00000000000000..e0609f7adc0ffc --- /dev/null +++ b/packages/e2e-tests/plugins/block-api.php @@ -0,0 +1,26 @@ + { + if ( name === 'e2e-tests/hello-world' ) { + return { + ...blockType, + title: 'Filtered Hello World', + }; + } + + return blockType; + } + ); +} )(); diff --git a/packages/e2e-tests/specs/editor/plugins/block-api.test.js b/packages/e2e-tests/specs/editor/plugins/block-api.test.js new file mode 100644 index 00000000000000..9d5d8f5e1a4b28 --- /dev/null +++ b/packages/e2e-tests/specs/editor/plugins/block-api.test.js @@ -0,0 +1,33 @@ +/** + * WordPress dependencies + */ +import { + activatePlugin, + createNewPost, + deactivatePlugin, + insertBlock, +} from '@wordpress/e2e-test-utils'; + +describe( 'Using Block API', () => { + beforeAll( async () => { + await activatePlugin( 'gutenberg-test-block-api' ); + } ); + + afterAll( async () => { + await deactivatePlugin( 'gutenberg-test-block-api' ); + } ); + + beforeEach( async () => { + await createNewPost(); + } ); + + test( 'Inserts the filtered hello world block even when filter added after block registration', async () => { + await insertBlock( 'Filtered Hello World' ); + + const blockContent = await page.$eval( + 'div[data-type="e2e-tests/hello-world"]', + ( element ) => element.textContent + ); + expect( blockContent ).toEqual( 'Hello Editor!' ); + } ); +} ); From aaabad760d9ecf9dec5b96369168703be8d59c3c Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Thu, 14 Oct 2021 11:10:52 +0200 Subject: [PATCH 15/17] Blocks: Move logic to actions and remove controls --- packages/blocks/src/store/actions.js | 159 ++++++++++++++++++++++-- packages/blocks/src/store/controls.js | 168 -------------------------- packages/blocks/src/store/index.js | 2 - 3 files changed, 151 insertions(+), 178 deletions(-) delete mode 100644 packages/blocks/src/store/controls.js diff --git a/packages/blocks/src/store/actions.js b/packages/blocks/src/store/actions.js index db9d2cd19e2f88..85620e7b335e01 100644 --- a/packages/blocks/src/store/actions.js +++ b/packages/blocks/src/store/actions.js @@ -1,10 +1,132 @@ /** * External dependencies */ -import { castArray } from 'lodash'; +import { castArray, isFunction, isPlainObject, omit, pick, some } from 'lodash'; + +/** + * WordPress dependencies + */ +import { select } from '@wordpress/data'; +import { applyFilters } from '@wordpress/hooks'; + +/** + * Internal dependencies + */ +import { STORE_NAME } from './constants'; +import { isValidIcon, normalizeIconObject } from '../api/utils'; +import { DEPRECATED_ENTRY_KEYS } from '../api/constants'; /** @typedef {import('../api/registration').WPBlockVariation} WPBlockVariation */ +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 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. + * + * @return {?WPBlockType} The block, if it has been successfully registered; otherwise `undefined`. + */ +function processBlockType( blockType ) { + const { name } = blockType; + + const settings = applyFilters( + 'blocks.registerBlockType', + { ...blockType }, + name + ); + + if ( settings.deprecated ) { + settings.deprecated = settings.deprecated.map( ( deprecation ) => + pick( + // 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 + ), + DEPRECATED_ENTRY_KEYS + ) + ); + } + + 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 && + ! some( select( STORE_NAME ).getCategories(), { + 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. * @@ -31,10 +153,12 @@ export function* __experimentalAddBlockType( blockType ) { type: 'ADD_UNPROCESSED_BLOCK_TYPE', blockType, }; - yield { - type: 'APPLY_BLOCK_TYPE_FILTERS', - blockType, - }; + + const processedBlockType = processBlockType( blockType ); + if ( ! processedBlockType ) { + return; + } + yield addBlockTypes( processedBlockType ); } /** @@ -44,9 +168,28 @@ export function* __experimentalAddBlockType( blockType ) { * @yield {Object} Action object. */ export function* __experimentalReapplyBlockTypeFilters() { - yield { - type: 'REAPPLY_BLOCK_TYPE_FILTERS', - }; + const unprocessedBlockTypes = select( + STORE_NAME + ).__experimentalGetUnprocessedBlockTypes(); + + const processedBlockTypes = Object.keys( unprocessedBlockTypes ).reduce( + ( accumulator, blockName ) => { + const result = processBlockType( + unprocessedBlockTypes[ blockName ] + ); + if ( result ) { + accumulator.push( result ); + } + return accumulator; + }, + [] + ); + + if ( ! processedBlockTypes.length ) { + return; + } + + yield addBlockTypes( processedBlockTypes ); } /** diff --git a/packages/blocks/src/store/controls.js b/packages/blocks/src/store/controls.js deleted file mode 100644 index d20ecb6c7f20ec..00000000000000 --- a/packages/blocks/src/store/controls.js +++ /dev/null @@ -1,168 +0,0 @@ -/** - * External dependencies - */ -import { isFunction, isPlainObject, omit, pick, some } from 'lodash'; - -/** - * WordPress dependencies - */ -import { createRegistryControl } from '@wordpress/data'; -import { applyFilters } from '@wordpress/hooks'; - -/** - * Internal dependencies - */ -import { STORE_NAME } from './constants'; -import { isValidIcon, normalizeIconObject } from '../api/utils'; -import { DEPRECATED_ENTRY_KEYS } from '../api/constants'; - -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 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 {Function} select Calls a selector given the current state and extra arguments. - * - * @return {?WPBlockType} The block, if it has been successfully registered; otherwise `undefined`. - */ -function processBlockType( blockType, select ) { - const { name } = blockType; - - const settings = applyFilters( - 'blocks.registerBlockType', - { ...blockType }, - name - ); - - if ( settings.deprecated ) { - settings.deprecated = settings.deprecated.map( ( deprecation ) => - pick( - // 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 - ), - DEPRECATED_ENTRY_KEYS - ) - ); - } - - 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 && - ! some( select( STORE_NAME ).getCategories(), { - 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; -} - -const controls = { - APPLY_BLOCK_TYPE_FILTERS: createRegistryControl( - ( { dispatch, select } ) => ( { blockType } ) => { - const processedBlockType = processBlockType( blockType, select ); - if ( ! processedBlockType ) { - return; - } - - dispatch( STORE_NAME ).addBlockTypes( processedBlockType ); - } - ), - REAPPLY_BLOCK_TYPE_FILTERS: createRegistryControl( - ( { dispatch, select } ) => () => { - const unprocessedBlockTypes = select( - STORE_NAME - ).__experimentalGetUnprocessedBlockTypes(); - - const processedBlockTypes = Object.keys( - unprocessedBlockTypes - ).reduce( ( accumulator, blockName ) => { - const result = processBlockType( - unprocessedBlockTypes[ blockName ], - select - ); - if ( result ) { - accumulator.push( result ); - } - return accumulator; - }, [] ); - - if ( ! processedBlockTypes.length ) { - return; - } - - dispatch( STORE_NAME ).addBlockTypes( processedBlockTypes ); - } - ), -}; - -export default controls; diff --git a/packages/blocks/src/store/index.js b/packages/blocks/src/store/index.js index d791880c4bdab4..6d2dc7c822dbde 100644 --- a/packages/blocks/src/store/index.js +++ b/packages/blocks/src/store/index.js @@ -9,7 +9,6 @@ import { createReduxStore, register } from '@wordpress/data'; import reducer from './reducer'; import * as selectors from './selectors'; import * as actions from './actions'; -import controls from './controls'; import { STORE_NAME } from './constants'; /** @@ -23,7 +22,6 @@ export const store = createReduxStore( STORE_NAME, { reducer, selectors, actions, - controls, } ); register( store ); From 03fb310c09ad120dc08f7ac2ff28390a19e144cb Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Thu, 14 Oct 2021 11:19:15 +0200 Subject: [PATCH 16/17] Rename __experimentalAddBlockType to __experimentalRegisterBlockType --- packages/blocks/src/api/registration.js | 2 +- packages/blocks/src/store/actions.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index 8e248a5e425549..eb61cf89d3d93e 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -270,7 +270,7 @@ export function registerBlockType( blockNameOrMetadata, settings ) { ...settings, }; - dispatch( blocksStore ).__experimentalAddBlockType( blockType ); + dispatch( blocksStore ).__experimentalRegisterBlockType( blockType ); return select( blocksStore ).getBlockType( name ); } diff --git a/packages/blocks/src/store/actions.js b/packages/blocks/src/store/actions.js index 85620e7b335e01..4bd8b9bd683822 100644 --- a/packages/blocks/src/store/actions.js +++ b/packages/blocks/src/store/actions.js @@ -148,7 +148,7 @@ export function addBlockTypes( blockTypes ) { * * @yield {Object} Action object. */ -export function* __experimentalAddBlockType( blockType ) { +export function* __experimentalRegisterBlockType( blockType ) { yield { type: 'ADD_UNPROCESSED_BLOCK_TYPE', blockType, From 5ad37b4498b0205fa194db463238ed9c3ad7995f Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Thu, 14 Oct 2021 11:28:38 +0200 Subject: [PATCH 17/17] Improve the description for __experimentalReapplyBlockTypeFilters --- packages/blocks/src/store/actions.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/blocks/src/store/actions.js b/packages/blocks/src/store/actions.js index 4bd8b9bd683822..fcb07e9df29edd 100644 --- a/packages/blocks/src/store/actions.js +++ b/packages/blocks/src/store/actions.js @@ -163,7 +163,17 @@ export function* __experimentalRegisterBlockType( blockType ) { /** * Yields an action object signaling that all block types should be computed again. - * It uses stored unprocessed block types and all the most recent list of registration filters. + * 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. * * @yield {Object} Action object. */