diff --git a/editor/store/defaults.js b/editor/store/defaults.js index 6cecfa08de0dbb..a99f2733600419 100644 --- a/editor/store/defaults.js +++ b/editor/store/defaults.js @@ -1,3 +1,3 @@ export const PREFERENCES_DEFAULTS = { - recentlyUsedBlocks: [], + recentInserts: [], }; diff --git a/editor/store/reducer.js b/editor/store/reducer.js index da53d5c48400b6..66616dd1ff9326 100644 --- a/editor/store/reducer.js +++ b/editor/store/reducer.js @@ -6,7 +6,6 @@ import { combineReducers } from 'redux'; import { flow, partialRight, - difference, reduce, keyBy, first, @@ -21,7 +20,7 @@ import { /** * WordPress dependencies */ -import { getBlockTypes, getBlockType } from '@wordpress/blocks'; +import { isReusableBlock } from '@wordpress/blocks'; /** * Internal dependencies @@ -30,11 +29,6 @@ import withHistory from '../utils/with-history'; import withChangeDetection from '../utils/with-change-detection'; import { PREFERENCES_DEFAULTS } from './defaults'; -/*** - * Module constants - */ -const MAX_RECENT_BLOCKS = 8; - /** * Returns a post attribute value, flattening nested rendered content using its * raw value in place of its original object form. @@ -531,28 +525,27 @@ export function isInsertionPointVisible( state = false, action ) { export function preferences( state = PREFERENCES_DEFAULTS, action ) { switch ( action.type ) { case 'INSERT_BLOCKS': - // put the block in the recently used blocks - let recentlyUsedBlocks = [ ...state.recentlyUsedBlocks ]; - action.blocks.forEach( ( block ) => { - recentlyUsedBlocks = [ block.name, ...without( recentlyUsedBlocks, block.name ) ].slice( 0, MAX_RECENT_BLOCKS ); - } ); - return { - ...state, - recentlyUsedBlocks, - }; - case 'SETUP_EDITOR': - const isBlockDefined = name => getBlockType( name ) !== undefined; - const filterInvalidBlocksFromList = list => list.filter( isBlockDefined ); - const commonBlocks = getBlockTypes() - .filter( ( blockType ) => 'common' === blockType.category ) - .map( ( blockType ) => blockType.name ); + return action.blocks.reduce( ( prevState, block ) => { + const insert = { name: block.name }; + if ( isReusableBlock( block ) ) { + insert.ref = block.attributes.ref; + } + + const isSameAsInsert = ( { name, ref } ) => name === insert.name && ref === insert.ref; + + return { + ...prevState, + recentInserts: [ + insert, + ...reject( prevState.recentInserts, isSameAsInsert ), + ], + }; + }, state ); + case 'REMOVE_REUSABLE_BLOCK': return { ...state, - // recently used gets filled up to `MAX_RECENT_BLOCKS` with blocks from the common category - recentlyUsedBlocks: filterInvalidBlocksFromList( [ ...state.recentlyUsedBlocks ] ) - .concat( difference( commonBlocks, state.recentlyUsedBlocks ) ) - .slice( 0, MAX_RECENT_BLOCKS ), + recentInserts: reject( state.recentInserts, insert => insert.ref === action.id ), }; } diff --git a/editor/store/selectors.js b/editor/store/selectors.js index b87000d7e1ae6d..ff7652f44120c3 100644 --- a/editor/store/selectors.js +++ b/editor/store/selectors.js @@ -11,6 +11,7 @@ import { compact, find, some, + unionWith, } from 'lodash'; import createSelector from 'rememo'; @@ -24,6 +25,7 @@ import { addQueryArgs } from '@wordpress/url'; /*** * Module constants */ +const MAX_RECENT_BLOCKS = 8; export const POST_UPDATE_TRANSACTION_ID = 'post-update'; /** @@ -1084,6 +1086,22 @@ export function getInserterItems( state, enabledBlockTypes = true ) { return compact( items ); } +const getRecentInserts = createSelector( + state => { + // Filter out any inserts that are associated with a block type that isn't registered + const inserts = state.preferences.recentInserts.filter( insert => getBlockType( insert.name ) ); + + // Common blocks that we'll use to pad out our list + const commonInserts = getBlockTypes() + .filter( blockType => blockType.category === 'common' ) + .map( blockType => ( { name: blockType.name } ) ); + + const areInsertsEqual = ( a, b ) => a.name === b.name && a.ref === b.ref; + return unionWith( inserts, commonInserts, areInsertsEqual ); + }, + state => state.preferences.recentInserts +); + /** * Determines the items that appear in the 'Recent' tab of the inserter. * @@ -1097,13 +1115,17 @@ export function getRecentInserterItems( state, enabledBlockTypes = true ) { return []; } - const items = state.preferences.recentlyUsedBlocks.map( name => - buildInserterItemFromBlockType( state, enabledBlockTypes, getBlockType( name ) ) - ); + const items = getRecentInserts( state ).map( insert => { + if ( insert.ref ) { + const reusableBlock = getReusableBlock( state, insert.ref ); + return buildInserterItemFromReusableBlock( enabledBlockTypes, reusableBlock ); + } - // TODO: Merge in recently used reusable blocks + const blockType = getBlockType( insert.name ); + return buildInserterItemFromBlockType( state, enabledBlockTypes, blockType ); + } ); - return compact( items ); + return compact( items ).slice( 0, MAX_RECENT_BLOCKS ); } /** diff --git a/editor/store/test/reducer.js b/editor/store/test/reducer.js index c3dc07796f9612..fd53c7a4a57950 100644 --- a/editor/store/test/reducer.js +++ b/editor/store/test/reducer.js @@ -11,7 +11,6 @@ import { registerCoreBlocks, registerBlockType, unregisterBlockType, - getBlockType, } from '@wordpress/blocks'; /** @@ -966,12 +965,12 @@ describe( 'state', () => { const state = preferences( undefined, {} ); expect( state ).toEqual( { - recentlyUsedBlocks: [], + recentInserts: [], } ); } ); it( 'should record recently used blocks', () => { - const state = preferences( deepFreeze( { recentlyUsedBlocks: [] } ), { + const state = preferences( deepFreeze( { recentInserts: [] } ), { type: 'INSERT_BLOCKS', blocks: [ { uid: 'bacon', @@ -979,42 +978,52 @@ describe( 'state', () => { } ], } ); - expect( state.recentlyUsedBlocks[ 0 ] ).toEqual( 'core-embed/twitter' ); + expect( state ).toEqual( { + recentInserts: [ + { name: 'core-embed/twitter' }, + ], + } ); - const twoRecentBlocks = preferences( deepFreeze( { recentlyUsedBlocks: [] } ), { + const twoRecentBlocks = preferences( deepFreeze( { recentInserts: [] } ), { type: 'INSERT_BLOCKS', blocks: [ { uid: 'eggs', name: 'core-embed/twitter', }, { uid: 'bacon', - name: 'core-embed/youtube', + name: 'core/block', + attributes: { ref: 123 }, } ], } ); - expect( twoRecentBlocks.recentlyUsedBlocks[ 0 ] ).toEqual( 'core-embed/youtube' ); - expect( twoRecentBlocks.recentlyUsedBlocks[ 1 ] ).toEqual( 'core-embed/twitter' ); - } ); - - it( 'should populate recentlyUsedBlocks, filling up with common blocks, on editor setup', () => { - const state = preferences( deepFreeze( { recentlyUsedBlocks: [ 'core-embed/twitter', 'core-embed/youtube' ] } ), { - type: 'SETUP_EDITOR', + expect( twoRecentBlocks ).toEqual( { + recentInserts: [ + { name: 'core/block', ref: 123 }, + { name: 'core-embed/twitter' }, + ], } ); + } ); - expect( state.recentlyUsedBlocks[ 0 ] ).toEqual( 'core-embed/twitter' ); - expect( state.recentlyUsedBlocks[ 1 ] ).toEqual( 'core-embed/youtube' ); + it( 'should remove recorded reusable blocks that are deleted', () => { + const initialState = { + recentInserts: [ + { name: 'core-embed/twitter' }, + { name: 'core/block', ref: 123 }, + { name: 'core/block', ref: 456 }, + ], + }; - state.recentlyUsedBlocks.slice( 2 ).forEach( - block => expect( getBlockType( block ).category ).toEqual( 'common' ) - ); - expect( state.recentlyUsedBlocks ).toHaveLength( 8 ); - } ); + const state = preferences( deepFreeze( initialState ), { + type: 'REMOVE_REUSABLE_BLOCK', + id: 123, + } ); - it( 'should remove unregistered blocks from persisted recent usage', () => { - const state = preferences( deepFreeze( { recentlyUsedBlocks: [ 'core-embed/i-do-not-exist', 'core-embed/youtube' ] } ), { - type: 'SETUP_EDITOR', + expect( state ).toEqual( { + recentInserts: [ + { name: 'core-embed/twitter' }, + { name: 'core/block', ref: 456 }, + ], } ); - expect( state.recentlyUsedBlocks[ 0 ] ).toEqual( 'core-embed/youtube' ); } ); } ); diff --git a/editor/store/test/selectors.js b/editor/store/test/selectors.js index 780999c2a7b117..f9f8f2367cf8e0 100644 --- a/editor/store/test/selectors.js +++ b/editor/store/test/selectors.js @@ -2,6 +2,7 @@ * External dependencies */ import moment from 'moment'; +import { union } from 'lodash'; /** * WordPress dependencies @@ -1921,18 +1922,70 @@ describe( 'selectors', () => { } ); describe( 'getRecentInserterItems', () => { - beforeEach( () => { + beforeAll( () => { registerCoreBlocks(); } ); - it( 'should return the most recently used blocks', () => { + + it( 'should return the 8 most recently used blocks', () => { const state = { preferences: { - recentlyUsedBlocks: [ 'core/deleted-block', 'core/paragraph', 'core/image' ], + recentInserts: [ + { name: 'core/deleted-block' }, // Deleted blocks should be filtered out + { name: 'core/block', ref: 456 }, // Deleted reusable blocks should be filtered out + { name: 'core/paragraph' }, + { name: 'core/block', ref: 123 }, + { name: 'core/image' }, + { name: 'core/quote' }, + { name: 'core/gallery' }, + { name: 'core/heading' }, + { name: 'core/list' }, + { name: 'core/video' }, + { name: 'core/audio' }, + { name: 'core/code' }, + ], + }, + editor: { + present: { + blockOrder: [], + }, + }, + reusableBlocks: { + data: { + 123: { id: 123, type: 'core/test-block' }, + }, + }, + }; + + expect( getRecentInserterItems( state ) ).toMatchObject( [ + { name: 'core/paragraph', initialAttributes: {} }, + { name: 'core/block', initialAttributes: { ref: 123 } }, + { name: 'core/image', initialAttributes: {} }, + { name: 'core/quote', initialAttributes: {} }, + { name: 'core/gallery', initialAttributes: {} }, + { name: 'core/heading', initialAttributes: {} }, + { name: 'core/list', initialAttributes: {} }, + { name: 'core/video', initialAttributes: {} }, + ] ); + } ); + + it( 'should pad list out with blocks from the common category', () => { + const state = { + preferences: { + recentInserts: [ + { name: 'core/paragraph' }, + ], + }, + editor: { + present: { + blockOrder: [], + }, }, }; - expect( getRecentInserterItems( state ).map( ( item ) => item.name ) ) - .toEqual( [ 'core/paragraph', 'core/image' ] ); + // We should get back 8 items with no duplicates + const items = getRecentInserterItems( state ); + const blockNames = items.map( item => item.name ); + expect( union( blockNames ) ).toHaveLength( 8 ); } ); } );