Skip to content

Commit

Permalink
Fix blocks completer to provide supported blocks
Browse files Browse the repository at this point in the history
This updates the blocks completer to use the editor data store so only
supported blocks will be offered as completion options. In addition to
respecting the supported blocks list, shared blocks are now included as
completion options.
  • Loading branch information
brandonpayton committed May 18, 2018
1 parent 2c32794 commit 533b299
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 115 deletions.
99 changes: 57 additions & 42 deletions editor/components/autocompleters/block.js
Original file line number Diff line number Diff line change
@@ -1,59 +1,74 @@
/**
* 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
*/
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 [
<BlockIcon key="icon" icon={ icon } />,
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 [
<BlockIcon key="icon" icon={ icon } />,
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();
118 changes: 45 additions & 73 deletions editor/components/autocompleters/test/block.js
Original file line number Diff line number Diff line change
@@ -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( <div>
{ blockCompleter.getOptionLabel( {
icon: 'expected-icon',
title: 'expected-text',
} ) }
</div> ).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' );
} );
} );
9 changes: 9 additions & 0 deletions editor/hooks/default-autocompleters.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand Down

0 comments on commit 533b299

Please sign in to comment.