From e1a6a78e60de39262cda7d96f0e905bb4f53679e Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Thu, 18 Jun 2020 15:30:44 +1000 Subject: [PATCH] Inserter: Fix handling of child blocks (#23231) * Inserter: Fix handling of child blocks When a block C specifies `parent: [ P ]`, it means that C may only be added to P. It does NOT mean that P may *only* contain C. (Which is what `` means.) This fixes the Inserter so that the correct blocks are shown when inserting into a block that is referenced by `parent`. It does so by leaning on `getInserterItems()` which does the right thing. * Inserter: Make ChildBlocks have a similar API to InserterPanel * E2E Tests: Add Child Blocks tests and test plugin * Inserter: Clarify inline comment --- .../src/components/inserter/block-list.js | 40 ++++------ .../src/components/inserter/child-blocks.js | 33 ++++---- .../components/inserter/test/block-list.js | 35 ++++++-- packages/e2e-tests/plugins/child-blocks.php | 28 +++++++ .../e2e-tests/plugins/child-blocks/index.js | 79 +++++++++++++++++++ .../specs/editor/plugins/child-blocks.test.js | 65 +++++++++++++++ 6 files changed, 231 insertions(+), 49 deletions(-) create mode 100644 packages/e2e-tests/plugins/child-blocks.php create mode 100644 packages/e2e-tests/plugins/child-blocks/index.js create mode 100644 packages/e2e-tests/specs/editor/plugins/child-blocks.test.js diff --git a/packages/block-editor/src/components/inserter/block-list.js b/packages/block-editor/src/components/inserter/block-list.js index f6bd172add888..5477c4a42d8e8 100644 --- a/packages/block-editor/src/components/inserter/block-list.js +++ b/packages/block-editor/src/components/inserter/block-list.js @@ -1,15 +1,7 @@ /** * External dependencies */ -import { - map, - includes, - findIndex, - flow, - sortBy, - groupBy, - isEmpty, -} from 'lodash'; +import { map, findIndex, flow, sortBy, groupBy, isEmpty } from 'lodash'; /** * WordPress dependencies @@ -48,13 +40,14 @@ export function InserterBlockList( { rootClientId, onInsert ); - const rootChildBlocks = useSelect( + + const hasChildItems = useSelect( ( select ) => { const { getBlockName } = select( 'core/block-editor' ); const { getChildBlockNames } = select( 'core/blocks' ); const rootBlockName = getBlockName( rootClientId ); - return getChildBlockNames( rootBlockName ); + return !! getChildBlockNames( rootBlockName ).length; }, [ rootClientId ] ); @@ -63,12 +56,6 @@ export function InserterBlockList( { return searchBlockItems( items, categories, collections, filterValue ); }, [ filterValue, items, categories, collections ] ); - const childItems = useMemo( () => { - return filteredItems.filter( ( { name } ) => - includes( rootChildBlocks, name ) - ); - }, [ filteredItems, rootChildBlocks ] ); - const suggestedItems = useMemo( () => { return items.slice( 0, MAX_SUGGESTED_ITEMS ); }, [ items ] ); @@ -127,16 +114,21 @@ export function InserterBlockList( { }, [ filterValue, debouncedSpeak ] ); const hasItems = ! isEmpty( filteredItems ); - const hasChildItems = childItems.length > 0; return (
- + { hasChildItems && ( + + + + ) } { ! hasChildItems && !! suggestedItems.length && ! filterValue && ( diff --git a/packages/block-editor/src/components/inserter/child-blocks.js b/packages/block-editor/src/components/inserter/child-blocks.js index 362a56b79035a..36e61006d4c60 100644 --- a/packages/block-editor/src/components/inserter/child-blocks.js +++ b/packages/block-editor/src/components/inserter/child-blocks.js @@ -1,16 +1,25 @@ /** * WordPress dependencies */ -import { withSelect } from '@wordpress/data'; -import { ifCondition, compose } from '@wordpress/compose'; +import { useSelect } from '@wordpress/data'; /** * Internal dependencies */ -import BlockTypesList from '../block-types-list'; import BlockIcon from '../block-icon'; -function ChildBlocks( { rootBlockIcon, rootBlockTitle, items, ...props } ) { +export default function ChildBlocks( { rootClientId, children } ) { + const { rootBlockTitle, rootBlockIcon } = useSelect( ( select ) => { + const { getBlockType } = select( 'core/blocks' ); + const { getBlockName } = select( 'core/block-editor' ); + const rootBlockName = getBlockName( rootClientId ); + const rootBlockType = getBlockType( rootBlockName ); + return { + rootBlockTitle: rootBlockType && rootBlockType.title, + rootBlockIcon: rootBlockType && rootBlockType.icon, + }; + } ); + return (
{ ( rootBlockIcon || rootBlockTitle ) && ( @@ -19,21 +28,7 @@ function ChildBlocks( { rootBlockIcon, rootBlockTitle, items, ...props } ) { { rootBlockTitle &&

{ rootBlockTitle }

}
) } - + { children }
); } - -export default compose( - ifCondition( ( { items } ) => items && items.length > 0 ), - withSelect( ( select, { rootClientId } ) => { - const { getBlockType } = select( 'core/blocks' ); - const { getBlockName } = select( 'core/block-editor' ); - const rootBlockName = getBlockName( rootClientId ); - const rootBlockType = getBlockType( rootBlockName ); - return { - rootBlockTitle: rootBlockType && rootBlockType.title, - rootBlockIcon: rootBlockType && rootBlockType.icon, - }; - } ) -)( ChildBlocks ); diff --git a/packages/block-editor/src/components/inserter/test/block-list.js b/packages/block-editor/src/components/inserter/test/block-list.js index 2e3bf78b3ffb5..2e69345d7b7f8 100644 --- a/packages/block-editor/src/components/inserter/test/block-list.js +++ b/packages/block-editor/src/components/inserter/test/block-list.js @@ -13,6 +13,13 @@ import { useSelect } from '@wordpress/data'; */ import { InserterBlockList as BaseInserterBlockList } from '../block-list'; import items, { categories, collections } from './fixtures'; +import useBlockTypesState from '../hooks/use-block-types-state'; + +jest.mock( '../hooks/use-block-types-state', () => { + // This allows us to tweak the returned value on each test + const mock = jest.fn(); + return mock; +} ); jest.mock( '@wordpress/data/src/components/use-select', () => { // This allows us to tweak the returned value on each test @@ -63,20 +70,22 @@ describe( 'InserterMenu', () => { beforeEach( () => { debouncedSpeak.mockClear(); - useSelect.mockImplementation( () => ( { + useBlockTypesState.mockImplementation( () => [ + items, categories, collections, - items, - } ) ); + ] ); + + useSelect.mockImplementation( () => false ); } ); it( 'should show nothing if there are no items', () => { const noItems = []; - useSelect.mockImplementation( () => ( { + useBlockTypesState.mockImplementation( () => [ + noItems, categories, collections, - items: noItems, - } ) ); + ] ); const { container } = render( ); @@ -149,6 +158,20 @@ describe( 'InserterMenu', () => { assertNoResultsMessageNotToBePresent( container ); } ); + it( 'displays child blocks UI when root block has child blocks', () => { + useSelect.mockImplementation( () => true ); + + const { container } = render( ); + + const childBlocksContent = container.querySelector( + '.block-editor-inserter__child-blocks' + ); + + expect( childBlocksContent ).not.toBeNull(); + + assertNoResultsMessageNotToBePresent( container ); + } ); + it( 'should disable items with `isDisabled`', () => { const { container } = initializeAllClosedMenuState(); const layoutTabContent = container.querySelectorAll( diff --git a/packages/e2e-tests/plugins/child-blocks.php b/packages/e2e-tests/plugins/child-blocks.php new file mode 100644 index 0000000000000..d13a2b8f10f61 --- /dev/null +++ b/packages/e2e-tests/plugins/child-blocks.php @@ -0,0 +1,28 @@ + { + beforeAll( async () => { + await activatePlugin( 'gutenberg-test-child-blocks' ); + } ); + + beforeEach( async () => { + await createNewPost(); + } ); + + afterAll( async () => { + await deactivatePlugin( 'gutenberg-test-child-blocks' ); + } ); + + it( 'are hidden from the global block inserter', async () => { + await openGlobalBlockInserter(); + await expect( await getAllBlockInserterItemTitles() ).not.toContain( + 'Child Blocks Child' + ); + } ); + + it( 'shows up in a parent block', async () => { + await insertBlock( 'Child Blocks Unrestricted Parent' ); + await closeGlobalBlockInserter(); + await page.waitForSelector( + '[data-type="test/child-blocks-unrestricted-parent"] .block-editor-default-block-appender' + ); + await page.click( + '[data-type="test/child-blocks-unrestricted-parent"] .block-editor-default-block-appender' + ); + await openGlobalBlockInserter(); + const inserterItemTitles = await getAllBlockInserterItemTitles(); + expect( inserterItemTitles ).toContain( 'Child Blocks Child' ); + expect( inserterItemTitles.length ).toBeGreaterThan( 20 ); + } ); + + it( 'display in a parent block with allowedItems', async () => { + await insertBlock( 'Child Blocks Restricted Parent' ); + await closeGlobalBlockInserter(); + await page.waitForSelector( + '[data-type="test/child-blocks-restricted-parent"] .block-editor-default-block-appender' + ); + await page.click( + '[data-type="test/child-blocks-restricted-parent"] .block-editor-default-block-appender' + ); + await openGlobalBlockInserter(); + expect( await getAllBlockInserterItemTitles() ).toEqual( [ + 'Child Blocks Child', + 'Image', + 'Paragraph', + ] ); + } ); +} );