-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fix blocks completer to provide supported blocks
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
1 parent
2c32794
commit 533b299
Showing
3 changed files
with
111 additions
and
115 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' ); | ||
} ); | ||
} ); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters