Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display reusable blocks in the inserter's 'Recent' tab #4768

Merged
merged 2 commits into from
Feb 1, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion editor/store/defaults.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export const PREFERENCES_DEFAULTS = {
recentlyUsedBlocks: [],
recentInserts: [],
};
45 changes: 19 additions & 26 deletions editor/store/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { combineReducers } from 'redux';
import {
flow,
partialRight,
difference,
reduce,
keyBy,
first,
Expand All @@ -21,7 +20,7 @@ import {
/**
* WordPress dependencies
*/
import { getBlockTypes, getBlockType } from '@wordpress/blocks';
import { isReusableBlock } from '@wordpress/blocks';

/**
* Internal dependencies
Expand All @@ -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.
Expand Down Expand Up @@ -530,28 +524,27 @@ export function blockInsertionPoint( state = {}, 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 ),
};
}

Expand Down
32 changes: 27 additions & 5 deletions editor/store/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
compact,
find,
some,
unionWith,
} from 'lodash';
import createSelector from 'rememo';

Expand All @@ -24,6 +25,7 @@ import { addQueryArgs } from '@wordpress/url';
/***
* Module constants
*/
const MAX_RECENT_BLOCKS = 8;
export const POST_UPDATE_TRANSACTION_ID = 'post-update';

/**
Expand Down Expand Up @@ -1106,6 +1108,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.
*
Expand All @@ -1119,13 +1137,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 );
}

/**
Expand Down
57 changes: 33 additions & 24 deletions editor/store/test/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
registerCoreBlocks,
registerBlockType,
unregisterBlockType,
getBlockType,
} from '@wordpress/blocks';

/**
Expand Down Expand Up @@ -970,55 +969,65 @@ 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',
name: 'core-embed/twitter',
} ],
} );

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' );
} );
} );

Expand Down
63 changes: 58 additions & 5 deletions editor/store/test/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* External dependencies
*/
import moment from 'moment';
import { union } from 'lodash';

/**
* WordPress dependencies
Expand Down Expand Up @@ -1961,18 +1962,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 );
} );
} );

Expand Down