From 3edb5707e8701ee9bc12611ab094c1c2ec1ae73a Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Wed, 2 Oct 2024 08:21:51 +0100 Subject: [PATCH] Inserter: Fix Block visibility manager (#65700) Co-authored-by: youknowriad Co-authored-by: t-hamano --- .../inserter/hooks/use-patterns-state.js | 42 ++--- packages/block-editor/src/store/selectors.js | 155 ++++++++++++------ .../editor/various/allowed-patterns.spec.js | 8 +- 3 files changed, 123 insertions(+), 82 deletions(-) diff --git a/packages/block-editor/src/components/inserter/hooks/use-patterns-state.js b/packages/block-editor/src/components/inserter/hooks/use-patterns-state.js index 13dae7f2ed7c0d..91b34c0ec72c3d 100644 --- a/packages/block-editor/src/components/inserter/hooks/use-patterns-state.js +++ b/packages/block-editor/src/components/inserter/hooks/use-patterns-state.js @@ -13,7 +13,7 @@ import { store as noticesStore } from '@wordpress/notices'; import { store as blockEditorStore } from '../../../store'; import { unlock } from '../../../lock-unlock'; import { INSERTER_PATTERN_TYPES } from '../block-patterns-tab/utils'; -import { getParsedPattern } from '../../../store/utils'; +import { isFiltered } from '../../../store/utils'; /** * Retrieves the block patterns inserter state. @@ -31,48 +31,34 @@ const usePatternsState = ( selectedCategory, isQuick ) => { - const { patternCategories, allPatterns, userPatternCategories } = useSelect( + const options = useMemo( + () => ( { [ isFiltered ]: !! isQuick } ), + [ isQuick ] + ); + const { patternCategories, patterns, userPatternCategories } = useSelect( ( select ) => { - const { - getAllPatterns, - getSettings, - __experimentalGetAllowedPatterns, - } = unlock( select( blockEditorStore ) ); + const { getSettings, __experimentalGetAllowedPatterns } = unlock( + select( blockEditorStore ) + ); const { __experimentalUserPatternCategories, __experimentalBlockPatternCategories, } = getSettings(); return { - allPatterns: isQuick - ? __experimentalGetAllowedPatterns() - : getAllPatterns(), + patterns: __experimentalGetAllowedPatterns( + rootClientId, + options + ), userPatternCategories: __experimentalUserPatternCategories, patternCategories: __experimentalBlockPatternCategories, }; }, - [ isQuick ] + [ rootClientId, options ] ); const { getClosestAllowedInsertionPointForPattern } = unlock( useSelect( blockEditorStore ) ); - const patterns = useMemo( - () => - isQuick - ? allPatterns - : allPatterns - .filter( ( { inserter = true } ) => !! inserter ) - .map( ( pattern ) => { - return { - ...pattern, - get blocks() { - return getParsedPattern( pattern ).blocks; - }, - }; - } ), - [ isQuick, allPatterns ] - ); - const allCategories = useMemo( () => { const categories = [ ...patternCategories ]; userPatternCategories?.forEach( ( userCategory ) => { diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 7bb002661565bd..6cf6aae296141f 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -1540,6 +1540,59 @@ export function getTemplateLock( state, rootClientId ) { return getBlockListSettings( state, rootClientId )?.templateLock ?? false; } +/** + * Determines if the given block type is visible in the inserter. + * Note that this is different than whether a block is allowed to be inserted. + * In some cases, the block is not allowed in a given position but + * it should still be visible in the inserter to be able to add it + * to a different position. + * + * @param {Object} state Editor state. + * @param {string|Object} blockNameOrType The block type object, e.g., the response + * from the block directory; or a string name of + * an installed block type, e.g.' core/paragraph'. + * + * @return {boolean} Whether the given block type is allowed to be inserted. + */ +const isBlockVisibleInTheInserter = ( state, blockNameOrType ) => { + let blockType; + let blockName; + if ( blockNameOrType && 'object' === typeof blockNameOrType ) { + blockType = blockNameOrType; + blockName = blockNameOrType.name; + } else { + blockType = getBlockType( blockNameOrType ); + blockName = blockNameOrType; + } + if ( ! blockType ) { + return false; + } + + const { allowedBlockTypes } = getSettings( state ); + + const isBlockAllowedInEditor = checkAllowList( + allowedBlockTypes, + blockName, + true + ); + if ( ! isBlockAllowedInEditor ) { + return false; + } + + // If parent blocks are not visible, child blocks should be hidden too. + if ( !! blockType.parent?.length ) { + return blockType.parent.some( + ( name ) => + isBlockVisibleInTheInserter( state, name ) || + // Exception for blocks with post-content parent, + // the root level is often consider as "core/post-content". + // This exception should only apply to the post editor ideally though. + name === 'core/post-content' + ); + } + return true; +}; + /** * Determines if the given block type is allowed to be inserted into the block list. * This function is not exported and not memoized because using a memoized selector @@ -1558,6 +1611,10 @@ const canInsertBlockTypeUnmemoized = ( blockName, rootClientId = null ) => { + if ( ! isBlockVisibleInTheInserter( state, blockName ) ) { + return false; + } + let blockType; if ( blockName && 'object' === typeof blockName ) { blockType = blockName; @@ -1565,20 +1622,6 @@ const canInsertBlockTypeUnmemoized = ( } else { blockType = getBlockType( blockName ); } - if ( ! blockType ) { - return false; - } - - const { allowedBlockTypes } = getSettings( state ); - - const isBlockAllowedInEditor = checkAllowList( - allowedBlockTypes, - blockName, - true - ); - if ( ! isBlockAllowedInEditor ) { - return false; - } const isLocked = !! getTemplateLock( state, rootClientId ); if ( isLocked ) { @@ -1972,6 +2015,7 @@ const buildBlockTypeItem = description: blockType.description, category: blockType.category, keywords: blockType.keywords, + parent: blockType.parent, variations: inserterVariations, example: blockType.example, utility: 1, // Deprecated. @@ -2067,16 +2111,18 @@ export const getInserterItems = createRegistrySelector( ( select ) => ) ); } else { - blockTypeInserterItems = blockTypeInserterItems.map( - ( blockType ) => ( { + blockTypeInserterItems = blockTypeInserterItems + .filter( ( blockType ) => + isBlockVisibleInTheInserter( state, blockType ) + ) + .map( ( blockType ) => ( { ...blockType, isAllowedInCurrentRoot: canIncludeBlockTypeInInserter( state, blockType, rootClientId ), - } ) - ); + } ) ); } const items = blockTypeInserterItems.reduce( @@ -2348,37 +2394,50 @@ const getAllowedPatternsDependants = ( select ) => ( state, rootClientId ) => [ */ export const __experimentalGetAllowedPatterns = createRegistrySelector( ( select ) => { - return createSelector( ( state, rootClientId = null ) => { - const { getAllPatterns } = unlock( select( STORE_NAME ) ); - const patterns = getAllPatterns(); - const { allowedBlockTypes } = getSettings( state ); - const parsedPatterns = patterns - .filter( ( { inserter = true } ) => !! inserter ) - .map( ( pattern ) => { - return { - ...pattern, - get blocks() { - return getParsedPattern( pattern ).blocks; - }, - }; - } ); - - const availableParsedPatterns = parsedPatterns.filter( - ( pattern ) => - checkAllowListRecursive( - getGrammar( pattern ), - allowedBlockTypes - ) - ); - const patternsAllowed = availableParsedPatterns.filter( - ( pattern ) => - getGrammar( pattern ).every( ( { blockName: name } ) => - canInsertBlockType( state, name, rootClientId ) - ) - ); + return createSelector( + ( + state, + rootClientId = null, + options = DEFAULT_INSERTER_OPTIONS + ) => { + const { getAllPatterns } = unlock( select( STORE_NAME ) ); + const patterns = getAllPatterns(); + const { allowedBlockTypes } = getSettings( state ); + const parsedPatterns = patterns + .filter( ( { inserter = true } ) => !! inserter ) + .map( ( pattern ) => { + return { + ...pattern, + get blocks() { + return getParsedPattern( pattern ).blocks; + }, + }; + } ); + + const availableParsedPatterns = parsedPatterns.filter( + ( pattern ) => + checkAllowListRecursive( + getGrammar( pattern ), + allowedBlockTypes + ) + ); + const patternsAllowed = availableParsedPatterns.filter( + ( pattern ) => + getGrammar( pattern ).every( ( { blockName: name } ) => + options[ isFiltered ] !== false + ? canInsertBlockType( + state, + name, + rootClientId + ) + : isBlockVisibleInTheInserter( state, name ) + ) + ); - return patternsAllowed; - }, getAllowedPatternsDependants( select ) ); + return patternsAllowed; + }, + getAllowedPatternsDependants( select ) + ); } ); diff --git a/test/e2e/specs/editor/various/allowed-patterns.spec.js b/test/e2e/specs/editor/various/allowed-patterns.spec.js index 894f143d19bb82..83d44403d60ee2 100644 --- a/test/e2e/specs/editor/various/allowed-patterns.spec.js +++ b/test/e2e/specs/editor/various/allowed-patterns.spec.js @@ -54,7 +54,7 @@ test.describe( 'Allowed Patterns', () => { ); } ); - test( 'should show all patterns even if not allowed', async ( { + test( 'should hide patterns with only hidden blocks', async ( { admin, page, } ) => { @@ -77,11 +77,7 @@ test.describe( 'Allowed Patterns', () => { page .getByRole( 'listbox', { name: 'Block patterns' } ) .getByRole( 'option' ) - ).toHaveText( [ - 'Test: Single heading', - 'Test: Single paragraph', - 'Test: Paragraph inside group', - ] ); + ).toHaveText( [ 'Test: Single heading' ] ); } ); } ); } );