diff --git a/editor/inserter/menu.js b/editor/inserter/menu.js index 7a91f440de8f0e..b847eca5e10537 100644 --- a/editor/inserter/menu.js +++ b/editor/inserter/menu.js @@ -18,6 +18,7 @@ import { getCategories, getBlockTypes } from 'blocks'; */ import './style.scss'; import { showInsertionPoint, hideInsertionPoint } from '../actions'; +import { getRecentlyUsedBlocks } from '../selectors'; class InserterMenu extends Component { constructor() { @@ -29,11 +30,12 @@ class InserterMenu extends Component { tab: 'recent', }; this.filter = this.filter.bind( this ); - this.isShownBlock = this.isShownBlock.bind( this ); this.setSearchFocus = this.setSearchFocus.bind( this ); this.onKeyDown = this.onKeyDown.bind( this ); - this.getVisibleBlocks = this.getVisibleBlocks.bind( this ); - this.sortBlocksByCategory = this.sortBlocksByCategory.bind( this ); + this.searchBlocks = this.searchBlocks.bind( this ); + this.getBlocksForCurrentTab = this.getBlocksForCurrentTab.bind( this ); + this.sortBlocks = this.sortBlocks.bind( this ); + this.addRecentBlocks = this.addRecentBlocks.bind( this ); } componentDidMount() { @@ -44,10 +46,6 @@ class InserterMenu extends Component { document.removeEventListener( 'keydown', this.onKeyDown ); } - isShownBlock( block ) { - return block.title.toLowerCase().indexOf( this.state.filterValue.toLowerCase() ) !== -1; - } - bindReferenceNode( nodeName ) { return ( node ) => this.nodes[ nodeName ] = node; } @@ -68,11 +66,31 @@ class InserterMenu extends Component { }; } - getVisibleBlocks( blockTypes ) { - return filter( blockTypes, this.isShownBlock ); + searchBlocks( blockTypes ) { + const matchesSearch = ( block ) => block.title.toLowerCase().indexOf( this.state.filterValue.toLowerCase() ) !== -1; + return filter( blockTypes, matchesSearch ); + } + + getBlocksForCurrentTab() { + // if we're searching, use everything, otherwise just get the blocks visible in this tab + if ( this.state.filterValue ) { + return getBlockTypes(); + } + switch ( this.state.tab ) { + case 'recent': + return this.props.recentlyUsedBlocks; + case 'blocks': + return filter( getBlockTypes(), ( block ) => block.category !== 'embed' ); + case 'embeds': + return filter( getBlockTypes(), ( block ) => block.category === 'embed' ); + } } - sortBlocksByCategory( blockTypes ) { + sortBlocks( blockTypes ) { + if ( 'recent' === this.state.tab && ! this.state.filterValue ) { + return blockTypes; + } + const getCategoryIndex = ( item ) => { return findIndex( getCategories(), ( category ) => category.slug === item.category ); }; @@ -80,15 +98,21 @@ class InserterMenu extends Component { return sortBy( blockTypes, getCategoryIndex ); } + addRecentBlocks( blocksByCategory ) { + blocksByCategory.recent = this.props.recentlyUsedBlocks; + return blocksByCategory; + } + groupByCategory( blockTypes ) { return groupBy( blockTypes, ( blockType ) => blockType.category ); } getVisibleBlocksByCategory( blockTypes ) { return flow( - this.getVisibleBlocks, - this.sortBlocksByCategory, - this.groupByCategory + this.searchBlocks, + this.sortBlocks, + this.groupByCategory, + this.addRecentBlocks )( blockTypes ); } @@ -137,9 +161,9 @@ class InserterMenu extends Component { focusNext() { const sortedByCategory = flow( - this.getVisibleBlocks, - this.sortBlocksByCategory, - )( getBlockTypes() ); + this.searchBlocks, + this.sortBlocks, + )( this.getBlocksForCurrentTab() ); // If the block list is empty return early. if ( ! sortedByCategory.length ) { @@ -152,9 +176,9 @@ class InserterMenu extends Component { focusPrevious() { const sortedByCategory = flow( - this.getVisibleBlocks, - this.sortBlocksByCategory, - )( getBlockTypes() ); + this.searchBlocks, + this.sortBlocks, + )( this.getBlocksForCurrentTab() ); // If the block list is empty return early. if ( ! sortedByCategory.length ) { @@ -249,8 +273,8 @@ class InserterMenu extends Component { render() { const { position, instanceId } = this.props; - const visibleBlocksByCategory = this.getVisibleBlocksByCategory( getBlockTypes() ); const isSearching = this.state.filterValue; + const visibleBlocksByCategory = this.getVisibleBlocksByCategory( this.getBlocksForCurrentTab() ); /* eslint-disable jsx-a11y/no-autofocus */ return ( @@ -272,19 +296,15 @@ class InserterMenu extends Component {
{ this.state.tab === 'recent' && ! isSearching &&
- { getCategories() - .map( ( category ) => category.slug === 'common' && !! visibleBlocksByCategory[ category.slug ] && ( -
- { visibleBlocksByCategory[ category.slug ].map( ( block ) => this.getBlockItem( block ) ) } -
- ) ) - } +
+ { visibleBlocksByCategory.recent.map( ( block ) => this.getBlockItem( block ) ) } +
} { this.state.tab === 'blocks' && ! isSearching && @@ -375,7 +395,11 @@ class InserterMenu extends Component { } const connectComponent = connect( - undefined, + ( state ) => { + return { + recentlyUsedBlocks: getRecentlyUsedBlocks( state ), + }; + }, { showInsertionPoint, hideInsertionPoint } ); diff --git a/editor/selectors.js b/editor/selectors.js index a6c4175177ac86..6e71424290b7d9 100644 --- a/editor/selectors.js +++ b/editor/selectors.js @@ -5,6 +5,11 @@ import moment from 'moment'; import { first, last, get, values } from 'lodash'; import createSelector from 'rememo'; +/** + * WordPress dependencies + */ +import { getBlockType } from 'blocks'; + /** * Internal dependencies */ @@ -668,3 +673,14 @@ export function getSuggestedPostFormat( state ) { export function getNotices( state ) { return values( state.notices ); } + +/** + * Resolves the list of recently used block names into a list of block type settings. + * + * @param {Object} state Global application state + * @return {Array} List of recently used blocks + */ +export function getRecentlyUsedBlocks( state ) { + // resolves the block names in the state to the block type settings + return state.editor.recentlyUsedBlocks.map( blockType => getBlockType( blockType ) ); +} diff --git a/editor/state.js b/editor/state.js index 97c41a0226c43c..b53c47893c0f10 100644 --- a/editor/state.js +++ b/editor/state.js @@ -6,6 +6,11 @@ import { combineReducers, applyMiddleware, createStore } from 'redux'; import refx from 'refx'; import { reduce, keyBy, first, last, omit, without, flowRight } from 'lodash'; +/** + * WordPress dependencies + */ +import { getBlockTypes } from 'blocks'; + /** * Internal dependencies */ @@ -219,6 +224,28 @@ export const editor = combineUndoableReducers( { return state; }, + + recentlyUsedBlocks( state = [], action ) { + const maxRecent = 8; + switch ( action.type ) { + case 'SETUP_NEW_POST': + // This is where we initially populate the recently used blocks, + // for now this inserts blocks from the common category. + return getBlockTypes() + .filter( ( blockType ) => 'common' === blockType.category ) + .slice( 0, maxRecent ) + .map( ( blockType ) => blockType.name ); + case 'INSERT_BLOCKS': + // This is where we record the block usage so it can show up in + // the recent blocks. + let newState = [ ...state ]; + action.blocks.forEach( ( block ) => { + newState = [ block.name, ...without( newState, block.name ) ]; + } ); + return newState.slice( 0, maxRecent ); + } + return state; + }, }, { resetTypes: [ 'RESET_BLOCKS' ] } ); /** diff --git a/editor/test/state.js b/editor/test/state.js index 9e6786ba1923e9..78bf877208a815 100644 --- a/editor/test/state.js +++ b/editor/test/state.js @@ -29,7 +29,11 @@ import { describe( 'state', () => { describe( 'editor()', () => { beforeAll( () => { - registerBlockType( 'core/test-block', { save: noop } ); + registerBlockType( 'core/test-block', { + save: noop, + edit: noop, + category: 'common', + } ); } ); afterAll( () => { @@ -78,6 +82,42 @@ describe( 'state', () => { expect( state.blockOrder ).toEqual( [ 'chicken', 'ribs' ] ); } ); + it( 'should record recently used blocks', () => { + const original = editor( undefined, {} ); + const state = editor( original, { + type: 'INSERT_BLOCKS', + blocks: [ { + uid: 'bacon', + name: 'core-embed/twitter', + } ], + } ); + + expect( state.recentlyUsedBlocks[ 0 ] ).toEqual( 'core-embed/twitter' ); + + const twoRecentBlocks = editor( state, { + type: 'INSERT_BLOCKS', + blocks: [ { + uid: 'eggs', + name: 'core-embed/youtube', + } ], + } ); + + expect( twoRecentBlocks.recentlyUsedBlocks[ 0 ] ).toEqual( 'core-embed/youtube' ); + expect( twoRecentBlocks.recentlyUsedBlocks[ 1 ] ).toEqual( 'core-embed/twitter' ); + } ); + + it( 'should populate recently used blocks with the common category', () => { + const initial = editor( undefined, { + type: 'SETUP_NEW_POST', + edits: { + status: 'draft', + title: 'post title', + }, + } ); + + expect( initial.recentlyUsedBlocks ).toEqual( expect.arrayContaining( [ 'core/test-block', 'core/text' ] ) ); + } ); + it( 'should replace the block', () => { const original = editor( undefined, { type: 'RESET_BLOCKS',