diff --git a/packages/blocks/src/api/index.js b/packages/blocks/src/api/index.js index 62074213559e6d..3a007954c66421 100644 --- a/packages/blocks/src/api/index.js +++ b/packages/blocks/src/api/index.js @@ -49,6 +49,8 @@ export { unstable__bootstrapServerSideBlockDefinitions, // eslint-disable-line camelcase registerBlockStyle, unregisterBlockStyle, + __experimentalRegisterBlockPattern, + __experimentalUnregisterBlockPattern, } from './registration'; export { isUnmodifiedDefaultBlock, diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index 0d3dd7827680ec..92e4472eb9578b 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -25,14 +25,21 @@ import { isValidIcon, normalizeIconObject } from './utils'; import { DEPRECATED_ENTRY_KEYS } from './constants'; /** - * Render behavior of a block type icon; one of a Dashicon slug, an element, + * An icon type definition. One of a Dashicon slug, an element, * or a component. * - * @typedef {(string|WPElement|WPComponent)} WPBlockTypeIconRender + * @typedef {(string|WPElement|WPComponent)} WPIcon * * @see https://developer.wordpress.org/resource/dashicons/ */ +/** + * Render behavior of a block type icon; one of a Dashicon slug, an element, + * or a component. + * + * @typedef {WPIcon} WPBlockTypeIconRender + */ + /** * An object describing a normalized block type icon. * @@ -57,26 +64,41 @@ import { DEPRECATED_ENTRY_KEYS } from './constants'; * @typedef {(WPBlockTypeIconDescriptor|WPBlockTypeIconRender)} WPBlockTypeIcon */ +/** + * An object describing a pattern defined for the block type. + * + * @typedef {Object} WPBlockPattern + * + * @property {string} name The unique and machine-readable name. + * @property {string} label A human-readable label. + * @property {WPIcon} [icon] An icon helping to visualize the pattern. + * @property {boolean} [isDefault] Indicates whether the current pattern is the default one. + * Defaults to `false`. + * @property {Object} [attributes] Values which override block attributes. + * @property {Array[]} [innerBlocks] Initial configuration of nested blocks. + */ + /** * Defined behavior of a block type. * * @typedef {Object} WPBlock * - * @property {string} name Block type's namespaced name. - * @property {string} title Human-readable block type label. - * @property {string} category Block type category classification, - * used in search interfaces to arrange - * block types by category. - * @property {WPBlockTypeIcon} [icon] Block type icon. - * @property {string[]} [keywords] Additional keywords to produce block - * type as result in search interfaces. - * @property {Object} [attributes] Block type attributes. - * @property {WPComponent} [save] Optional component describing - * serialized markup structure of a - * block type. - * @property {WPComponent} edit Component rendering an element to - * manipulate the attributes of a block - * in the context of an editor. + * @property {string} name Block type's namespaced name. + * @property {string} title Human-readable block type label. + * @property {string} category Block type category classification, + * used in search interfaces to arrange + * block types by category. + * @property {WPBlockTypeIcon} [icon] Block type icon. + * @property {string[]} [keywords] Additional keywords to produce block + * type as result in search interfaces. + * @property {Object} [attributes] Block type attributes. + * @property {WPComponent} [save] Optional component describing + * serialized markup structure of a + * block type. + * @property {WPComponent} edit Component rendering an element to + * manipulate the attributes of a block + * in the context of an editor. + * @property {WPBlockPattern[]} [patterns] The list of block patterns. */ /** @@ -436,3 +458,23 @@ export const registerBlockStyle = ( blockName, styleVariation ) => { export const unregisterBlockStyle = ( blockName, styleVariationName ) => { dispatch( 'core/blocks' ).removeBlockStyles( blockName, styleVariationName ); }; + +/** + * Registers a new block pattern for the given block. + * + * @param {string} blockName Name of the block (example: “core/columns”). + * @param {WPBlockPattern} pattern Object describing a block pattern. + */ +export const __experimentalRegisterBlockPattern = ( blockName, pattern ) => { + dispatch( 'core/blocks' ).__experimentalAddBlockPatterns( blockName, pattern ); +}; + +/** + * Unregisters a block pattern defined for the given block. + * + * @param {string} blockName Name of the block (example: “core/columns”). + * @param {string} patternName Name of the pattern defined for the block. + */ +export const __experimentalUnregisterBlockPattern = ( blockName, patternName ) => { + dispatch( 'core/blocks' ).__experimentalRemoveBlockPatterns( blockName, patternName ); +}; diff --git a/packages/blocks/src/store/actions.js b/packages/blocks/src/store/actions.js index 220ae9be0ea0d3..c7b6ae42cb81b9 100644 --- a/packages/blocks/src/store/actions.js +++ b/packages/blocks/src/store/actions.js @@ -63,6 +63,38 @@ export function removeBlockStyles( blockName, styleNames ) { }; } +/** + * Returns an action object used in signalling that new block patterns have been added. + * + * @param {string} blockName Block name. + * @param {WPBlockPattern|WPBlockPattern[]} patterns Block patterns. + * + * @return {Object} Action object. + */ +export function __experimentalAddBlockPatterns( blockName, patterns ) { + return { + type: 'ADD_BLOCK_PATTERNS', + patterns: castArray( patterns ), + blockName, + }; +} + +/** + * Returns an action object used in signalling that block patterns have been removed. + * + * @param {string} blockName Block name. + * @param {string|string[]} patternNames Block pattern names. + * + * @return {Object} Action object. + */ +export function __experimentalRemoveBlockPatterns( blockName, patternNames ) { + return { + type: 'REMOVE_BLOCK_PATTERNS', + patternNames: castArray( patternNames ), + blockName, + }; +} + /** * Returns an action object used to set the default block name. * diff --git a/packages/blocks/src/store/reducer.js b/packages/blocks/src/store/reducer.js index 405e60109adda3..ad947696a5f1c9 100644 --- a/packages/blocks/src/store/reducer.js +++ b/packages/blocks/src/store/reducer.js @@ -97,6 +97,47 @@ export function blockStyles( state = {}, action ) { return state; } +/** + * Reducer managing the block patterns. + * + * @param {Object} state Current state. + * @param {Object} action Dispatched action. + * + * @return {Object} Updated state. + */ +export function blockPatterns( state = {}, action ) { + switch ( action.type ) { + case 'ADD_BLOCK_TYPES': + return { + ...state, + ...mapValues( keyBy( action.blockTypes, 'name' ), ( blockType ) => { + return uniqBy( [ + ...get( blockType, [ 'patterns' ], [] ), + ...get( state, [ blockType.name ], [] ), + ], ( style ) => style.name ); + } ), + }; + case 'ADD_BLOCK_PATTERNS': + return { + ...state, + [ action.blockName ]: uniqBy( [ + ...get( state, [ action.blockName ], [] ), + ...( action.patterns ), + ], ( pattern ) => pattern.name ), + }; + case 'REMOVE_BLOCK_PATTERNS': + return { + ...state, + [ action.blockName ]: filter( + get( state, [ action.blockName ], [] ), + ( pattern ) => action.patternNames.indexOf( pattern.name ) === -1, + ), + }; + } + + return state; +} + /** * Higher-order Reducer creating a reducer keeping track of given block name. * @@ -162,6 +203,7 @@ export function categories( state = DEFAULT_CATEGORIES, action ) { export default combineReducers( { blockTypes, blockStyles, + blockPatterns, defaultBlockName, freeformFallbackBlockName, unregisteredFallbackBlockName, diff --git a/packages/blocks/src/store/selectors.js b/packages/blocks/src/store/selectors.js index cb9333e3f6ff12..d334cbd5992edd 100644 --- a/packages/blocks/src/store/selectors.js +++ b/packages/blocks/src/store/selectors.js @@ -57,6 +57,18 @@ export function getBlockStyles( state, name ) { return state.blockStyles[ name ]; } +/** + * Returns block patterns by block name. + * + * @param {Object} state Data state. + * @param {string} blockName Block type name. + * + * @return {(WPBlockPattern[]|void)} Block patterns. + */ +export function __experimentalGetBlockPatterns( state, blockName ) { + return state.blockPatterns[ blockName ]; +} + /** * Returns all the available categories. * diff --git a/packages/blocks/src/store/test/actions.js b/packages/blocks/src/store/test/actions.js new file mode 100644 index 00000000000000..9e537432f406ab --- /dev/null +++ b/packages/blocks/src/store/test/actions.js @@ -0,0 +1,43 @@ +/** + * Internal dependencies + */ +import { + __experimentalAddBlockPatterns, + __experimentalRemoveBlockPatterns, +} from '../actions'; + +describe( 'actions', () => { + describe( 'addBlockPatterns', () => { + const blockName = 'block/name'; + const patternName = 'my-pattern'; + + it( 'should return the ADD_BLOCK_PATTERNS action', () => { + const pattern = { + name: patternName, + label: 'My Pattern', + attributes: { + example: 'foo', + }, + }; + const result = __experimentalAddBlockPatterns( blockName, pattern ); + expect( result ).toEqual( { + type: 'ADD_BLOCK_PATTERNS', + patterns: [ + pattern, + ], + blockName, + } ); + } ); + + it( 'should return the REMOVE_BLOCK_PATTERNS action', () => { + const result = __experimentalRemoveBlockPatterns( blockName, patternName ); + expect( result ).toEqual( { + type: 'REMOVE_BLOCK_PATTERNS', + patternNames: [ + patternName, + ], + blockName, + } ); + } ); + } ); +} ); diff --git a/packages/blocks/src/store/test/reducer.js b/packages/blocks/src/store/test/reducer.js index 346bb2e9aab1bf..2567903962d14f 100644 --- a/packages/blocks/src/store/test/reducer.js +++ b/packages/blocks/src/store/test/reducer.js @@ -7,6 +7,12 @@ import deepFreeze from 'deep-freeze'; * Internal dependencies */ import { + __experimentalAddBlockPatterns, + addBlockTypes, + __experimentalRemoveBlockPatterns, +} from '../actions'; +import { + blockPatterns, blockStyles, blockTypes, categories, @@ -136,6 +142,108 @@ describe( 'blockStyles', () => { } ); } ); +describe( 'blockPatterns', () => { + const blockName = 'block/name'; + + const blockPatternName = 'pattern-name'; + const blockPattern = { + name: blockPatternName, + label: 'My pattern', + }; + + const secondBlockPatternName = 'second-pattern-name'; + const secondBlockPattern = { + name: secondBlockPatternName, + label: 'My Second Pattern', + }; + + it( 'should return an empty object as default state', () => { + const state = blockPatterns( undefined, {} ); + + expect( state ).toEqual( {} ); + } ); + + it( 'should add a new block pattern when no pattern register', () => { + const initialState = deepFreeze( {} ); + + const state = blockPatterns( + initialState, + __experimentalAddBlockPatterns( blockName, blockPattern ) + ); + + expect( state ).toEqual( { + [ blockName ]: [ + blockPattern, + ], + } ); + } ); + + it( 'should add another pattern when a block pattern already present for the block', () => { + const initialState = deepFreeze( { + [ blockName ]: [ + blockPattern, + ], + } ); + + const state = blockPatterns( + initialState, + __experimentalAddBlockPatterns( blockName, secondBlockPattern ), + ); + + expect( state ).toEqual( { + [ blockName ]: [ + blockPattern, + secondBlockPattern, + ], + } ); + } ); + + it( 'should prepend block patterns added when adding a block', () => { + const initialState = deepFreeze( { + [ blockName ]: [ + secondBlockPattern, + ], + } ); + + const state = blockPatterns( + initialState, + addBlockTypes( { + name: blockName, + patterns: [ + blockPattern, + ], + } ) + ); + + expect( state ).toEqual( { + [ blockName ]: [ + blockPattern, + secondBlockPattern, + ], + } ); + } ); + + it( 'should remove a block pattern', () => { + const initialState = deepFreeze( { + [ blockName ]: [ + blockPattern, + secondBlockPattern, + ], + } ); + + const state = blockPatterns( + initialState, + __experimentalRemoveBlockPatterns( blockName, blockPatternName ) + ); + + expect( state ).toEqual( { + [ blockName ]: [ + secondBlockPattern, + ], + } ); + } ); +} ); + describe( 'defaultBlockName', () => { it( 'should return null as default state', () => { expect( defaultBlockName( undefined, {} ) ).toBeNull(); diff --git a/packages/eslint-plugin/configs/jsdoc.js b/packages/eslint-plugin/configs/jsdoc.js index 8e0935dc316881..45c58d2891b4c9 100644 --- a/packages/eslint-plugin/configs/jsdoc.js +++ b/packages/eslint-plugin/configs/jsdoc.js @@ -16,6 +16,7 @@ const temporaryWordPressInternalTypes = [ 'WPBlockSelection', 'WPBlockSerializationOptions', 'WPBlock', + 'WPBlockPattern', 'WPBlockTypeIcon', 'WPBlockTypeIconRender', 'WPBlockTypeIconDescriptor', @@ -27,6 +28,7 @@ const temporaryWordPressInternalTypes = [ 'WPElement', 'WPFormat', 'WPEditorInserterItem', + 'WPIcon', 'WPNotice', 'WPNoticeAction', 'WPPlugin',