diff --git a/editor/components/autocompleters/block.js b/editor/components/autocompleters/block.js index 65dc6b6ccf7c22..e87d217f34fd5a 100644 --- a/editor/components/autocompleters/block.js +++ b/editor/components/autocompleters/block.js @@ -1,12 +1,8 @@ -/** - * External dependencies - */ -import { filter, sortBy, once, flow } from 'lodash'; - /** * WordPress dependencies */ -import { createBlock, getBlockTypes, hasBlockSupport } from '@wordpress/blocks'; +import { select } from '@wordpress/data'; +import { createBlock } from '@wordpress/blocks'; /** * Internal dependencies @@ -14,46 +10,65 @@ import { createBlock, getBlockTypes, hasBlockSupport } from '@wordpress/blocks'; import './style.scss'; import BlockIcon from '../block-icon'; -function filterBlockTypes( blockTypes ) { - // Exclude blocks that don't support being shown in the inserter - return filter( blockTypes, ( blockType ) => hasBlockSupport( blockType, 'inserter', true ) ); +function defaultGetBlockInsertionPoint() { + return select( 'core/editor' ).getBlockInsertionPoint(); +} + +function defaultGetInserterItems( parentUID ) { + // TODO: Update call to getInserterItems when the child block support PR is merged and that function simplified. + const { + getEditorSettings, + getSupportedBlocks, + getInserterItems, + } = select( 'core/editor' ); + const supportedBlocks = getSupportedBlocks( parentUID, getEditorSettings().allowedBlockTypes ); + return getInserterItems( supportedBlocks ); } -function sortBlockTypes( blockTypes ) { - // Prioritize blocks in the common common category - return sortBy( blockTypes, ( { category } ) => 'common' !== category ); +/** + * Creates a blocks repeater for replacing the current block with a selected block type. + * + * @return {Completer} A blocks completer. + */ +export function createBlockCompleter( { + // Allow store-based selectors to be overridden for unit test. + getBlockInsertionPoint = defaultGetBlockInsertionPoint, + getInserterItems = defaultGetInserterItems, +} = {} ) { + return { + name: 'blocks', + className: 'editor-autocompleters__block', + triggerPrefix: '/', + options() { + return getInserterItems( getBlockInsertionPoint() ); + }, + getOptionKeywords( inserterItem ) { + const { title, keywords = [] } = inserterItem; + return [ ...keywords, title ]; + }, + getOptionLabel( inserterItem ) { + const { icon, title } = inserterItem; + return [ + , + title, + ]; + }, + allowContext( before, after ) { + return ! ( /\S/.test( before.toString() ) || /\S/.test( after.toString() ) ); + }, + getOptionCompletion( inserterItem ) { + const { name, initialAttributes } = inserterItem; + return { + action: 'replace', + value: createBlock( name, initialAttributes ), + }; + }, + }; } /** - * A blocks repeater for replacing the current block with a selected block type. + * Creates a blocks repeater for replacing the current block with a selected block type. * - * @type {Completer} + * @return {Completer} A blocks completer. */ -export default { - name: 'blocks', - className: 'editor-autocompleters__block', - triggerPrefix: '/', - options: once( function options() { - return Promise.resolve( flow( filterBlockTypes, sortBlockTypes )( getBlockTypes() ) ); - } ), - getOptionKeywords( blockSettings ) { - const { title, keywords = [] } = blockSettings; - return [ ...keywords, title ]; - }, - getOptionLabel( blockSettings ) { - const { icon, title } = blockSettings; - return [ - , - title, - ]; - }, - allowContext( before, after ) { - return ! ( /\S/.test( before.toString() ) || /\S/.test( after.toString() ) ); - }, - getOptionCompletion( blockData ) { - return { - action: 'replace', - value: createBlock( blockData.name ), - }; - }, -}; +export default createBlockCompleter(); diff --git a/editor/components/autocompleters/test/block.js b/editor/components/autocompleters/test/block.js index 9453c066edfad0..2a39110101d3e7 100644 --- a/editor/components/autocompleters/test/block.js +++ b/editor/components/autocompleters/test/block.js @@ -1,95 +1,67 @@ /** * External dependencies */ -import { noop } from 'lodash'; - -/** - * WordPress dependencies - */ -import { registerBlockType, unregisterBlockType, getBlockTypes } from '@wordpress/blocks'; +import { shallow } from 'enzyme'; /** * Internal dependencies */ -import { blockAutocompleter } from '../'; +import blockCompleter, { createBlockCompleter } from '../block'; describe( 'block', () => { - const blockTypes = { - 'core/foo': { - save: noop, - category: 'common', + it( 'should retrieve block options for current insertion point', () => { + const expectedOptions = [ {}, {}, {} ]; + const mockGetBlockInsertionPoint = jest.fn( () => 'expected-insertion-point' ); + const mockGetInserterItems = jest.fn( () => expectedOptions ); + + const completer = createBlockCompleter( { + getBlockInsertionPoint: mockGetBlockInsertionPoint, + getInserterItems: mockGetInserterItems, + } ); + + const actualOptions = completer.options(); + expect( mockGetBlockInsertionPoint ).toHaveBeenCalled(); + expect( mockGetInserterItems ).toHaveBeenCalledWith( 'expected-insertion-point' ); + expect( actualOptions ).toBe( expectedOptions ); + } ); + + it( 'should derive option keywords from block keywords and block title', () => { + const inserterItemWithTitleAndKeywords = { + name: 'core/foo', title: 'foo', keywords: [ 'foo-keyword-1', 'foo-keyword-2' ], - }, - 'core/bar': { - save: noop, - category: 'layout', + }; + const inserterItemWithTitleAndEmptyKeywords = { + name: 'core/bar', title: 'bar', // Intentionally empty keyword list keywords: [], - }, - 'core/baz': { - save: noop, - category: 'common', + }; + const inserterItemWithTitleAndUndefinedKeywords = { + name: 'core/baz', title: 'baz', // Intentionally omitted keyword list - }, - }; + }; - beforeEach( () => { - Object.entries( blockTypes ).forEach( - ( [ name, settings ] ) => registerBlockType( name, settings ) - ); + expect( blockCompleter.getOptionKeywords( inserterItemWithTitleAndKeywords ) ) + .toEqual( [ 'foo-keyword-1', 'foo-keyword-2', 'foo' ] ); + expect( blockCompleter.getOptionKeywords( inserterItemWithTitleAndEmptyKeywords ) ) + .toEqual( [ 'bar' ] ); + expect( blockCompleter.getOptionKeywords( inserterItemWithTitleAndUndefinedKeywords ) ) + .toEqual( [ 'baz' ] ); } ); - afterEach( () => { - getBlockTypes().forEach( ( block ) => { - unregisterBlockType( block.name ); - } ); - } ); - - it( 'should prioritize common blocks in options', () => { - return blockAutocompleter.options().then( ( options ) => { - expect( options ).toMatchObject( [ - blockTypes[ 'core/foo' ], - blockTypes[ 'core/baz' ], - blockTypes[ 'core/bar' ], - ] ); - } ); - } ); - - it( 'should render a block option label composed of @wordpress/element Elements and/or strings', () => { - expect.hasAssertions(); - - // Only verify that a populated label is returned. - // It is likely to be fragile to assert that the contents are renderable by @wordpress/element. - const isAllowedLabelType = ( label ) => Array.isArray( label ) || ( typeof label === 'string' ); - - getBlockTypes().forEach( ( blockType ) => { - const label = blockAutocompleter.getOptionLabel( blockType ); - expect( isAllowedLabelType( label ) ).toBeTruthy(); - } ); - } ); - - it( 'should derive option keywords from block keywords and block title', () => { - const optionKeywords = getBlockTypes().reduce( - ( map, blockType ) => map.set( - blockType.name, - blockAutocompleter.getOptionKeywords( blockType ) - ), - new Map() - ); + it( 'should render a block option label', () => { + const labelComponents = shallow(
+ { blockCompleter.getOptionLabel( { + icon: 'expected-icon', + title: 'expected-text', + } ) } +
).children(); - expect( optionKeywords.get( 'core/foo' ) ).toEqual( [ - 'foo-keyword-1', - 'foo-keyword-2', - blockTypes[ 'core/foo' ].title, - ] ); - expect( optionKeywords.get( 'core/bar' ) ).toEqual( [ - blockTypes[ 'core/bar' ].title, - ] ); - expect( optionKeywords.get( 'core/baz' ) ).toEqual( [ - blockTypes[ 'core/baz' ].title, - ] ); + expect( labelComponents ).toHaveLength( 2 ); + expect( labelComponents.at( 0 ).name() ).toBe( 'BlockIcon' ); + expect( labelComponents.at( 0 ).prop( 'icon' ) ).toBe( 'expected-icon' ); + expect( labelComponents.at( 1 ).text() ).toBe( 'expected-text' ); } ); } ); diff --git a/editor/hooks/default-autocompleters.js b/editor/hooks/default-autocompleters.js index 8e132e417bfa80..21c30ab7efa2f6 100644 --- a/editor/hooks/default-autocompleters.js +++ b/editor/hooks/default-autocompleters.js @@ -8,6 +8,7 @@ import { clone } from 'lodash'; */ import { addFilter } from '@wordpress/hooks'; import { getDefaultBlockName } from '@wordpress/blocks'; +import { dispatch } from '@wordpress/data'; /** * Internal dependencies @@ -23,6 +24,14 @@ function setDefaultCompleters( completers, blockName ) { // Add blocks autocompleter for Paragraph block if ( blockName === getDefaultBlockName() ) { completers.push( clone( blockAutocompleter ) ); + + /* + * NOTE: This is a hack to help ensure shared blocks are loaded + * so they may be included in the block completer. It can be removed + * once we have a way for completers to Promise options while + * store-based data dependencies are being resolved. + */ + dispatch( 'core/editor' ).fetchSharedBlocks(); } } return completers;