diff --git a/editor/components/inserter/group.js b/editor/components/inserter/group.js
index 8126a55159f6b..ab9abf5013727 100644
--- a/editor/components/inserter/group.js
+++ b/editor/components/inserter/group.js
@@ -10,8 +10,8 @@ import { Component } from '@wordpress/element';
import { NavigableMenu } from '@wordpress/components';
import { BlockIcon } from '@wordpress/blocks';
-function deriveActiveBlocks( blocks ) {
- return blocks.filter( ( block ) => ! block.disabled );
+function deriveActiveItems( items ) {
+ return items.filter( ( item ) => ! item.isDisabled );
}
export default class InserterGroup extends Component {
@@ -20,61 +20,56 @@ export default class InserterGroup extends Component {
this.onNavigate = this.onNavigate.bind( this );
- this.activeBlocks = deriveActiveBlocks( this.props.blockTypes );
+ this.activeItems = deriveActiveItems( this.props.items );
this.state = {
- current: this.activeBlocks.length > 0 ? this.activeBlocks[ 0 ].name : null,
+ current: this.activeItems.length > 0 ? this.activeItems[ 0 ] : null,
};
}
componentWillReceiveProps( nextProps ) {
- if ( ! isEqual( this.props.blockTypes, nextProps.blockTypes ) ) {
- this.activeBlocks = deriveActiveBlocks( nextProps.blockTypes );
+ if ( ! isEqual( this.props.items, nextProps.items ) ) {
+ this.activeItems = deriveActiveItems( nextProps.items );
// Try and preserve any still valid selected state.
- const current = find( this.activeBlocks, { name: this.state.current } );
+ const current = find( this.activeItems, ( item ) => isEqual( item, this.state.current ) );
if ( ! current ) {
this.setState( {
- current: this.activeBlocks.length > 0 ? this.activeBlocks[ 0 ].name : null,
+ current: this.activeItems.length > 0 ? this.activeItems[ 0 ] : null,
} );
}
}
}
- renderItem( block ) {
+ renderItem( item, index ) {
const { current } = this.state;
- const { selectBlock, bindReferenceNode } = this.props;
- const { disabled } = block;
+ const { onSelectItem } = this.props;
return (
);
}
onNavigate( index ) {
- const { activeBlocks } = this;
- const dest = activeBlocks[ index ];
+ const { activeItems } = this;
+ const dest = activeItems[ index ];
if ( dest ) {
this.setState( {
- current: dest.name,
+ current: dest,
} );
}
}
render() {
- const { labelledBy, blockTypes } = this.props;
+ const { labelledBy, items } = this.props;
return (
- { blockTypes.map( this.renderItem, this ) }
+ { items.map( this.renderItem, this ) }
);
}
diff --git a/editor/components/inserter/index.js b/editor/components/inserter/index.js
index 51f385435f69b..e77ec98c034ea 100644
--- a/editor/components/inserter/index.js
+++ b/editor/components/inserter/index.js
@@ -82,17 +82,12 @@ class Inserter extends Component {
) }
renderContent={ ( { onClose } ) => {
- const onInsert = ( name, initialAttributes ) => {
- onInsertBlock(
- name,
- initialAttributes,
- insertionPoint
- );
-
+ const onSelect = ( item ) => {
+ onInsertBlock( item, insertionPoint );
onClose();
};
- return ;
+ return ;
} }
/>
);
@@ -108,9 +103,9 @@ export default compose( [
};
},
( dispatch ) => ( {
- onInsertBlock( name, initialAttributes, position ) {
+ onInsertBlock( item, position ) {
dispatch( insertBlock(
- createBlock( name, initialAttributes ),
+ createBlock( item.name, item.initialAttributes ),
position
) );
},
diff --git a/editor/components/inserter/menu.js b/editor/components/inserter/menu.js
index be59c24a1815f..600603c3f4956 100644
--- a/editor/components/inserter/menu.js
+++ b/editor/components/inserter/menu.js
@@ -3,7 +3,6 @@
*/
import {
filter,
- find,
findIndex,
flow,
groupBy,
@@ -27,7 +26,7 @@ import {
withSpokenMessages,
withContext,
} from '@wordpress/components';
-import { getCategories, getBlockTypes } from '@wordpress/blocks';
+import { getCategories } from '@wordpress/blocks';
import { keycodes } from '@wordpress/utils';
/**
@@ -35,16 +34,16 @@ import { keycodes } from '@wordpress/utils';
*/
import './style.scss';
-import { getBlocks, getRecentlyUsedBlocks, getReusableBlocks } from '../../store/selectors';
+import { getInserterItems, getRecentInserterItems } from '../../store/selectors';
import { fetchReusableBlocks } from '../../store/actions';
import { default as InserterGroup } from './group';
-export const searchBlocks = ( blocks, searchTerm ) => {
+export const searchItems = ( items, searchTerm ) => {
const normalizedSearchTerm = searchTerm.toLowerCase().trim();
const matchSearch = ( string ) => string.toLowerCase().indexOf( normalizedSearchTerm ) !== -1;
- return blocks.filter( ( block ) =>
- matchSearch( block.title ) || some( block.keywords, matchSearch )
+ return items.filter( ( item ) =>
+ matchSearch( item.title ) || some( item.keywords, matchSearch )
);
};
@@ -62,11 +61,10 @@ export class InserterMenu extends Component {
tab: 'recent',
};
this.filter = this.filter.bind( this );
- this.searchBlocks = this.searchBlocks.bind( this );
- this.getBlocksForTab = this.getBlocksForTab.bind( this );
- this.sortBlocks = this.sortBlocks.bind( this );
- this.bindReferenceNode = this.bindReferenceNode.bind( this );
- this.selectBlock = this.selectBlock.bind( this );
+ this.searchItems = this.searchItems.bind( this );
+ this.getItemsForTab = this.getItemsForTab.bind( this );
+ this.sortItems = this.sortItems.bind( this );
+ this.selectItem = this.selectItem.bind( this );
this.tabScrollTop = { recent: 0, blocks: 0, embeds: 0 };
this.switchTab = this.switchTab.bind( this );
@@ -77,8 +75,8 @@ export class InserterMenu extends Component {
}
componentDidUpdate( prevProps, prevState ) {
- const searchResults = this.searchBlocks( this.getBlockTypes() );
- // Announce the blocks search results to screen readers.
+ const searchResults = this.searchItems( this.props.items );
+ // Announce the search results to screen readers.
if ( this.state.filterValue && !! searchResults.length ) {
this.props.debouncedSpeak( sprintf( _n(
'%d result found',
@@ -94,152 +92,91 @@ export class InserterMenu extends Component {
}
}
- isDisabledBlock( blockType ) {
- return blockType.useOnce && find( this.props.blocks, ( { name } ) => blockType.name === name );
- }
-
- bindReferenceNode( nodeName ) {
- return ( node ) => this.nodes[ nodeName ] = node;
- }
-
filter( event ) {
this.setState( {
filterValue: event.target.value,
} );
}
- selectBlock( block ) {
- return () => {
- this.props.onSelect( block.name, block.initialAttributes );
- this.setState( {
- filterValue: '',
- } );
- };
- }
-
- getStaticBlockTypes() {
- const { blockTypes } = this.props;
-
- // If all block types disabled, return empty set
- if ( ! blockTypes ) {
- return [];
- }
-
- // Block types that are marked as private should not appear in the inserter
- return getBlockTypes().filter( ( block ) => {
- if ( block.isPrivate ) {
- return false;
- }
-
- // Block types defined as either `true` or array:
- // - True: Allow
- // - Array: Check block name within whitelist
- return (
- ! Array.isArray( blockTypes ) ||
- includes( blockTypes, block.name )
- );
+ selectItem( item ) {
+ this.props.onSelect( item );
+ this.setState( {
+ filterValue: '',
} );
}
- getReusableBlockTypes() {
- const { reusableBlocks } = this.props;
-
- // Display reusable blocks that we've fetched in the inserter
- return reusableBlocks.map( ( reusableBlock ) => ( {
- name: 'core/block',
- initialAttributes: {
- ref: reusableBlock.id,
- },
- title: reusableBlock.title,
- icon: 'layout',
- category: 'reusable-blocks',
- } ) );
+ searchItems( items ) {
+ return searchItems( items, this.state.filterValue );
}
- getBlockTypes() {
- return [
- ...this.getStaticBlockTypes(),
- ...this.getReusableBlockTypes(),
- ];
- }
-
- searchBlocks( blockTypes ) {
- return searchBlocks( blockTypes, this.state.filterValue );
- }
+ getItemsForTab( tab ) {
+ const { items, recentItems } = this.props;
- getBlocksForTab( tab ) {
- const blockTypes = this.getBlockTypes();
- // if we're searching, use everything, otherwise just get the blocks visible in this tab
+ // If we're searching, use everything, otherwise just get the items visible in this tab
if ( this.state.filterValue ) {
- return blockTypes;
+ return items;
}
let predicate;
switch ( tab ) {
case 'recent':
- return filter( this.props.recentlyUsedBlocks,
- ( { name } ) => find( blockTypes, { name } ) );
+ return recentItems;
case 'blocks':
- predicate = ( block ) => block.category !== 'embed' && block.category !== 'reusable-blocks';
+ predicate = ( item ) => item.category !== 'embed' && item.category !== 'reusable-blocks';
break;
case 'embeds':
- predicate = ( block ) => block.category === 'embed';
+ predicate = ( item ) => item.category === 'embed';
break;
case 'saved':
- predicate = ( block ) => block.category === 'reusable-blocks';
+ predicate = ( item ) => item.category === 'reusable-blocks';
break;
}
- return filter( blockTypes, predicate );
+ return filter( items, predicate );
}
- sortBlocks( blockTypes ) {
+ sortItems( items ) {
if ( 'recent' === this.state.tab && ! this.state.filterValue ) {
- return blockTypes;
+ return items;
}
const getCategoryIndex = ( item ) => {
return findIndex( getCategories(), ( category ) => category.slug === item.category );
};
- return sortBy( blockTypes, getCategoryIndex );
+ return sortBy( items, getCategoryIndex );
}
- groupByCategory( blockTypes ) {
- return groupBy( blockTypes, ( blockType ) => blockType.category );
+ groupByCategory( items ) {
+ return groupBy( items, ( item ) => item.category );
}
- getVisibleBlocksByCategory( blockTypes ) {
+ getVisibleItemsByCategory( items ) {
return flow(
- this.searchBlocks,
- this.sortBlocks,
+ this.searchItems,
+ this.sortItems,
this.groupByCategory
- )( blockTypes );
+ )( items );
}
- renderBlocks( blockTypes, separatorSlug ) {
+ renderItems( items, separatorSlug ) {
const { instanceId } = this.props;
const labelledBy = separatorSlug === undefined ? null : `editor-inserter__separator-${ separatorSlug }-${ instanceId }`;
- const blockTypesInfo = blockTypes.map( ( blockType ) => (
- { ...blockType, disabled: this.isDisabledBlock( blockType ) }
- ) );
-
return (
);
}
- renderCategory( category, blockTypes ) {
+ renderCategory( category, items ) {
const { instanceId } = this.props;
- return blockTypes && (
+ return items && (
{ __( 'No blocks found' ) }
@@ -263,7 +200,7 @@ export class InserterMenu extends Component {
}
return getCategories().map(
- ( category ) => this.renderCategory( category, visibleBlocksByCategory[ category.slug ] )
+ ( category ) => this.renderCategory( category, visibleItemsByCategory[ category.slug ] )
);
}
@@ -274,15 +211,15 @@ export class InserterMenu extends Component {
}
renderTabView( tab ) {
- const blocksForTab = this.getBlocksForTab( tab );
+ const itemsForTab = this.getItemsForTab( tab );
// If the Recent tab is selected, don't render category headers
if ( 'recent' === tab ) {
- return this.renderBlocks( blocksForTab );
+ return this.renderItems( itemsForTab );
}
// If the Saved tab is selected and we have no results, display a friendly message
- if ( 'saved' === tab && blocksForTab.length === 0 ) {
+ if ( 'saved' === tab && itemsForTab.length === 0 ) {
return (
{ __( 'No saved blocks.' ) }
@@ -290,16 +227,16 @@ export class InserterMenu extends Component {
);
}
- const visibleBlocksByCategory = this.getVisibleBlocksByCategory( blocksForTab );
+ const visibleItemsByCategory = this.getVisibleItemsByCategory( itemsForTab );
- // If our results have only blocks from one category, don't render category headers
- const categories = Object.keys( visibleBlocksByCategory );
+ // If our results have only items from one category, don't render category headers
+ const categories = Object.keys( visibleItemsByCategory );
if ( categories.length === 1 ) {
const [ soleCategory ] = categories;
- return this.renderBlocks( visibleBlocksByCategory[ soleCategory ] );
+ return this.renderItems( visibleItemsByCategory[ soleCategory ] );
}
- return this.renderCategories( visibleBlocksByCategory );
+ return this.renderCategories( visibleItemsByCategory );
}
// Passed to TabbableContainer, extending its event-handling logic
@@ -324,7 +261,7 @@ export class InserterMenu extends Component {
}
render() {
- const { instanceId } = this.props;
+ const { instanceId, items } = this.props;
const isSearching = this.state.filterValue;
return (
@@ -340,7 +277,6 @@ export class InserterMenu extends Component {
placeholder={ __( 'Search for a block' ) }
className="editor-inserter__search"
onChange={ this.filter }
- ref={ this.bindReferenceNode( 'search' ) }
/>
{ ! isSearching &&
- { this.renderCategories( this.getVisibleBlocksByCategory( this.getBlockTypes() ) ) }
+ { this.renderCategories( this.getVisibleItemsByCategory( items ) ) }
}
@@ -385,20 +321,23 @@ export class InserterMenu extends Component {
}
}
-const connectComponent = connect(
- ( state ) => {
+export default compose(
+ withContext( 'editor' )( ( settings ) => {
+ const { blockTypes } = settings;
+
return {
- recentlyUsedBlocks: getRecentlyUsedBlocks( state ),
- blocks: getBlocks( state ),
- reusableBlocks: getReusableBlocks( state ),
+ enabledBlockTypes: blockTypes,
};
- },
- { fetchReusableBlocks }
-);
-
-export default compose(
- connectComponent,
- withContext( 'editor' )( ( settings ) => pick( settings, 'blockTypes' ) ),
+ } ),
+ connect(
+ ( state, ownProps ) => {
+ return {
+ items: getInserterItems( state, ownProps.enabledBlockTypes ),
+ recentItems: getRecentInserterItems( state, ownProps.enabledBlockTypes ),
+ };
+ },
+ { fetchReusableBlocks }
+ ),
withSpokenMessages,
withInstanceId
)( InserterMenu );
diff --git a/editor/components/inserter/test/menu.js b/editor/components/inserter/test/menu.js
index 60dd6e4e9bc2d..1cdffc42d4928 100644
--- a/editor/components/inserter/test/menu.js
+++ b/editor/components/inserter/test/menu.js
@@ -4,99 +4,90 @@
import { mount } from 'enzyme';
import { noop } from 'lodash';
-/**
- * WordPress dependencies
- */
-import { registerBlockType, unregisterBlockType, getBlockTypes } from '@wordpress/blocks';
-
/**
* Internal dependencies
*/
-import { InserterMenu, searchBlocks } from '../menu';
+import { InserterMenu, searchItems } from '../menu';
-const textBlock = {
+const textItem = {
name: 'core/text-block',
+ initialAttributes: {},
title: 'Text',
- save: noop,
- edit: noop,
category: 'common',
+ isDisabled: false,
};
-const advancedTextBlock = {
+const advancedTextItem = {
name: 'core/advanced-text-block',
+ initialAttributes: {},
title: 'Advanced Text',
- save: noop,
- edit: noop,
category: 'common',
+ isDisabled: false,
};
-const someOtherBlock = {
+const someOtherItem = {
name: 'core/some-other-block',
+ initialAttributes: {},
title: 'Some Other Block',
- save: noop,
- edit: noop,
category: 'common',
+ isDisabled: false,
};
-const moreBlock = {
+const moreItem = {
name: 'core/more-block',
+ initialAttributes: {},
title: 'More',
- save: noop,
- edit: noop,
category: 'layout',
- useOnce: 'true',
+ isDisabled: true,
};
-const youtubeBlock = {
+const youtubeItem = {
name: 'core-embed/youtube',
+ initialAttributes: {},
title: 'YouTube',
- save: noop,
- edit: noop,
category: 'embed',
keywords: [ 'google' ],
+ isDisabled: false,
};
-const textEmbedBlock = {
+const textEmbedItem = {
name: 'core-embed/a-text-embed',
+ initialAttributes: {},
title: 'A Text Embed',
- save: noop,
- edit: noop,
category: 'embed',
+ isDisabled: false,
};
+const reusableItem = {
+ name: 'core/block',
+ initialAttributes: { ref: 123 },
+ title: 'My reusable block',
+ category: 'reusable-blocks',
+ isDisabled: false,
+};
+
+const items = [
+ textItem,
+ advancedTextItem,
+ someOtherItem,
+ moreItem,
+ youtubeItem,
+ textEmbedItem,
+ reusableItem,
+];
+
describe( 'InserterMenu', () => {
// NOTE: Due to https://github.com/airbnb/enzyme/issues/1174, some of the selectors passed through to
// wrapper.find have had to be strengthened (and the filterWhere strengthened also), otherwise two
// results would be returned even though only one was in the DOM.
- const unregisterAllBlocks = () => {
- getBlockTypes().forEach( ( block ) => {
- unregisterBlockType( block.name );
- } );
- };
-
- afterEach( () => {
- unregisterAllBlocks();
- } );
-
- beforeEach( () => {
- unregisterAllBlocks();
- registerBlockType( textBlock.name, textBlock );
- registerBlockType( advancedTextBlock.name, advancedTextBlock );
- registerBlockType( someOtherBlock.name, someOtherBlock );
- registerBlockType( moreBlock.name, moreBlock );
- registerBlockType( youtubeBlock.name, youtubeBlock );
- registerBlockType( textEmbedBlock.name, textEmbedBlock );
- } );
-
it( 'should show the recent tab by default', () => {
const wrapper = mount(
{
expect( visibleBlocks ).toHaveLength( 0 );
} );
- it( 'should show no blocks if all block types disabled', () => {
+ it( 'should show nothing if there are no items', () => {
const wrapper = mount(
);
@@ -128,90 +117,81 @@ describe( 'InserterMenu', () => {
expect( visibleBlocks ).toHaveLength( 0 );
} );
- it( 'should show filtered block types', () => {
+ it( 'should show the recently used items in the recent tab', () => {
const wrapper = mount(
);
const visibleBlocks = wrapper.find( '.editor-inserter__block' );
- expect( visibleBlocks ).toHaveLength( 1 );
- expect( visibleBlocks.at( 0 ).text() ).toBe( 'Text' );
+ expect( visibleBlocks ).toHaveLength( 3 );
+ expect( visibleBlocks.at( 0 ).text() ).toBe( 'Advanced Text' );
+ expect( visibleBlocks.at( 1 ).text() ).toBe( 'Text' );
+ expect( visibleBlocks.at( 2 ).text() ).toBe( 'Some Other Block' );
} );
- it( 'should show the recently used blocks in the recent tab', () => {
+ it( 'should show items from the embed category in the embed tab', () => {
const wrapper = mount(
);
+ const embedTab = wrapper.find( '.editor-inserter__tab' )
+ .filterWhere( ( node ) => node.text() === 'Embeds' && node.name() === 'button' );
+ embedTab.simulate( 'click' );
+
+ const activeCategory = wrapper.find( '.editor-inserter__tab button.is-active' );
+ expect( activeCategory.text() ).toBe( 'Embeds' );
const visibleBlocks = wrapper.find( '.editor-inserter__block' );
- expect( visibleBlocks ).toHaveLength( 3 );
- expect( visibleBlocks.at( 0 ).childAt( 0 ).name() ).toBe( 'BlockIcon' );
- expect( visibleBlocks.at( 0 ).text() ).toBe( 'Advanced Text' );
+ expect( visibleBlocks ).toHaveLength( 2 );
+ expect( visibleBlocks.at( 0 ).text() ).toBe( 'YouTube' );
+ expect( visibleBlocks.at( 1 ).text() ).toBe( 'A Text Embed' );
} );
- it( 'should show blocks from the embed category in the embed tab', () => {
+ it( 'should show reusable items in the saved tab', () => {
const wrapper = mount(
);
const embedTab = wrapper.find( '.editor-inserter__tab' )
- .filterWhere( ( node ) => node.text() === 'Embeds' && node.name() === 'button' );
+ .filterWhere( ( node ) => node.text() === 'Saved' && node.name() === 'button' );
embedTab.simulate( 'click' );
const activeCategory = wrapper.find( '.editor-inserter__tab button.is-active' );
- expect( activeCategory.text() ).toBe( 'Embeds' );
+ expect( activeCategory.text() ).toBe( 'Saved' );
const visibleBlocks = wrapper.find( '.editor-inserter__block' );
- expect( visibleBlocks ).toHaveLength( 2 );
- expect( visibleBlocks.at( 0 ).text() ).toBe( 'YouTube' );
- expect( visibleBlocks.at( 1 ).text() ).toBe( 'A Text Embed' );
+ expect( visibleBlocks ).toHaveLength( 1 );
+ expect( visibleBlocks.at( 0 ).text() ).toBe( 'My reusable block' );
} );
- it( 'should show all blocks except embeds in the blocks tab', () => {
+ it( 'should show all items except embeds and reusable blocks in the blocks tab', () => {
const wrapper = mount(
);
const blocksTab = wrapper.find( '.editor-inserter__tab' )
@@ -229,40 +209,32 @@ describe( 'InserterMenu', () => {
expect( visibleBlocks.at( 3 ).text() ).toBe( 'More' );
} );
- it( 'should disable already used blocks with `usedOnce`', () => {
+ it( 'should disable items with `isDisabled`', () => {
const wrapper = mount(
);
- const blocksTab = wrapper.find( '.editor-inserter__tab' )
- .filterWhere( ( node ) => node.text() === 'Blocks' && node.name() === 'button' );
- blocksTab.simulate( 'click' );
- wrapper.update();
- const disabledBlocks = wrapper.find( '.editor-inserter__block[disabled]' );
+ const disabledBlocks = wrapper.find( '.editor-inserter__block[disabled=true]' );
expect( disabledBlocks ).toHaveLength( 1 );
expect( disabledBlocks.at( 0 ).text() ).toBe( 'More' );
} );
- it( 'should allow searching for blocks', () => {
+ it( 'should allow searching for items', () => {
const wrapper = mount(
);
wrapper.setState( { filterValue: 'text' } );
@@ -282,12 +254,10 @@ describe( 'InserterMenu', () => {
);
wrapper.setState( { filterValue: ' text' } );
@@ -303,18 +273,16 @@ describe( 'InserterMenu', () => {
} );
} );
-describe( 'searchBlocks', () => {
- it( 'should search blocks using the title ignoring case', () => {
- const blocks = [ textBlock, advancedTextBlock, moreBlock, youtubeBlock, textEmbedBlock ];
- expect( searchBlocks( blocks, 'TEXT' ) ).toEqual(
- [ textBlock, advancedTextBlock, textEmbedBlock ]
+describe( 'searchItems', () => {
+ it( 'should search items using the title ignoring case', () => {
+ expect( searchItems( items, 'TEXT' ) ).toEqual(
+ [ textItem, advancedTextItem, textEmbedItem ]
);
} );
- it( 'should search blocks using the keywords', () => {
- const blocks = [ textBlock, advancedTextBlock, moreBlock, youtubeBlock, textEmbedBlock ];
- expect( searchBlocks( blocks, 'GOOGL' ) ).toEqual(
- [ youtubeBlock ]
+ it( 'should search items using the keywords', () => {
+ expect( searchItems( items, 'GOOGL' ) ).toEqual(
+ [ youtubeItem ]
);
} );
} );
diff --git a/editor/store/selectors.js b/editor/store/selectors.js
index f05f794c07da3..4036afdc0068d 100644
--- a/editor/store/selectors.js
+++ b/editor/store/selectors.js
@@ -18,7 +18,7 @@ import createSelector from 'rememo';
/**
* WordPress dependencies
*/
-import { serialize, getBlockType } from '@wordpress/blocks';
+import { serialize, getBlockType, getBlockTypes } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import { addQueryArgs } from '@wordpress/url';
@@ -1111,15 +1111,129 @@ export function getNotices( state ) {
}
/**
- * Resolves the list of recently used block names into a list of block type settings.
- *
- * @param {Object} state Global application state
- *
- * @returns {Array} List of recently used blocks.
+ * An item that appears in the inserter. Inserting this item will create a new
+ * block. Inserter items encapsulate both regular blocks and reusable blocks.
+ *
+ * @typedef {Object} Editor.InserterItem
+ * @property {string} name The type of block to create.
+ * @property {Object} initialAttributes Attributes to pass to the newly created block.
+ * @property {string} title Title of the item, as it appears in the inserter.
+ * @property {string} icon Dashicon for the item, as it appears in the inserter.
+ * @property {string} category Block category that the item is associated with.
+ * @property {string[]} keywords Keywords that can be searched to find this item.
+ * @property {boolean} isDisabled Whether or not the user should be prevented from inserting this item.
+ */
+
+/**
+ * Given a regular block type, constructs an item that appears in the inserter.
+ *
+ * @param {Object} state Global application state.
+ * @param {string[]|boolean} enabledBlockTypes Enabled block types, or true/false to enable/disable all types.
+ * @param {Object} blockType Block type, likely from getBlockType().
+ * @returns {Editor.InserterItem} Item that appears in inserter.
+ */
+function buildInserterItemFromBlockType( state, enabledBlockTypes, blockType ) {
+ if ( ! enabledBlockTypes || ! blockType ) {
+ return null;
+ }
+
+ const blockTypeIsDisabled = Array.isArray( enabledBlockTypes ) && ! enabledBlockTypes.includes( blockType.name );
+ if ( blockTypeIsDisabled ) {
+ return null;
+ }
+
+ if ( blockType.isPrivate ) {
+ return null;
+ }
+
+ return {
+ name: blockType.name,
+ initialAttributes: {},
+ title: blockType.title,
+ icon: blockType.icon,
+ category: blockType.category,
+ keywords: blockType.keywords,
+ isDisabled: !! blockType.useOnce && getBlocks( state ).some( block => block.name === blockType.name ),
+ };
+}
+
+/**
+ * Given a reusable block, constructs an item that appears in the inserter.
+ *
+ * @param {string[]|boolean} enabledBlockTypes Enabled block types, or true/false to enable/disable all types.
+ * @param {Object} reusableBlock Reusable block, likely from getReusableBlock().
+ * @returns {Editor.InserterItem} Item that appears in inserter.
*/
-export function getRecentlyUsedBlocks( state ) {
- // resolves the block names in the state to the block type settings
- return compact( state.preferences.recentlyUsedBlocks.map( blockType => getBlockType( blockType ) ) );
+function buildInserterItemFromReusableBlock( enabledBlockTypes, reusableBlock ) {
+ if ( ! enabledBlockTypes || ! reusableBlock ) {
+ return null;
+ }
+
+ const blockTypeIsDisabled = Array.isArray( enabledBlockTypes ) && ! enabledBlockTypes.includes( 'core/block' );
+ if ( blockTypeIsDisabled ) {
+ return null;
+ }
+
+ const referencedBlockType = getBlockType( reusableBlock.type );
+ if ( ! referencedBlockType ) {
+ return null;
+ }
+
+ return {
+ name: 'core/block',
+ initialAttributes: { ref: reusableBlock.id },
+ title: reusableBlock.title,
+ icon: referencedBlockType.icon,
+ category: 'reusable-blocks',
+ keywords: [],
+ isDisabled: false,
+ };
+}
+
+/**
+ * Determines the items that appear in the the inserter. Includes both static
+ * items (e.g. a regular block type) and dynamic items (e.g. a reusable block).
+ *
+ * @param {Object} state Global application state.
+ * @param {string[]|boolean} enabledBlockTypes Enabled block types, or true/false to enable/disable all types.
+ * @returns {Editor.InserterItem[]} Items that appear in inserter.
+ */
+export function getInserterItems( state, enabledBlockTypes = true ) {
+ if ( ! enabledBlockTypes ) {
+ return [];
+ }
+
+ const staticItems = getBlockTypes().map( blockType =>
+ buildInserterItemFromBlockType( state, enabledBlockTypes, blockType )
+ );
+
+ const dynamicItems = getReusableBlocks( state ).map( reusableBlock =>
+ buildInserterItemFromReusableBlock( enabledBlockTypes, reusableBlock )
+ );
+
+ const items = [ ...staticItems, ...dynamicItems ];
+ return compact( items );
+}
+
+/**
+ * Determines the items that appear in the 'Recent' tab of the inserter.
+ *
+ * @param {Object} state Global application state.
+ * @param {string[]|boolean} enabledBlockTypes Enabled block types, or true/false to enable/disable all types.
+ * @returns {Editor.InserterItem[]} Items that appear in the 'Recent' tab.
+ */
+export function getRecentInserterItems( state, enabledBlockTypes = true ) {
+ if ( ! enabledBlockTypes ) {
+ return [];
+ }
+
+ const items = state.preferences.recentlyUsedBlocks.map( name =>
+ buildInserterItemFromBlockType( state, enabledBlockTypes, getBlockType( name ) )
+ );
+
+ // TODO: Merge in recently used reusable blocks
+
+ return compact( items );
}
/**
diff --git a/editor/store/test/selectors.js b/editor/store/test/selectors.js
index 790932fa3ff2b..08cea88df1912 100644
--- a/editor/store/test/selectors.js
+++ b/editor/store/test/selectors.js
@@ -7,7 +7,7 @@ import moment from 'moment';
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
-import { registerBlockType, unregisterBlockType } from '@wordpress/blocks';
+import { registerBlockType, unregisterBlockType, getBlockTypes } from '@wordpress/blocks';
/**
* Internal dependencies
@@ -70,8 +70,9 @@ import {
didPostSaveRequestFail,
getSuggestedPostFormat,
getNotices,
+ getInserterItems,
getMostFrequentlyUsedBlocks,
- getRecentlyUsedBlocks,
+ getRecentInserterItems,
getMetaBoxes,
getDirtyMetaBoxes,
getMetaBox,
@@ -97,6 +98,9 @@ describe( 'selectors', () => {
save: ( props ) => props.attributes.text,
category: 'common',
title: 'test block',
+ icon: 'test',
+ keywords: [ 'testing' ],
+ useOnce: true,
} );
} );
@@ -2248,7 +2252,107 @@ describe( 'selectors', () => {
} );
} );
- describe( 'getRecentlyUsedBlocks', () => {
+ describe( 'getInserterItems', () => {
+ it( 'should list all non-private regular block types', () => {
+ const state = {
+ editor: {
+ present: {
+ blocksByUid: {},
+ blockOrder: [],
+ },
+ },
+ reusableBlocks: {
+ data: {},
+ },
+ };
+
+ const blockTypes = getBlockTypes().filter( blockType => ! blockType.isPrivate );
+ expect( getInserterItems( state ) ).toHaveLength( blockTypes.length );
+ } );
+
+ it( 'should properly list a regular block type', () => {
+ const state = {
+ editor: {
+ present: {
+ blocksByUid: {},
+ blockOrder: [],
+ },
+ },
+ reusableBlocks: {
+ data: {},
+ },
+ };
+
+ expect( getInserterItems( state, [ 'core/test-block' ] ) ).toEqual( [
+ {
+ name: 'core/test-block',
+ initialAttributes: {},
+ title: 'test block',
+ icon: 'test',
+ category: 'common',
+ keywords: [ 'testing' ],
+ isDisabled: false,
+ },
+ ] );
+ } );
+
+ it( 'should set isDisabled when a regular block type with useOnce has been used', () => {
+ const state = {
+ editor: {
+ present: {
+ blocksByUid: {
+ 1: { uid: 1, name: 'core/test-block', attributes: {} },
+ },
+ blockOrder: [ 1 ],
+ },
+ },
+ reusableBlocks: {
+ data: {},
+ },
+ };
+
+ const items = getInserterItems( state, [ 'core/test-block' ] );
+ expect( items[ 0 ].isDisabled ).toBe( true );
+ } );
+
+ it( 'should properly list reusable blocks', () => {
+ const state = {
+ editor: {
+ present: {
+ blocksByUid: {},
+ blockOrder: [],
+ },
+ },
+ reusableBlocks: {
+ data: {
+ 123: {
+ id: 123,
+ title: 'My reusable block',
+ type: 'core/test-block',
+ },
+ },
+ },
+ };
+
+ expect( getInserterItems( state, [ 'core/block' ] ) ).toEqual( [
+ {
+ name: 'core/block',
+ initialAttributes: { ref: 123 },
+ title: 'My reusable block',
+ icon: 'test',
+ category: 'reusable-blocks',
+ keywords: [],
+ isDisabled: false,
+ },
+ ] );
+ } );
+
+ it( 'should return nothing when all block types are disabled', () => {
+ expect( getInserterItems( {}, false ) ).toEqual( [] );
+ } );
+ } );
+
+ describe( 'getRecentInserterItems', () => {
it( 'should return the most recently used blocks', () => {
const state = {
preferences: {
@@ -2256,7 +2360,7 @@ describe( 'selectors', () => {
},
};
- expect( getRecentlyUsedBlocks( state ).map( ( block ) => block.name ) )
+ expect( getRecentInserterItems( state ).map( ( item ) => item.name ) )
.toEqual( [ 'core/paragraph', 'core/image' ] );
} );
} );