-
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
59f2ad8
commit 47449be
Showing
3 changed files
with
104 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,67 @@ | ||
/** | ||
* 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( { rootUID } ) { | ||
return select( 'core/editor' ).getInserterItems( rootUID ); | ||
} | ||
|
||
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