diff --git a/packages/block-editor/src/store/index.js b/packages/block-editor/src/store/index.js index 0bcc00cb5f6ae8..10e16a0779cd63 100644 --- a/packages/block-editor/src/store/index.js +++ b/packages/block-editor/src/store/index.js @@ -10,6 +10,7 @@ import reducer from './reducer'; import * as selectors from './selectors'; import * as privateActions from './private-actions'; import * as privateSelectors from './private-selectors'; +import * as resolvers from './resolvers'; import * as actions from './actions'; import { STORE_NAME } from './constants'; import { unlock } from '../lock-unlock'; @@ -22,6 +23,7 @@ import { unlock } from '../lock-unlock'; export const storeConfig = { reducer, selectors, + resolvers, actions, }; diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js index e8230eea89daa3..adad08c7b98dc8 100644 --- a/packages/block-editor/src/store/private-selectors.js +++ b/packages/block-editor/src/store/private-selectors.js @@ -3,6 +3,11 @@ */ import createSelector from 'rememo'; +/** + * WordPress dependencies + */ +import { createRegistrySelector } from '@wordpress/data'; + /** * Internal dependencies */ @@ -11,11 +16,12 @@ import { getBlockParents, getBlockEditingMode, getSettings, - __experimentalGetParsedPattern, canInsertBlockType, - __experimentalGetAllowedPatterns, } from './selectors'; -import { getAllPatterns, checkAllowListRecursive } from './utils'; +import { checkAllowListRecursive, getAllPatternsDependants } from './utils'; +import { INSERTER_PATTERN_TYPES } from '../components/inserter/block-patterns-tab/utils'; +import { store } from './'; +import { unlock } from '../lock-unlock'; /** * Returns true if the block interface is hidden, or false otherwise. @@ -242,6 +248,10 @@ export const getInserterMediaCategories = createSelector( ] ); +export function getFetchedPatterns( state ) { + return state.blockPatterns; +} + /** * Returns whether there is at least one allowed pattern for inner blocks children. * This is useful for deferring the parsing of all patterns until needed. @@ -251,29 +261,74 @@ export const getInserterMediaCategories = createSelector( * * @return {boolean} If there is at least one allowed pattern. */ -export const hasAllowedPatterns = createSelector( - ( state, rootClientId = null ) => { - const patterns = getAllPatterns( state ); - const { allowedBlockTypes } = getSettings( state ); - return patterns.some( ( { name, inserter = true } ) => { - if ( ! inserter ) { - return false; - } - const { blocks } = __experimentalGetParsedPattern( state, name ); - return ( - checkAllowListRecursive( blocks, allowedBlockTypes ) && - blocks.every( ( { name: blockName } ) => - canInsertBlockType( state, blockName, rootClientId ) - ) +export const hasAllowedPatterns = createRegistrySelector( ( select ) => + createSelector( + ( state, rootClientId = null ) => { + const { getAllPatterns, __experimentalGetParsedPattern } = unlock( + select( store ) ); - } ); - }, - ( state, rootClientId ) => [ - ...__experimentalGetAllowedPatterns.getDependants( - state, - rootClientId - ), - ] + const patterns = getAllPatterns(); + const { allowedBlockTypes } = getSettings( state ); + return patterns.some( ( { name, inserter = true } ) => { + if ( ! inserter ) { + return false; + } + const { blocks } = __experimentalGetParsedPattern( name ); + return ( + checkAllowListRecursive( blocks, allowedBlockTypes ) && + blocks.every( ( { name: blockName } ) => + canInsertBlockType( state, blockName, rootClientId ) + ) + ); + } ); + }, + ( state, rootClientId ) => [ + getAllPatternsDependants( state ), + state.settings.allowedBlockTypes, + state.settings.templateLock, + state.blockListSettings[ rootClientId ], + state.blocks.byClientId.get( rootClientId ), + ] + ) +); + +export const getAllPatterns = createRegistrySelector( ( select ) => + createSelector( ( state ) => { + // This setting is left for back compat. + const { + __experimentalBlockPatterns = [], + __experimentalUserPatternCategories = [], + __experimentalReusableBlocks = [], + } = state.settings; + const userPatterns = ( __experimentalReusableBlocks ?? [] ).map( + ( userPattern ) => { + return { + name: `core/block/${ userPattern.id }`, + id: userPattern.id, + type: INSERTER_PATTERN_TYPES.user, + title: userPattern.title.raw, + categories: userPattern.wp_pattern_category.map( + ( catId ) => { + const category = ( + __experimentalUserPatternCategories ?? [] + ).find( ( { id } ) => id === catId ); + return category ? category.slug : catId; + } + ), + content: userPattern.content.raw, + syncStatus: userPattern.wp_pattern_sync_status, + }; + } + ); + return [ + ...userPatterns, + ...__experimentalBlockPatterns, + ...unlock( select( store ) ).getFetchedPatterns(), + ].filter( + ( x, index, arr ) => + index === arr.findIndex( ( y ) => x.name === y.name ) + ); + }, getAllPatternsDependants ) ); /** diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index 1091d5b5e7aff8..70e2dc3488772d 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -2023,6 +2023,15 @@ export function lastFocus( state = false, action ) { return state; } +function blockPatterns( state = [], action ) { + switch ( action.type ) { + case 'RECEIVE_BLOCK_PATTERNS': + return action.patterns; + } + + return state; +} + const combinedReducers = combineReducers( { blocks, isTyping, @@ -2053,6 +2062,7 @@ const combinedReducers = combineReducers( { blockRemovalRules, openedBlockSettingsMenu, registeredInserterMediaCategories, + blockPatterns, } ); function withAutomaticChangeReset( reducer ) { diff --git a/packages/block-editor/src/store/resolvers.js b/packages/block-editor/src/store/resolvers.js new file mode 100644 index 00000000000000..40c51d241ac676 --- /dev/null +++ b/packages/block-editor/src/store/resolvers.js @@ -0,0 +1,17 @@ +export const getFetchedPatterns = + () => + async ( { dispatch, select } ) => { + const { __experimentalFetchBlockPatterns } = select.getSettings(); + if ( ! __experimentalFetchBlockPatterns ) { + return []; + } + const patterns = await __experimentalFetchBlockPatterns(); + dispatch( { type: 'RECEIVE_BLOCK_PATTERNS', patterns } ); + }; + +getFetchedPatterns.shouldInvalidate = ( action ) => { + return ( + action.type === 'UPDATE_SETTINGS' && + !! action.settings.__experimentalFetchBlockPatterns + ); +}; diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 120b85a04af0ef..099c6b30222efc 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -27,11 +27,13 @@ import { createRegistrySelector } from '@wordpress/data'; * Internal dependencies */ import { - getAllPatterns, checkAllowListRecursive, checkAllowList, + getAllPatternsDependants, } from './utils'; import { orderBy } from '../utils/sorting'; +import { STORE_NAME } from './constants'; +import { unlock } from '../lock-unlock'; /** * A block selection object. @@ -2260,41 +2262,36 @@ export const __experimentalGetDirectInsertBlock = createSelector( ] ); -export const __experimentalGetParsedPattern = createSelector( - ( state, patternName ) => { - const patterns = getAllPatterns( state ); - const pattern = patterns.find( ( { name } ) => name === patternName ); - if ( ! pattern ) { - return null; - } - return { - ...pattern, - blocks: parse( pattern.content, { - __unstableSkipMigrationLogs: true, - } ), - }; - }, - ( state ) => [ getAllPatterns( state ) ] -); - -const getAllAllowedPatterns = createSelector( - ( state ) => { - const patterns = getAllPatterns( state ); - const { allowedBlockTypes } = getSettings( state ); - - const parsedPatterns = patterns - .filter( ( { inserter = true } ) => !! inserter ) - .map( ( { name } ) => - __experimentalGetParsedPattern( state, name ) +export const __experimentalGetParsedPattern = createRegistrySelector( + ( select ) => + createSelector( ( state, patternName ) => { + const { getAllPatterns } = unlock( select( STORE_NAME ) ); + const patterns = getAllPatterns(); + const pattern = patterns.find( + ( { name } ) => name === patternName ); - const allowedPatterns = parsedPatterns.filter( ( { blocks } ) => - checkAllowListRecursive( blocks, allowedBlockTypes ) - ); - return allowedPatterns; - }, - ( state ) => [ getAllPatterns( state ), state.settings.allowedBlockTypes ] + if ( ! pattern ) { + return null; + } + return { + ...pattern, + blocks: parse( pattern.content, { + __unstableSkipMigrationLogs: true, + } ), + }; + }, getAllPatternsDependants ) ); +const getAllowedPatternsDependants = ( state, rootClientId ) => { + return [ + ...getAllPatternsDependants( state ), + state.settings.allowedBlockTypes, + state.settings.templateLock, + state.blockListSettings[ rootClientId ], + state.blocks.byClientId.get( rootClientId ), + ]; +}; + /** * Returns the list of allowed patterns for inner blocks children. * @@ -2303,24 +2300,33 @@ const getAllAllowedPatterns = createSelector( * * @return {Array?} The list of allowed patterns. */ -export const __experimentalGetAllowedPatterns = createSelector( - ( state, rootClientId = null ) => { - const availableParsedPatterns = getAllAllowedPatterns( state ); - const patternsAllowed = availableParsedPatterns.filter( - ( { blocks } ) => - blocks.every( ( { name } ) => - canInsertBlockType( state, name, rootClientId ) - ) - ); +export const __experimentalGetAllowedPatterns = createRegistrySelector( + ( select ) => { + return createSelector( ( state, rootClientId = null ) => { + const { + getAllPatterns, + __experimentalGetParsedPattern: getParsedPattern, + } = unlock( select( STORE_NAME ) ); + const patterns = getAllPatterns(); + const { allowedBlockTypes } = getSettings( state ); + + const parsedPatterns = patterns + .filter( ( { inserter = true } ) => !! inserter ) + .map( ( { name } ) => getParsedPattern( name ) ); + const availableParsedPatterns = parsedPatterns.filter( + ( { blocks } ) => + checkAllowListRecursive( blocks, allowedBlockTypes ) + ); + const patternsAllowed = availableParsedPatterns.filter( + ( { blocks } ) => + blocks.every( ( { name } ) => + canInsertBlockType( state, name, rootClientId ) + ) + ); - return patternsAllowed; - }, - ( state, rootClientId ) => [ - getAllAllowedPatterns( state ), - state.settings.templateLock, - state.blockListSettings[ rootClientId ], - state.blocks.byClientId.get( rootClientId ), - ] + return patternsAllowed; + }, getAllowedPatternsDependants ); + } ); /** @@ -2336,36 +2342,34 @@ export const __experimentalGetAllowedPatterns = createSelector( * * @return {Array} The list of matched block patterns based on declared `blockTypes` and block name. */ -export const getPatternsByBlockTypes = createSelector( - ( state, blockNames, rootClientId = null ) => { - if ( ! blockNames ) return EMPTY_ARRAY; - const patterns = __experimentalGetAllowedPatterns( - state, - rootClientId - ); - const normalizedBlockNames = Array.isArray( blockNames ) - ? blockNames - : [ blockNames ]; - const filteredPatterns = patterns.filter( ( pattern ) => - pattern?.blockTypes?.some?.( ( blockName ) => - normalizedBlockNames.includes( blockName ) - ) - ); - if ( filteredPatterns.length === 0 ) { - return EMPTY_ARRAY; - } - return filteredPatterns; - }, - ( state, blockNames, rootClientId ) => [ - ...__experimentalGetAllowedPatterns.getDependants( - state, - rootClientId - ), - ] +export const getPatternsByBlockTypes = createRegistrySelector( ( select ) => + createSelector( + ( state, blockNames, rootClientId = null ) => { + if ( ! blockNames ) return EMPTY_ARRAY; + const patterns = + select( STORE_NAME ).__experimentalGetAllowedPatterns( + rootClientId + ); + const normalizedBlockNames = Array.isArray( blockNames ) + ? blockNames + : [ blockNames ]; + const filteredPatterns = patterns.filter( ( pattern ) => + pattern?.blockTypes?.some?.( ( blockName ) => + normalizedBlockNames.includes( blockName ) + ) + ); + if ( filteredPatterns.length === 0 ) { + return EMPTY_ARRAY; + } + return filteredPatterns; + }, + ( state, blockNames, rootClientId ) => + getAllowedPatternsDependants( state, rootClientId ) + ) ); -export const __experimentalGetPatternsByBlockTypes = createSelector( - ( state, blockNames, rootClientId = null ) => { +export const __experimentalGetPatternsByBlockTypes = createRegistrySelector( + ( select ) => { deprecated( 'wp.data.select( "core/block-editor" ).__experimentalGetPatternsByBlockTypes', { @@ -2375,14 +2379,8 @@ export const __experimentalGetPatternsByBlockTypes = createSelector( version: '6.4', } ); - return getPatternsByBlockTypes( state, blockNames, rootClientId ); - }, - ( state, blockNames, rootClientId ) => [ - ...__experimentalGetAllowedPatterns.getDependants( - state, - rootClientId - ), - ] + return select( STORE_NAME ).getPatternsByBlockTypes; + } ); /** @@ -2402,45 +2400,46 @@ export const __experimentalGetPatternsByBlockTypes = createSelector( * * @return {WPBlockPattern[]} Items that are eligible for a pattern transformation. */ -export const __experimentalGetPatternTransformItems = createSelector( - ( state, blocks, rootClientId = null ) => { - if ( ! blocks ) return EMPTY_ARRAY; - /** - * For now we only handle blocks without InnerBlocks and take into account - * the `__experimentalRole` property of blocks' attributes for the transformation. - * Note that the blocks have been retrieved through `getBlock`, which doesn't - * return the inner blocks of an inner block controller, so we still need - * to check for this case too. - */ - if ( - blocks.some( - ( { clientId, innerBlocks } ) => - innerBlocks.length || - areInnerBlocksControlled( state, clientId ) - ) - ) { - return EMPTY_ARRAY; - } - - // Create a Set of the selected block names that is used in patterns filtering. - const selectedBlockNames = Array.from( - new Set( blocks.map( ( { name } ) => name ) ) - ); - /** - * Here we will return first set of possible eligible block patterns, - * by checking the `blockTypes` property. We still have to recurse through - * block pattern's blocks and try to find matches from the selected blocks. - * Now this happens in the consumer to avoid heavy operations in the selector. - */ - return getPatternsByBlockTypes( - state, - selectedBlockNames, - rootClientId - ); - }, - ( state, blocks, rootClientId ) => [ - ...getPatternsByBlockTypes.getDependants( state, rootClientId ), - ] +export const __experimentalGetPatternTransformItems = createRegistrySelector( + ( select ) => + createSelector( + ( state, blocks, rootClientId = null ) => { + if ( ! blocks ) return EMPTY_ARRAY; + /** + * For now we only handle blocks without InnerBlocks and take into account + * the `__experimentalRole` property of blocks' attributes for the transformation. + * Note that the blocks have been retrieved through `getBlock`, which doesn't + * return the inner blocks of an inner block controller, so we still need + * to check for this case too. + */ + if ( + blocks.some( + ( { clientId, innerBlocks } ) => + innerBlocks.length || + areInnerBlocksControlled( state, clientId ) + ) + ) { + return EMPTY_ARRAY; + } + + // Create a Set of the selected block names that is used in patterns filtering. + const selectedBlockNames = Array.from( + new Set( blocks.map( ( { name } ) => name ) ) + ); + /** + * Here we will return first set of possible eligible block patterns, + * by checking the `blockTypes` property. We still have to recurse through + * block pattern's blocks and try to find matches from the selected blocks. + * Now this happens in the consumer to avoid heavy operations in the selector. + */ + return select( STORE_NAME ).getPatternsByBlockTypes( + selectedBlockNames, + rootClientId + ); + }, + ( state, blocks, rootClientId ) => + getAllowedPatternsDependants( state, rootClientId ) + ) ); /** diff --git a/packages/block-editor/src/store/test/registry-selectors.js b/packages/block-editor/src/store/test/registry-selectors.js new file mode 100644 index 00000000000000..89115c75f05144 --- /dev/null +++ b/packages/block-editor/src/store/test/registry-selectors.js @@ -0,0 +1,431 @@ +/** + * WordPress dependencies + */ +import { registerBlockType, unregisterBlockType } from '@wordpress/blocks'; +import { select, dispatch } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { store } from '../'; + +describe( 'selectors', () => { + beforeEach( () => { + registerBlockType( 'core/test-block-a', { + save: ( props ) => props.attributes.text, + category: 'design', + title: 'Test Block A', + icon: 'test', + keywords: [ 'testing' ], + } ); + + registerBlockType( 'core/test-block-b', { + save: ( props ) => props.attributes.text, + category: 'text', + title: 'Test Block B', + icon: 'test', + keywords: [ 'testing' ], + supports: { + multiple: false, + }, + } ); + } ); + + afterEach( async () => { + unregisterBlockType( 'core/test-block-a' ); + unregisterBlockType( 'core/test-block-b' ); + } ); + + describe( '__experimentalGetAllowedPatterns', () => { + beforeAll( async () => { + await dispatch( store ).resetBlocks( [ + { + clientId: 'block1', + name: 'core/test-block-a', + innerBlocks: [], + }, + { + clientId: 'block2', + name: 'core/test-block-b', + innerBlocks: [], + }, + ] ); + await dispatch( store ).updateSettings( { + __experimentalBlockPatterns: [ + { + name: 'pattern-a', + title: 'pattern with a', + content: ``, + }, + { + name: 'pattern-b', + title: 'pattern with b', + content: + '', + }, + { + name: 'pattern-c', + title: 'pattern hidden from UI', + inserter: false, + content: + '', + }, + ], + } ); + await dispatch( store ).updateBlockListSettings( 'block1', { + allowedBlocks: [ 'core/test-block-b' ], + } ); + } ); + + afterAll( async () => { + await dispatch( store ).resetBlocks( [] ); + await dispatch( store ).updateSettings( { + __experimentalBlockPatterns: [], + } ); + await dispatch( store ).updateBlockListSettings( 'block1', {} ); + } ); + + it( 'should return all patterns for root level', () => { + expect( + select( store ).__experimentalGetAllowedPatterns( null ) + ).toHaveLength( 2 ); + } ); + it( 'should return patterns that consists of blocks allowed for the specified client ID', () => { + expect( + select( store ).__experimentalGetAllowedPatterns( 'block1' ) + ).toHaveLength( 1 ); + expect( + select( store ).__experimentalGetAllowedPatterns( 'block2' ) + ).toHaveLength( 0 ); + } ); + it( 'should return empty array if only patterns hidden from UI exist', () => { + expect( + select( store ).__experimentalGetAllowedPatterns( { + blocks: { byClientId: new Map() }, + blockListSettings: {}, + settings: { + __experimentalBlockPatterns: [ + { + name: 'pattern-c', + title: 'pattern hidden from UI', + inserter: false, + content: + '', + }, + ], + }, + } ) + ).toHaveLength( 0 ); + } ); + } ); + + describe( '__experimentalGetParsedPattern', () => { + beforeAll( async () => { + await dispatch( store ).updateSettings( { + __experimentalBlockPatterns: [ + { + name: 'pattern-a', + title: 'pattern with a', + content: ``, + }, + { + name: 'pattern-hidden-from-ui', + title: 'pattern hidden from UI', + inserter: false, + content: + '', + }, + ], + } ); + } ); + + afterAll( async () => { + await dispatch( store ).updateSettings( { + __experimentalBlockPatterns: [], + } ); + } ); + + it( 'should return proper results when pattern does not exist', () => { + expect( + select( store ).__experimentalGetParsedPattern( 'not there' ) + ).toBeNull(); + } ); + it( 'should return existing pattern properly parsed', () => { + const { name, blocks } = + select( store ).__experimentalGetParsedPattern( 'pattern-a' ); + expect( name ).toEqual( 'pattern-a' ); + expect( blocks ).toHaveLength( 1 ); + expect( blocks[ 0 ] ).toEqual( + expect.objectContaining( { + name: 'core/test-block-a', + } ) + ); + } ); + it( 'should return hidden from UI pattern when requested', () => { + const { name, blocks, inserter } = select( + store + ).__experimentalGetParsedPattern( 'pattern-hidden-from-ui' ); + expect( name ).toEqual( 'pattern-hidden-from-ui' ); + expect( inserter ).toBeFalsy(); + expect( blocks ).toHaveLength( 2 ); + expect( blocks[ 0 ] ).toEqual( + expect.objectContaining( { + name: 'core/test-block-a', + } ) + ); + } ); + } ); + + describe( 'getPatternsByBlockTypes', () => { + beforeAll( async () => { + await dispatch( store ).resetBlocks( [ + { + clientId: 'block1', + name: 'core/test-block-a', + innerBlocks: [], + }, + ] ); + await dispatch( store ).updateSettings( { + __experimentalBlockPatterns: [ + { + name: 'pattern-a', + blockTypes: [ 'test/block-a' ], + title: 'pattern a', + content: + '', + }, + { + name: 'pattern-b', + blockTypes: [ 'test/block-b' ], + title: 'pattern b', + content: + '', + }, + { + title: 'pattern c', + blockTypes: [ 'test/block-a' ], + content: + '', + }, + ], + } ); + await dispatch( store ).updateBlockListSettings( 'block1', { + allowedBlocks: [ 'core/test-block-b' ], + } ); + } ); + + afterAll( async () => { + await dispatch( store ).resetBlocks( [] ); + await dispatch( store ).updateSettings( { + __experimentalBlockPatterns: [], + } ); + await dispatch( store ).updateBlockListSettings( 'block1', {} ); + } ); + + it( 'should return empty array if no block name is provided', () => { + expect( select( store ).getPatternsByBlockTypes() ).toEqual( [] ); + } ); + it( 'should return empty array if no match is found', () => { + const patterns = select( store ).getPatternsByBlockTypes( + 'test/block-not-exists' + ); + expect( patterns ).toEqual( [] ); + } ); + it( 'should return the same empty array in both empty array cases', () => { + const patterns1 = select( store ).getPatternsByBlockTypes(); + const patterns2 = select( store ).getPatternsByBlockTypes( + 'test/block-not-exists' + ); + expect( patterns1 ).toBe( patterns2 ); + } ); + it( 'should return proper results when there are matched block patterns', () => { + const patterns = + select( store ).getPatternsByBlockTypes( 'test/block-a' ); + expect( patterns ).toHaveLength( 2 ); + expect( patterns ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { title: 'pattern a' } ), + expect.objectContaining( { title: 'pattern c' } ), + ] ) + ); + } ); + it( 'should return proper result with matched patterns and allowed blocks from rootClientId', () => { + const patterns = select( store ).getPatternsByBlockTypes( + 'test/block-a', + 'block1' + ); + expect( patterns ).toHaveLength( 1 ); + expect( patterns[ 0 ] ).toEqual( + expect.objectContaining( { title: 'pattern c' } ) + ); + } ); + } ); + + describe( '__experimentalGetPatternTransformItems', () => { + beforeAll( async () => { + await dispatch( store ).resetBlocks( [ + { + clientId: 'block1', + name: 'core/test-block-a', + innerBlocks: [], + }, + { + clientId: 'block2', + name: 'core/test-block-b', + innerBlocks: [], + }, + ] ); + await dispatch( store ).setHasControlledInnerBlocks( + 'block2-clientId', + true + ); + await dispatch( store ).updateSettings( { + __experimentalBlockPatterns: [ + { + name: 'pattern-a', + blockTypes: [ 'test/block-a' ], + title: 'pattern a', + content: + '', + }, + { + name: 'pattern-b', + blockTypes: [ 'test/block-b' ], + title: 'pattern b', + content: + '', + }, + { + name: 'pattern-c', + title: 'pattern c', + blockTypes: [ 'test/block-a' ], + content: + '', + }, + { + name: 'pattern-mix', + title: 'pattern mix', + blockTypes: [ + 'core/test-block-a', + 'core/test-block-b', + ], + content: + '', + }, + ], + } ); + } ); + + afterAll( async () => { + await dispatch( store ).resetBlocks( [] ); + await dispatch( store ).updateSettings( { + __experimentalBlockPatterns: [], + } ); + await dispatch( store ).updateBlockListSettings( 'block1', {} ); + } ); + + describe( 'should return empty array', () => { + it( 'when no blocks are selected', () => { + expect( + select( store ).__experimentalGetPatternTransformItems() + ).toEqual( [] ); + } ); + it( 'when a selected block has inner blocks', () => { + const blocks = [ + { name: 'core/test-block-a', innerBlocks: [] }, + { + name: 'core/test-block-b', + innerBlocks: [ { name: 'some inner block' } ], + }, + ]; + expect( + select( store ).__experimentalGetPatternTransformItems( + blocks + ) + ).toEqual( [] ); + } ); + it( 'when a selected block has controlled inner blocks', () => { + const blocks = [ + { name: 'core/test-block-a', innerBlocks: [] }, + { + name: 'core/test-block-b', + clientId: 'block2-clientId', + innerBlocks: [], + }, + ]; + expect( + select( store ).__experimentalGetPatternTransformItems( + blocks + ) + ).toEqual( [] ); + } ); + it( 'when no patterns are available based on the selected blocks', () => { + const blocks = [ + { name: 'block-with-no-patterns', innerBlocks: [] }, + ]; + expect( + select( store ).__experimentalGetPatternTransformItems( + blocks + ) + ).toEqual( [] ); + } ); + } ); + describe( 'should return proper results', () => { + it( 'when a single block is selected', () => { + const blocks = [ + { name: 'core/test-block-b', innerBlocks: [] }, + ]; + const patterns = + select( store ).__experimentalGetPatternTransformItems( + blocks + ); + expect( patterns ).toHaveLength( 1 ); + expect( patterns[ 0 ] ).toEqual( + expect.objectContaining( { + name: 'pattern-mix', + } ) + ); + } ); + it( 'when different multiple blocks are selected', () => { + const blocks = [ + { name: 'core/test-block-b', innerBlocks: [] }, + { name: 'test/block-b', innerBlocks: [] }, + { name: 'some other block', innerBlocks: [] }, + ]; + const patterns = + select( store ).__experimentalGetPatternTransformItems( + blocks + ); + expect( patterns ).toHaveLength( 2 ); + expect( patterns ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + name: 'pattern-mix', + } ), + expect.objectContaining( { + name: 'pattern-b', + } ), + ] ) + ); + } ); + it( 'when multiple blocks are selected containing multiple times the same block', () => { + const blocks = [ + { name: 'core/test-block-b', innerBlocks: [] }, + { name: 'some other block', innerBlocks: [] }, + { name: 'core/test-block-a', innerBlocks: [] }, + { name: 'core/test-block-b', innerBlocks: [] }, + ]; + const patterns = + select( store ).__experimentalGetPatternTransformItems( + blocks + ); + expect( patterns ).toHaveLength( 1 ); + expect( patterns[ 0 ] ).toEqual( + expect.objectContaining( { + name: 'pattern-mix', + } ) + ); + } ); + } ); + } ); +} ); diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js index b9cc5a9c306984..29833611b17f4a 100644 --- a/packages/block-editor/src/store/test/selectors.js +++ b/packages/block-editor/src/store/test/selectors.js @@ -65,12 +65,8 @@ const { __experimentalGetLastBlockAttributeChanges, getLowestCommonAncestorWithSelectedBlock, __experimentalGetActiveBlockIdByBlockNames: getActiveBlockIdByBlockNames, - __experimentalGetAllowedPatterns, - __experimentalGetParsedPattern, - getPatternsByBlockTypes, __unstableGetClientIdWithClientIdsTree, __unstableGetClientIdsTree, - __experimentalGetPatternTransformItems, wasBlockJustInserted, getBlocksByName, getBlockEditingMode, @@ -4199,382 +4195,6 @@ describe( 'selectors', () => { } ); } ); - describe( '__experimentalGetAllowedPatterns', () => { - const state = { - blocks: { - byClientId: new Map( - Object.entries( { - block1: { name: 'core/test-block-a' }, - block2: { name: 'core/test-block-b' }, - } ) - ), - attributes: new Map( - Object.entries( { - block1: {}, - block2: {}, - } ) - ), - parents: new Map( - Object.entries( { - block1: '', - block2: '', - } ) - ), - }, - blockListSettings: { - block1: { - allowedBlocks: [ 'core/test-block-b' ], - }, - block2: { - allowedBlocks: [], - }, - }, - settings: { - __experimentalBlockPatterns: [ - { - name: 'pattern-a', - title: 'pattern with a', - content: ``, - }, - { - name: 'pattern-b', - title: 'pattern with b', - content: - '', - }, - { - name: 'pattern-c', - title: 'pattern hidden from UI', - inserter: false, - content: - '', - }, - ], - }, - blockEditingModes: new Map(), - }; - - it( 'should return all patterns for root level', () => { - expect( - __experimentalGetAllowedPatterns( state, null ) - ).toHaveLength( 2 ); - } ); - - it( 'should return patterns that consists of blocks allowed for the specified client ID', () => { - expect( - __experimentalGetAllowedPatterns( state, 'block1' ) - ).toHaveLength( 1 ); - - expect( - __experimentalGetAllowedPatterns( state, 'block2' ) - ).toHaveLength( 0 ); - } ); - it( 'should return empty array if only patterns hidden from UI exist', () => { - expect( - __experimentalGetAllowedPatterns( { - blocks: { byClientId: new Map() }, - blockListSettings: {}, - settings: { - __experimentalBlockPatterns: [ - { - name: 'pattern-c', - title: 'pattern hidden from UI', - inserter: false, - content: - '', - }, - ], - }, - } ) - ).toHaveLength( 0 ); - } ); - } ); - describe( '__experimentalGetParsedPattern', () => { - const state = { - settings: { - __experimentalBlockPatterns: [ - { - name: 'pattern-a', - title: 'pattern with a', - content: ``, - }, - { - name: 'pattern-hidden-from-ui', - title: 'pattern hidden from UI', - inserter: false, - content: - '', - }, - ], - }, - }; - it( 'should return proper results when pattern does not exist', () => { - expect( - __experimentalGetParsedPattern( state, 'not there' ) - ).toBeNull(); - } ); - it( 'should return existing pattern properly parsed', () => { - const { name, blocks } = __experimentalGetParsedPattern( - state, - 'pattern-a' - ); - expect( name ).toEqual( 'pattern-a' ); - expect( blocks ).toHaveLength( 1 ); - expect( blocks[ 0 ] ).toEqual( - expect.objectContaining( { - name: 'core/test-block-a', - } ) - ); - } ); - it( 'should return hidden from UI pattern when requested', () => { - const { name, blocks, inserter } = __experimentalGetParsedPattern( - state, - 'pattern-hidden-from-ui' - ); - expect( name ).toEqual( 'pattern-hidden-from-ui' ); - expect( inserter ).toBeFalsy(); - expect( blocks ).toHaveLength( 2 ); - expect( blocks[ 0 ] ).toEqual( - expect.objectContaining( { - name: 'core/test-block-a', - } ) - ); - } ); - } ); - describe( 'getPatternsByBlockTypes', () => { - const state = { - blocks: { - byClientId: new Map( - Object.entries( { - block1: { name: 'core/test-block-a' }, - } ) - ), - parents: new Map(), - }, - blockListSettings: { - block1: { - allowedBlocks: [ 'core/test-block-b' ], - }, - }, - settings: { - __experimentalBlockPatterns: [ - { - name: 'pattern-a', - blockTypes: [ 'test/block-a' ], - title: 'pattern a', - content: - '', - }, - { - name: 'pattern-b', - blockTypes: [ 'test/block-b' ], - title: 'pattern b', - content: - '', - }, - { - title: 'pattern c', - blockTypes: [ 'test/block-a' ], - content: - '', - }, - ], - }, - blockEditingModes: new Map(), - }; - it( 'should return empty array if no block name is provided', () => { - expect( getPatternsByBlockTypes( state ) ).toEqual( [] ); - } ); - it( 'should return empty array if no match is found', () => { - const patterns = getPatternsByBlockTypes( - state, - 'test/block-not-exists' - ); - expect( patterns ).toEqual( [] ); - } ); - it( 'should return the same empty array in both empty array cases', () => { - const patterns1 = getPatternsByBlockTypes( state ); - const patterns2 = getPatternsByBlockTypes( - state, - 'test/block-not-exists' - ); - expect( patterns1 ).toBe( patterns2 ); - } ); - it( 'should return proper results when there are matched block patterns', () => { - const patterns = getPatternsByBlockTypes( state, 'test/block-a' ); - expect( patterns ).toHaveLength( 2 ); - expect( patterns ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { title: 'pattern a' } ), - expect.objectContaining( { title: 'pattern c' } ), - ] ) - ); - } ); - it( 'should return proper result with matched patterns and allowed blocks from rootClientId', () => { - const patterns = getPatternsByBlockTypes( - state, - 'test/block-a', - 'block1' - ); - expect( patterns ).toHaveLength( 1 ); - expect( patterns[ 0 ] ).toEqual( - expect.objectContaining( { title: 'pattern c' } ) - ); - } ); - } ); - describe( '__experimentalGetPatternTransformItems', () => { - const state = { - blocks: { - byClientId: new Map( - Object.entries( { - block1: { name: 'core/test-block-a' }, - block2: { name: 'core/test-block-b' }, - } ) - ), - parents: new Map(), - controlledInnerBlocks: { 'block2-clientId': true }, - }, - blockListSettings: { - block1: { - allowedBlocks: [ 'core/test-block-b' ], - }, - }, - settings: { - __experimentalBlockPatterns: [ - { - name: 'pattern-a', - blockTypes: [ 'test/block-a' ], - title: 'pattern a', - content: - '', - }, - { - name: 'pattern-b', - blockTypes: [ 'test/block-b' ], - title: 'pattern b', - content: - '', - }, - { - name: 'pattern-c', - title: 'pattern c', - blockTypes: [ 'test/block-a' ], - content: - '', - }, - { - name: 'pattern-mix', - title: 'pattern mix', - blockTypes: [ - 'core/test-block-a', - 'core/test-block-b', - ], - content: - '', - }, - ], - }, - blockEditingModes: new Map(), - }; - describe( 'should return empty array', () => { - it( 'when no blocks are selected', () => { - expect( - __experimentalGetPatternTransformItems( state ) - ).toEqual( [] ); - } ); - it( 'when a selected block has inner blocks', () => { - const blocks = [ - { name: 'core/test-block-a', innerBlocks: [] }, - { - name: 'core/test-block-b', - innerBlocks: [ { name: 'some inner block' } ], - }, - ]; - expect( - __experimentalGetPatternTransformItems( state, blocks ) - ).toEqual( [] ); - } ); - it( 'when a selected block has controlled inner blocks', () => { - const blocks = [ - { name: 'core/test-block-a', innerBlocks: [] }, - { - name: 'core/test-block-b', - clientId: 'block2-clientId', - innerBlocks: [], - }, - ]; - expect( - __experimentalGetPatternTransformItems( state, blocks ) - ).toEqual( [] ); - } ); - it( 'when no patterns are available based on the selected blocks', () => { - const blocks = [ - { name: 'block-with-no-patterns', innerBlocks: [] }, - ]; - expect( - __experimentalGetPatternTransformItems( state, blocks ) - ).toEqual( [] ); - } ); - } ); - describe( 'should return proper results', () => { - it( 'when a single block is selected', () => { - const blocks = [ - { name: 'core/test-block-b', innerBlocks: [] }, - ]; - const patterns = __experimentalGetPatternTransformItems( - state, - blocks - ); - expect( patterns ).toHaveLength( 1 ); - expect( patterns[ 0 ] ).toEqual( - expect.objectContaining( { - name: 'pattern-mix', - } ) - ); - } ); - it( 'when different multiple blocks are selected', () => { - const blocks = [ - { name: 'core/test-block-b', innerBlocks: [] }, - { name: 'test/block-b', innerBlocks: [] }, - { name: 'some other block', innerBlocks: [] }, - ]; - const patterns = __experimentalGetPatternTransformItems( - state, - blocks - ); - expect( patterns ).toHaveLength( 2 ); - expect( patterns ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - name: 'pattern-mix', - } ), - expect.objectContaining( { - name: 'pattern-b', - } ), - ] ) - ); - } ); - it( 'when multiple blocks are selected containing multiple times the same block', () => { - const blocks = [ - { name: 'core/test-block-b', innerBlocks: [] }, - { name: 'some other block', innerBlocks: [] }, - { name: 'core/test-block-a', innerBlocks: [] }, - { name: 'core/test-block-b', innerBlocks: [] }, - ]; - const patterns = __experimentalGetPatternTransformItems( - state, - blocks - ); - expect( patterns ).toHaveLength( 1 ); - expect( patterns[ 0 ] ).toEqual( - expect.objectContaining( { - name: 'pattern-mix', - } ) - ); - } ); - } ); - } ); - describe( 'wasBlockJustInserted', () => { it( 'should return true if the client id passed to wasBlockJustInserted is found within the state', () => { const expectedClientId = '62bfef6e-d5e9-43ba-b7f9-c77cf354141f'; diff --git a/packages/block-editor/src/store/utils.js b/packages/block-editor/src/store/utils.js index 7587dcdf56fd79..6cde56da1b55a7 100644 --- a/packages/block-editor/src/store/utils.js +++ b/packages/block-editor/src/store/utils.js @@ -1,53 +1,3 @@ -/** - * External dependencies - */ -import createSelector from 'rememo'; - -/** - * Internal dependencies - */ -import { INSERTER_PATTERN_TYPES } from '../components/inserter/block-patterns-tab/utils'; - -export const getUserPatterns = createSelector( - ( state ) => { - const userPatterns = state.settings.__experimentalReusableBlocks ?? []; - const userPatternCategories = - state.settings.__experimentalUserPatternCategories ?? []; - return userPatterns.map( ( userPattern ) => { - return { - name: `core/block/${ userPattern.id }`, - id: userPattern.id, - type: INSERTER_PATTERN_TYPES.user, - title: userPattern.title.raw, - categories: userPattern.wp_pattern_category.map( ( catId ) => { - const category = userPatternCategories.find( - ( { id } ) => id === catId - ); - return category ? category.slug : catId; - } ), - content: userPattern.content.raw, - syncStatus: userPattern.wp_pattern_sync_status, - }; - } ); - }, - ( state ) => [ - state.settings.__experimentalReusableBlocks, - state.settings.__experimentalUserPatternCategories, - ] -); - -export const getAllPatterns = createSelector( - ( state ) => { - const patterns = state.settings.__experimentalBlockPatterns; - const userPatterns = getUserPatterns( state ); - return [ ...userPatterns, ...patterns ]; - }, - ( state ) => [ - state.settings.__experimentalBlockPatterns, - getUserPatterns( state ), - ] -); - export const checkAllowList = ( list, item, defaultResult = null ) => { if ( typeof list === 'boolean' ) { return list; @@ -89,3 +39,13 @@ export const checkAllowListRecursive = ( blocks, allowedBlockTypes ) => { return true; }; + +export const getAllPatternsDependants = ( state ) => { + return [ + state.settings.__experimentalBlockPatterns, + state.settings.__experimentalUserPatternCategories, + state.settings.__experimentalReusableBlocks, + state.settings.__experimentalFetchBlockPatterns, + state.blockPatterns, + ]; +}; diff --git a/packages/core-data/src/fetch/index.js b/packages/core-data/src/fetch/index.js index 8d4d28e3b0db82..de45f3399dac87 100644 --- a/packages/core-data/src/fetch/index.js +++ b/packages/core-data/src/fetch/index.js @@ -1,2 +1,29 @@ +/** + * External dependencies + */ +import { camelCase } from 'change-case'; + +/** + * WordPress dependencies + */ +import apiFetch from '@wordpress/api-fetch'; + export { default as __experimentalFetchLinkSuggestions } from './__experimental-fetch-link-suggestions'; export { default as __experimentalFetchUrlData } from './__experimental-fetch-url-data'; + +export async function fetchBlockPatterns() { + const restPatterns = await apiFetch( { + path: '/wp/v2/block-patterns/patterns', + } ); + if ( ! restPatterns ) { + return []; + } + return restPatterns.map( ( pattern ) => + Object.fromEntries( + Object.entries( pattern ).map( ( [ key, value ] ) => [ + camelCase( key ), + value, + ] ) + ) + ); +} diff --git a/packages/core-data/src/resolvers.js b/packages/core-data/src/resolvers.js index 807005ec4a6e8d..7f4d3f54776d41 100644 --- a/packages/core-data/src/resolvers.js +++ b/packages/core-data/src/resolvers.js @@ -17,6 +17,7 @@ import { STORE_NAME } from './name'; import { getOrLoadEntitiesConfig, DEFAULT_ENTITY_KEY } from './entities'; import { forwardResolver, getNormalizedCommaSeparable } from './utils'; import { getSyncProvider } from './sync'; +import { fetchBlockPatterns } from './fetch'; /** * Requests authors from the REST API. @@ -619,17 +620,7 @@ getCurrentThemeGlobalStylesRevisions.shouldInvalidate = ( action ) => { export const getBlockPatterns = () => async ( { dispatch } ) => { - const restPatterns = await apiFetch( { - path: '/wp/v2/block-patterns/patterns', - } ); - const patterns = restPatterns?.map( ( pattern ) => - Object.fromEntries( - Object.entries( pattern ).map( ( [ key, value ] ) => [ - camelCase( key ), - value, - ] ) - ) - ); + const patterns = await fetchBlockPatterns(); dispatch( { type: 'RECEIVE_BLOCK_PATTERNS', patterns } ); }; diff --git a/packages/editor/src/components/provider/use-block-editor-settings.js b/packages/editor/src/components/provider/use-block-editor-settings.js index 5a9b3a82b1bdbb..78c9b1a56e83a7 100644 --- a/packages/editor/src/components/provider/use-block-editor-settings.js +++ b/packages/editor/src/components/provider/use-block-editor-settings.js @@ -7,6 +7,7 @@ import { store as coreStore, __experimentalFetchLinkSuggestions as fetchLinkSuggestions, __experimentalFetchUrlData as fetchUrlData, + fetchBlockPatterns, } from '@wordpress/core-data'; import { __ } from '@wordpress/i18n'; import { store as preferencesStore } from '@wordpress/preferences'; @@ -101,7 +102,6 @@ function useBlockEditorSettings( settings, postType, postId ) { pageOnFront, pageForPosts, userPatternCategories, - restBlockPatterns, restBlockPatternCategories, } = useSelect( ( select ) => { @@ -112,7 +112,6 @@ function useBlockEditorSettings( settings, postType, postId ) { getEntityRecord, getUserPatternCategories, getEntityRecords, - getBlockPatterns, getBlockPatternCategories, } = select( coreStore ); const { get } = select( preferencesStore ); @@ -148,7 +147,6 @@ function useBlockEditorSettings( settings, postType, postId ) { pageOnFront: siteSettings?.page_on_front, pageForPosts: siteSettings?.page_for_posts, userPatternCategories: getUserPatternCategories(), - restBlockPatterns: getBlockPatterns(), restBlockPatternCategories: getBlockPatternCategories(), }; }, @@ -164,22 +162,16 @@ function useBlockEditorSettings( settings, postType, postId ) { const blockPatterns = useMemo( () => - [ - ...( settingsBlockPatterns || [] ), - ...( restBlockPatterns || [] ), - ] - .filter( - ( x, index, arr ) => - index === arr.findIndex( ( y ) => x.name === y.name ) - ) - .filter( ( { postTypes } ) => { + [ ...( settingsBlockPatterns || [] ) ].filter( + ( { postTypes } ) => { return ( ! postTypes || ( Array.isArray( postTypes ) && postTypes.includes( postType ) ) ); - } ), - [ settingsBlockPatterns, restBlockPatterns, postType ] + } + ), + [ settingsBlockPatterns, postType ] ); const blockPatternCategories = useMemo( @@ -254,8 +246,19 @@ function useBlockEditorSettings( settings, postType, postId ) { isDistractionFree, keepCaretInsideBlock, mediaUpload: hasUploadPermissions ? mediaUpload : undefined, - __experimentalReusableBlocks: reusableBlocks, __experimentalBlockPatterns: blockPatterns, + __experimentalFetchBlockPatterns: async () => { + return ( await fetchBlockPatterns() ).filter( + ( { postTypes } ) => { + return ( + ! postTypes || + ( Array.isArray( postTypes ) && + postTypes.includes( postType ) ) + ); + } + ); + }, + __experimentalReusableBlocks: reusableBlocks, __experimentalBlockPatternCategories: blockPatternCategories, __experimentalUserPatternCategories: userPatternCategories, __experimentalFetchLinkSuggestions: ( search, searchOptions ) =>