From 4ba530c55bf4271a41ccd640f84165584d8fefd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Zi=C3=B3=C5=82kowski?= Date: Thu, 14 Oct 2021 15:40:15 +0200 Subject: [PATCH] Blocks: Apply the most recent filters to previously registered blocks (#34299) * Blocks: Try to split block registration from filtering * Experiment with data control for processing block settings * Add store controls that reapplies block type filters * Add test block and filter to validate if it works * Meassure impact on the loading time when 100 custom blocks and filters registerd * Reapply block type filters for all editor screens * Remove test code examples * Improve code documentation for new functionality * Remove broken functionality in the block types reducer * Apply suggestions from code review * Provide the default value for variations iin block type to align with WordPress core * Improve handling for block styles and variations when `ADD_BLOCK_TYPES` called again * Update buildBlockTypeItem to correctly fetch block type variations * Test: Add e2e test that ensures that filters can be added after block registration * Blocks: Move logic to actions and remove controls * Rename __experimentalAddBlockType to __experimentalRegisterBlockType * Improve the description for __experimentalReapplyBlockTypeFilters --- packages/block-editor/src/store/selectors.js | 5 +- .../blocks/src/api/raw-handling/test/utils.js | 3 + packages/blocks/src/api/registration.js | 127 ++---------- packages/blocks/src/api/test/registration.js | 15 ++ packages/blocks/src/store/actions.js | 185 +++++++++++++++++- packages/blocks/src/store/reducer.js | 57 ++++-- packages/blocks/src/store/selectors.js | 22 ++- packages/blocks/src/store/test/reducer.js | 97 +++++++-- packages/customize-widgets/src/index.js | 6 +- 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 ++++ packages/edit-navigation/src/index.js | 6 +- packages/edit-post/src/index.js | 2 + packages/edit-site/src/index.js | 3 + packages/edit-widgets/src/index.js | 4 +- 16 files changed, 469 insertions(+), 153 deletions(-) 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/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: {}, 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 e1ff1c2a23f0e2..eb61cf89d3d93e 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 = {}; /** @@ -256,13 +238,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: [], @@ -271,103 +264,15 @@ export function registerBlockType( blockNameOrMetadata, settings ) { usesContext: [], supports: {}, styles: [], + variations: [], save: () => null, ...serverSideBlockDefinitions?.[ name ], ...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; - } + dispatch( blocksStore ).__experimentalRegisterBlockType( blockType ); - const preFilterSettings = { ...settings }; - settings = applyFilters( 'blocks.registerBlockType', settings, 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( preFilterSettings, 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 ); - - return 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 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/actions.js b/packages/blocks/src/store/actions.js index ff24ac6b1a4148..fcb07e9df29edd 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. * @@ -19,6 +141,67 @@ export function addBlockTypes( blockTypes ) { }; } +/** + * Yields action objects signaling 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* __experimentalRegisterBlockType( blockType ) { + yield { + type: 'ADD_UNPROCESSED_BLOCK_TYPE', + blockType, + }; + + const processedBlockType = processBlockType( blockType ); + if ( ! processedBlockType ) { + return; + } + yield addBlockTypes( processedBlockType ); +} + +/** + * 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 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. + */ +export function* __experimentalReapplyBlockTypeFilters() { + 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 ); +} + /** * Returns an action object used to remove a registered block type. * diff --git a/packages/blocks/src/store/reducer.js b/packages/blocks/src/store/reducer.js index 6eb859369b320d..79169f5e415f9a 100644 --- a/packages/blocks/src/store/reducer.js +++ b/packages/blocks/src/store/reducer.js @@ -42,7 +42,32 @@ export const DEFAULT_CATEGORIES = [ ]; /** - * Reducer managing the block types + * 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 + * get added or removed. + * + * @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. + * The state is derived from the `unprocessedBlockTypes` reducer. * * @param {Object} state Current state. * @param {Object} action Dispatched action. @@ -54,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 ); @@ -86,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 ); @@ -136,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 ); @@ -256,6 +290,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..1afae5f0de0877 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 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. * @@ -40,15 +51,8 @@ const getNormalizedBlockType = ( state, nameOrType ) => * @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 ] ); /** diff --git a/packages/blocks/src/store/test/reducer.js b/packages/blocks/src/store/test/reducer.js index 55d77ec74f4217..b4312d0fd7df22 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( {} ); @@ -62,6 +101,8 @@ describe( 'blockTypes', () => { } ); describe( 'blockStyles', () => { + const blockName = 'core/image'; + it( 'should return an empty object as default state', () => { expect( blockStyles( undefined, {} ) ).toEqual( {} ); } ); @@ -71,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' } ], } ); } ); } ); @@ -190,7 +256,10 @@ describe( 'blockVariations', () => { ); expect( state ).toEqual( { - [ blockName ]: [ blockVariation, secondBlockVariation ], + [ blockName ]: [ + { ...blockVariation, source: 'block' }, + secondBlockVariation, + ], } ); } ); 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/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!' ); + } ); +} ); 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 c20cff1441222b..1b206b1c0dd411 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,7 @@ export function initializeEditor( welcomeGuideTemplate: true, } ); + 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 ) {