diff --git a/packages/block-editor/src/components/inserter/block-types-tab.js b/packages/block-editor/src/components/inserter/block-types-tab.js index 50a8b46b46427..844d5dd341437 100644 --- a/packages/block-editor/src/components/inserter/block-types-tab.js +++ b/packages/block-editor/src/components/inserter/block-types-tab.js @@ -186,7 +186,7 @@ export function BlockTypesTab( continue; } - if ( rootClientId && item.rootClientId === rootClientId ) { + if ( rootClientId && item.isAllowedInCurrentRoot ) { itemsForCurrentRoot.push( item ); } else { itemsRemaining.push( item ); diff --git a/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js b/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js index 8db23267eee8f..6f11060c75c49 100644 --- a/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js +++ b/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js @@ -2,19 +2,23 @@ * WordPress dependencies */ import { + getBlockType, createBlock, createBlocksFromInnerBlocksTemplate, store as blocksStore, parse, } from '@wordpress/blocks'; -import { useSelect } from '@wordpress/data'; +import { useSelect, useDispatch } from '@wordpress/data'; import { useCallback, useMemo } from '@wordpress/element'; +import { store as noticesStore } from '@wordpress/notices'; +import { __, sprintf } from '@wordpress/i18n'; /** * Internal dependencies */ import { store as blockEditorStore } from '../../../store'; -import { withRootClientIdOptionKey } from '../../../store/utils'; +import { isFiltered } from '../../../store/utils'; +import { unlock } from '../../../lock-unlock'; /** * Retrieves the block types inserter state. @@ -26,7 +30,7 @@ import { withRootClientIdOptionKey } from '../../../store/utils'; */ const useBlockTypesState = ( rootClientId, onInsert, isQuick ) => { const options = useMemo( - () => ( { [ withRootClientIdOptionKey ]: ! isQuick } ), + () => ( { [ isFiltered ]: !! isQuick } ), [ isQuick ] ); const [ items ] = useSelect( @@ -38,6 +42,10 @@ const useBlockTypesState = ( rootClientId, onInsert, isQuick ) => { ], [ rootClientId, options ] ); + const { getClosestAllowedInsertionPoint } = unlock( + useSelect( blockEditorStore ) + ); + const { createErrorNotice } = useDispatch( noticesStore ); const [ categories, collections ] = useSelect( ( select ) => { const { getCategories, getCollections } = select( blocksStore ); @@ -46,16 +54,29 @@ const useBlockTypesState = ( rootClientId, onInsert, isQuick ) => { const onSelectItem = useCallback( ( - { - name, - initialAttributes, - innerBlocks, - syncStatus, - content, - rootClientId: _rootClientId, - }, + { name, initialAttributes, innerBlocks, syncStatus, content }, shouldFocusBlock ) => { + const destinationClientId = getClosestAllowedInsertionPoint( + name, + rootClientId + ); + if ( destinationClientId === null ) { + const title = getBlockType( name )?.title ?? name; + createErrorNotice( + sprintf( + /* translators: %s: block pattern title. */ + __( 'Block "%s" can\'t be inserted.' ), + title + ), + { + type: 'snackbar', + id: 'inserter-notice', + } + ); + return; + } + const insertedBlock = syncStatus === 'unsynced' ? parse( content, { @@ -66,15 +87,14 @@ const useBlockTypesState = ( rootClientId, onInsert, isQuick ) => { initialAttributes, createBlocksFromInnerBlocksTemplate( innerBlocks ) ); - onInsert( insertedBlock, undefined, shouldFocusBlock, - _rootClientId + destinationClientId ); }, - [ onInsert ] + [ onInsert, getClosestAllowedInsertionPoint, rootClientId ] ); return [ items, categories, collections, onSelectItem ]; diff --git a/packages/block-editor/src/components/inserter/hooks/use-insertion-point.js b/packages/block-editor/src/components/inserter/hooks/use-insertion-point.js index 24074ec500456..0cd71bf77b983 100644 --- a/packages/block-editor/src/components/inserter/hooks/use-insertion-point.js +++ b/packages/block-editor/src/components/inserter/hooks/use-insertion-point.js @@ -71,7 +71,11 @@ function useInsertionPoint( { selectBlockOnInsert = true, } ) { const registry = useRegistry(); - const { getSelectedBlock } = useSelect( blockEditorStore ); + const { + getSelectedBlock, + getClosestAllowedInsertionPoint, + isBlockInsertionPointVisible, + } = unlock( useSelect( blockEditorStore ) ); const { destinationRootClientId, destinationIndex } = useSelect( ( select ) => { const { @@ -193,21 +197,30 @@ function useInsertionPoint( { const onToggleInsertionPoint = useCallback( ( item ) => { - if ( item?.hasOwnProperty( 'rootClientId' ) ) { - showInsertionPoint( - item.rootClientId, - getIndex( { - destinationRootClientId, - destinationIndex, - rootClientId: item.rootClientId, - registry, - } ) - ); + if ( item && ! isBlockInsertionPointVisible() ) { + const allowedDestinationRootClientId = + getClosestAllowedInsertionPoint( + item.name, + destinationRootClientId + ); + if ( allowedDestinationRootClientId !== null ) { + showInsertionPoint( + allowedDestinationRootClientId, + getIndex( { + destinationRootClientId, + destinationIndex, + rootClientId: allowedDestinationRootClientId, + registry, + } ) + ); + } } else { hideInsertionPoint(); } }, [ + getClosestAllowedInsertionPoint, + isBlockInsertionPointVisible, showInsertionPoint, hideInsertionPoint, destinationRootClientId, 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 6483dc58ae8b9..f8b083d4eedf1 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 @@ -87,7 +87,7 @@ const usePatternsState = ( onInsert, rootClientId, selectedCategory ) => { ), { type: 'snackbar', - id: 'block-pattern-inserted-notice', + id: 'inserter-notice', } ); }, diff --git a/packages/block-editor/src/components/inserter/media-tab/media-preview.js b/packages/block-editor/src/components/inserter/media-tab/media-preview.js index 64088f45fa1c3..a890e5fe8dc13 100644 --- a/packages/block-editor/src/components/inserter/media-tab/media-preview.js +++ b/packages/block-editor/src/components/inserter/media-tab/media-preview.js @@ -184,13 +184,16 @@ export function MediaPreview( { media, onClick, category } ) { } ); createSuccessNotice( __( 'Image uploaded and inserted.' ), - { type: 'snackbar' } + { type: 'snackbar', id: 'inserter-notice' } ); setIsInserting( false ); }, allowedTypes: ALLOWED_MEDIA_TYPES, onError( message ) { - createErrorNotice( message, { type: 'snackbar' } ); + createErrorNotice( message, { + type: 'snackbar', + id: 'inserter-notice', + } ); setIsInserting( false ); }, } ); @@ -281,6 +284,7 @@ export function MediaPreview( { media, onClick, category } ) { onClick( cloneBlock( block ) ); createSuccessNotice( __( 'Image inserted.' ), { type: 'snackbar', + id: 'inserter-notice', } ); setShowExternalUploadModal( false ); } } diff --git a/packages/block-editor/src/components/inserter/test/block-types-tab.native.js b/packages/block-editor/src/components/inserter/test/block-types-tab.native.js deleted file mode 100644 index 925570130359a..0000000000000 --- a/packages/block-editor/src/components/inserter/test/block-types-tab.native.js +++ /dev/null @@ -1,67 +0,0 @@ -/** - * External dependencies - */ -import { render } from 'test/helpers'; - -/** - * WordPress dependencies - */ -import { useSelect } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import items from './fixtures'; -import BlockTypesTab from '../block-types-tab'; - -jest.mock( '../hooks/use-clipboard-block' ); -jest.mock( '@wordpress/data/src/components/use-select' ); - -const selectMock = { - getCategories: jest.fn().mockReturnValue( [] ), - getCollections: jest.fn().mockReturnValue( [] ), - getInserterItems: jest.fn().mockReturnValue( [] ), - canInsertBlockType: jest.fn(), - getBlockType: jest.fn(), - getClipboard: jest.fn(), - getSettings: jest.fn( () => ( { impressions: {} } ) ), -}; - -describe( 'BlockTypesTab component', () => { - beforeEach( () => { - useSelect.mockImplementation( ( callback ) => - callback( () => selectMock ) - ); - } ); - - it( 'renders without crashing', () => { - const component = render( - - ); - expect( component ).toBeTruthy(); - } ); - - it( 'shows block items', () => { - selectMock.getInserterItems.mockReturnValue( items ); - - const blockItems = items.filter( - ( { id, category } ) => - category !== 'reusable' && id !== 'core-embed/a-paragraph-embed' - ); - const component = render( - - ); - - blockItems.forEach( ( item ) => { - expect( component.getByText( item.title ) ).toBeTruthy(); - } ); - } ); -} ); diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js index 9e99176819ae8..02a37b94ec27f 100644 --- a/packages/block-editor/src/store/private-selectors.js +++ b/packages/block-editor/src/store/private-selectors.js @@ -16,6 +16,7 @@ import { getTemplateLock, getClientIdsWithDescendants, isNavigationMode, + getBlockRootClientId, } from './selectors'; import { checkAllowListRecursive, @@ -637,3 +638,40 @@ export function getZoomLevel( state ) { export function isZoomOut( state ) { return getZoomLevel( state ) < 100; } + +/** + * Finds the closest block where the block is allowed to be inserted. + * + * @param {Object} state Editor state. + * @param {string} name Block name. + * @param {string} clientId Default insertion point. + * + * @return {string} clientID of the closest container when the block name can be inserted. + */ +export function getClosestAllowedInsertionPoint( state, name, clientId = '' ) { + // If we're trying to insert at the root level and it's not allowed + // Try the section root instead. + if ( ! clientId ) { + if ( canInsertBlockType( state, name, clientId ) ) { + return clientId; + } + + const sectionRootClientId = getSectionRootClientId( state ); + if ( + sectionRootClientId && + canInsertBlockType( state, name, sectionRootClientId ) + ) { + return sectionRootClientId; + } + return null; + } + + // Traverse the block tree up until we find a place where we can insert. + let current = clientId; + while ( current !== null && ! canInsertBlockType( state, name, current ) ) { + const parentClientId = getBlockRootClientId( state, current ); + current = parentClientId; + } + + return current; +} diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 20d6627398886..3163bb5257a9a 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -21,7 +21,7 @@ import { createSelector, createRegistrySelector } from '@wordpress/data'; * Internal dependencies */ import { - withRootClientIdOptionKey, + isFiltered, checkAllowListRecursive, checkAllowList, getAllPatternsDependants, @@ -80,7 +80,9 @@ const EMPTY_ARRAY = []; */ const EMPTY_SET = new Set(); -const EMPTY_OBJECT = {}; +const DEFAULT_INSERTER_OPTIONS = { + [ isFiltered ]: true, +}; /** * Returns a block's name given its client ID, or null if no block exists with @@ -2008,7 +2010,7 @@ const buildBlockTypeItem = */ export const getInserterItems = createRegistrySelector( ( select ) => createSelector( - ( state, rootClientId = null, options = EMPTY_OBJECT ) => { + ( state, rootClientId = null, options = DEFAULT_INSERTER_OPTIONS ) => { const buildReusableBlockInserterItem = ( reusableBlock ) => { const icon = ! reusableBlock.wp_pattern_sync_status ? { @@ -2056,56 +2058,7 @@ export const getInserterItems = createRegistrySelector( ( select ) => ) .map( buildBlockTypeInserterItem ); - if ( options[ withRootClientIdOptionKey ] ) { - blockTypeInserterItems = blockTypeInserterItems.reduce( - ( accumulator, item ) => { - item.rootClientId = rootClientId ?? ''; - - while ( - ! canInsertBlockTypeUnmemoized( - state, - item.name, - item.rootClientId - ) - ) { - if ( ! item.rootClientId ) { - let sectionRootClientId; - try { - sectionRootClientId = - getSectionRootClientId( state ); - } catch ( e ) {} - if ( - sectionRootClientId && - canInsertBlockTypeUnmemoized( - state, - item.name, - sectionRootClientId - ) - ) { - item.rootClientId = sectionRootClientId; - } else { - delete item.rootClientId; - } - break; - } else { - const parentClientId = getBlockRootClientId( - state, - item.rootClientId - ); - item.rootClientId = parentClientId; - } - } - - // We could also add non insertable items and gray them out. - if ( item.hasOwnProperty( 'rootClientId' ) ) { - accumulator.push( item ); - } - - return accumulator; - }, - [] - ); - } else { + if ( options[ isFiltered ] !== false ) { blockTypeInserterItems = blockTypeInserterItems.filter( ( blockType ) => canIncludeBlockTypeInInserter( @@ -2114,6 +2067,17 @@ export const getInserterItems = createRegistrySelector( ( select ) => rootClientId ) ); + } else { + blockTypeInserterItems = blockTypeInserterItems.map( + ( blockType ) => ( { + ...blockType, + isAllowedInCurrentRoot: canIncludeBlockTypeInInserter( + state, + blockType, + rootClientId + ), + } ) + ); } const items = blockTypeInserterItems.reduce( diff --git a/packages/block-editor/src/store/utils.js b/packages/block-editor/src/store/utils.js index 79e15255e6cc1..9b83a8f74cf9a 100644 --- a/packages/block-editor/src/store/utils.js +++ b/packages/block-editor/src/store/utils.js @@ -12,8 +12,7 @@ import { unlock } from '../lock-unlock'; import { STORE_NAME } from './constants'; import { getSectionRootClientId } from './private-selectors'; -export const withRootClientIdOptionKey = Symbol( 'withRootClientId' ); - +export const isFiltered = Symbol( 'isFiltered' ); const parsedPatternCache = new WeakMap(); const grammarMapCache = new WeakMap();