Skip to content

Commit

Permalink
Blocks: move bootstrapped block types to Redux state
Browse files Browse the repository at this point in the history
  • Loading branch information
jsnajdr committed Aug 18, 2023
1 parent 8623a94 commit 2d9c9f9
Show file tree
Hide file tree
Showing 8 changed files with 331 additions and 268 deletions.
90 changes: 12 additions & 78 deletions packages/blocks/src/api/registration.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
/* eslint no-console: [ 'error', { allow: [ 'error', 'warn' ] } ] */

/**
* External dependencies
*/
import { camelCase } from 'change-case';

/**
* WordPress dependencies
*/
Expand All @@ -15,8 +10,8 @@ import { _x } from '@wordpress/i18n';
* Internal dependencies
*/
import i18nBlockSchema from './i18n-block.json';
import { BLOCK_ICON_DEFAULT } from './constants';
import { store as blocksStore } from '../store';
import { unlock } from '../lock-unlock';

/**
* An icon type definition. One of a Dashicon slug, an element,
Expand Down Expand Up @@ -129,8 +124,6 @@ import { store as blocksStore } from '../store';
* then no preview is shown.
*/

const serverSideBlockDefinitions = {};

function isObject( object ) {
return object !== null && typeof object === 'object';
}
Expand All @@ -142,54 +135,9 @@ function isObject( object ) {
*/
// eslint-disable-next-line camelcase
export function unstable__bootstrapServerSideBlockDefinitions( definitions ) {
for ( const blockName of Object.keys( definitions ) ) {
// Don't overwrite if already set. It covers the case when metadata
// was initialized from the server.
if ( serverSideBlockDefinitions[ blockName ] ) {
// We still need to polyfill `apiVersion` for WordPress version
// lower than 5.7. If it isn't present in the definition shared
// from the server, we try to fallback to the definition passed.
// @see https://github.com/WordPress/gutenberg/pull/29279
if (
serverSideBlockDefinitions[ blockName ].apiVersion ===
undefined &&
definitions[ blockName ].apiVersion
) {
serverSideBlockDefinitions[ blockName ].apiVersion =
definitions[ blockName ].apiVersion;
}
// The `ancestor` prop is not included in the definitions shared
// from the server yet, so it needs to be polyfilled as well.
// @see https://github.com/WordPress/gutenberg/pull/39894
if (
serverSideBlockDefinitions[ blockName ].ancestor ===
undefined &&
definitions[ blockName ].ancestor
) {
serverSideBlockDefinitions[ blockName ].ancestor =
definitions[ blockName ].ancestor;
}
// The `selectors` prop is not yet included in the server provided
// definitions. Polyfill it as well. This can be removed when the
// minimum supported WordPress is >= 6.3.
if (
serverSideBlockDefinitions[ blockName ].selectors ===
undefined &&
definitions[ blockName ].selectors
) {
serverSideBlockDefinitions[ blockName ].selectors =
definitions[ blockName ].selectors;
}
continue;
}

serverSideBlockDefinitions[ blockName ] = Object.fromEntries(
Object.entries( definitions[ blockName ] )
.filter(
( [ , value ] ) => value !== null && value !== undefined
)
.map( ( [ key, value ] ) => [ camelCase( key ), value ] )
);
const { addBootstrappedBlock } = unlock( dispatch( blocksStore ) );
for ( const [ name, blockType ] of Object.entries( definitions ) ) {
addBootstrappedBlock( name, blockType );
}
}

Expand Down Expand Up @@ -290,29 +238,15 @@ export function registerBlockType( blockNameOrMetadata, settings ) {
return;
}

if ( isObject( blockNameOrMetadata ) ) {
unstable__bootstrapServerSideBlockDefinitions( {
[ name ]: getBlockSettingsFromMetadata( blockNameOrMetadata ),
} );
}
const metadata = isObject( blockNameOrMetadata )
? getBlockSettingsFromMetadata( blockNameOrMetadata )
: getBlockSettingsFromMetadata( settings );

const blockType = {
name,
icon: BLOCK_ICON_DEFAULT,
keywords: [],
attributes: {},
providesContext: {},
usesContext: [],
selectors: {},
supports: {},
styles: [],
variations: [],
save: () => null,
...serverSideBlockDefinitions?.[ name ],
...settings,
};

dispatch( blocksStore ).__experimentalRegisterBlockType( blockType );
const { addBootstrappedBlock, addUnprocessedBlock } = unlock(
dispatch( blocksStore )
);
addBootstrappedBlock( name, metadata );
addUnprocessedBlock( name, settings );

return select( blocksStore ).getBlockType( name );
}
Expand Down
189 changes: 10 additions & 179 deletions packages/blocks/src/store/actions.js
Original file line number Diff line number Diff line change
@@ -1,154 +1,12 @@
/**
* External dependencies
*/
import { isPlainObject } from 'is-plain-object';

/**
* WordPress dependencies
*/
import deprecated from '@wordpress/deprecated';
import { applyFilters } from '@wordpress/hooks';

/**
* Internal dependencies
*/
import { isValidIcon, normalizeIconObject, omit } from '../api/utils';
import { DEPRECATED_ENTRY_KEYS } from '../api/constants';
import { processBlockType } from './process-block-type';

/** @typedef {import('../api/registration').WPBlockVariation} WPBlockVariation */
/** @typedef {import('../api/registration').WPBlockType} WPBlockType */
/** @typedef {import('./reducer').WPBlockCategory} WPBlockCategory */

const { error, warn } = window.console;

/**
* Mapping of legacy category slugs to their latest normal values, used to
* accommodate updates of the default set of block categories.
*
* @type {Record<string,string>}
*/
const LEGACY_CATEGORY_MAPPING = {
common: 'text',
formatting: 'text',
layout: 'design',
};

/**
* Whether the argument is a function.
*
* @param {*} maybeFunc The argument to check.
* @return {boolean} True if the argument is a function, false otherwise.
*/
function isFunction( maybeFunc ) {
return typeof maybeFunc === 'function';
}

/**
* Takes the unprocessed block type data and applies all the existing filters for the registered block type.
* Next, it validates all the settings and performs additional processing to the block type definition.
*
* @param {WPBlockType} blockType Unprocessed block type settings.
* @param {Object} thunkArgs Argument object for the thunk middleware.
* @param {Function} thunkArgs.select Function to select from the store.
*
* @return {WPBlockType | undefined} The block, if it has been successfully registered; otherwise `undefined`.
*/
const processBlockType = ( blockType, { select } ) => {
const { name } = blockType;

const settings = applyFilters(
'blocks.registerBlockType',
{ ...blockType },
name,
null
);

if ( settings.description && typeof settings.description !== 'string' ) {
deprecated( 'Declaring non-string block descriptions', {
since: '6.2',
} );
}

if ( settings.deprecated ) {
settings.deprecated = settings.deprecated.map( ( deprecation ) =>
Object.fromEntries(
Object.entries(
// Only keep valid deprecation keys.
applyFilters(
'blocks.registerBlockType',
// Merge deprecation keys with pre-filter settings
// so that filters that depend on specific keys being
// present don't fail.
{
// Omit deprecation keys here so that deprecations
// can opt out of specific keys like "supports".
...omit( blockType, DEPRECATED_ENTRY_KEYS ),
...deprecation,
},
name,
deprecation
)
).filter( ( [ key ] ) => DEPRECATED_ENTRY_KEYS.includes( key ) )
)
);
}

if ( ! isPlainObject( settings ) ) {
error( 'Block settings must be a valid object.' );
return;
}

if ( ! isFunction( settings.save ) ) {
error( 'The "save" property must be a valid function.' );
return;
}
if ( 'edit' in settings && ! isFunction( settings.edit ) ) {
error( 'The "edit" property must be a valid function.' );
return;
}

// Canonicalize legacy categories to equivalent fallback.
if ( LEGACY_CATEGORY_MAPPING.hasOwnProperty( settings.category ) ) {
settings.category = LEGACY_CATEGORY_MAPPING[ settings.category ];
}

if (
'category' in settings &&
! select
.getCategories()
.some( ( { slug } ) => slug === settings.category )
) {
warn(
'The block "' +
name +
'" is registered with an invalid category "' +
settings.category +
'".'
);
delete settings.category;
}

if ( ! ( 'title' in settings ) || settings.title === '' ) {
error( 'The block "' + name + '" must have a title.' );
return;
}
if ( typeof settings.title !== 'string' ) {
error( 'Block titles must be strings.' );
return;
}

settings.icon = normalizeIconObject( settings.icon );
if ( ! isValidIcon( settings.icon.src ) ) {
error(
'The icon passed is invalid. ' +
'The icon should be a string, an element, a function, or an object following the specifications documented in https://developer.wordpress.org/block-editor/developers/block-api/block-registration/#icon-optional'
);
return;
}

return settings;
};

/**
* Returns an action object used in signalling that block types have been added.
* Ignored from documentation as the recommended usage for this action through registerBlockType from @wordpress/blocks.
Expand All @@ -167,26 +25,6 @@ export function addBlockTypes( blockTypes ) {
};
}

/**
* Signals that the passed block type's settings should be stored in the state.
*
* @param {WPBlockType} blockType Unprocessed block type settings.
*/
export const __experimentalRegisterBlockType =
( blockType ) =>
( { dispatch, select } ) => {
dispatch( {
type: 'ADD_UNPROCESSED_BLOCK_TYPE',
blockType,
} );

const processedBlockType = processBlockType( blockType, { select } );
if ( ! processedBlockType ) {
return;
}
dispatch.addBlockTypes( processedBlockType );
};

/**
* Signals that all block types should be computed again.
* It uses stored unprocessed block types and all the most recent list of registered filters.
Expand All @@ -204,22 +42,15 @@ export const __experimentalRegisterBlockType =
export const __experimentalReapplyBlockTypeFilters =
() =>
( { dispatch, select } ) => {
const unprocessedBlockTypes =
select.__experimentalGetUnprocessedBlockTypes();

const processedBlockTypes = Object.keys( unprocessedBlockTypes ).reduce(
( accumulator, blockName ) => {
const result = processBlockType(
unprocessedBlockTypes[ blockName ],
{ select }
);
if ( result ) {
accumulator.push( result );
}
return accumulator;
},
[]
);
const processedBlockTypes = [];
for ( const [ name, settings ] of Object.entries(
select.getUnprocessedBlockTypes()
) ) {
const result = dispatch( processBlockType( name, settings ) );
if ( result ) {
processedBlockTypes.push( result );
}
}

if ( ! processedBlockTypes.length ) {
return;
Expand Down
2 changes: 2 additions & 0 deletions packages/blocks/src/store/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import reducer from './reducer';
import * as selectors from './selectors';
import * as privateSelectors from './private-selectors';
import * as actions from './actions';
import * as privateActions from './private-actions';
import { STORE_NAME } from './constants';
import { unlock } from '../lock-unlock';

Expand All @@ -28,3 +29,4 @@ export const store = createReduxStore( STORE_NAME, {

register( store );
unlock( store ).registerPrivateSelectors( privateSelectors );
unlock( store ).registerPrivateActions( privateActions );
39 changes: 39 additions & 0 deletions packages/blocks/src/store/private-actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Internal dependencies
*/
import { processBlockType } from './process-block-type';

/** @typedef {import('../api/registration').WPBlockType} WPBlockType */

/**
* Add block type metadata to the store.
*
* @param {string} name Block name.
* @param {WPBlockType} blockType Block type metadata.
*/
export function addBootstrappedBlock( name, blockType ) {
return {
type: 'ADD_BOOTSTRAPPED_BLOCK_TYPE',
name,
blockType,
};
}

/**
* Add unprocessed block type settings to the store.
*
* @param {string} name Block name.
* @param {WPBlockType} blockType Unprocessed block type settings.
*/
export const addUnprocessedBlock =
( name, blockType ) =>
( { dispatch } ) => {
dispatch( { type: 'ADD_UNPROCESSED_BLOCK_TYPE', name, blockType } );
const processedBlockType = dispatch(
processBlockType( name, blockType )
);
if ( ! processedBlockType ) {
return;
}
dispatch.addBlockTypes( processedBlockType );
};
Loading

0 comments on commit 2d9c9f9

Please sign in to comment.