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

Show most frequently used blocks next to inserter #2877

Merged
merged 9 commits into from Oct 10, 2017
41 changes: 21 additions & 20 deletions editor/modes/visual-editor/inserter.js
Expand Up @@ -7,7 +7,7 @@ import classnames from 'classnames';
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { __, sprintf } from '@wordpress/i18n';
import { IconButton } from '@wordpress/components';
import { Component } from '@wordpress/element';
import { createBlock } from '@wordpress/blocks';
Expand All @@ -17,15 +17,15 @@ import { createBlock } from '@wordpress/blocks';
*/
import Inserter from '../../inserter';
import { insertBlock } from '../../actions';
import { getMostFrequentlyUsedBlocks } from '../../selectors';

export class VisualEditorInserter extends Component {
constructor() {
super( ...arguments );

this.showControls = this.toggleControls.bind( this, true );
this.hideControls = this.toggleControls.bind( this, false );
this.insertParagraph = this.insertBlock.bind( this, 'core/paragraph' );
this.insertImage = this.insertBlock.bind( this, 'core/image' );
this.insertFrequentBlock = this.insertBlock.bind( this );

this.state = {
isShowingControls: false,
Expand All @@ -43,6 +43,7 @@ export class VisualEditorInserter extends Component {

render() {
const { isShowingControls } = this.state;
const { mostFrequentlyUsedBlocks } = this.props;
const classes = classnames( 'editor-visual-editor__inserter', {
'is-showing-controls': isShowingControls,
} );
Expand All @@ -54,28 +55,28 @@ export class VisualEditorInserter extends Component {
onBlur={ this.hideControls }
>
<Inserter position="top right" />
<IconButton
icon="editor-paragraph"
className="editor-inserter__block"
onClick={ this.insertParagraph }
label={ __( 'Insert paragraph block' ) }
>
{ __( 'Paragraph' ) }
</IconButton>
<IconButton
icon="format-image"
className="editor-inserter__block"
onClick={ this.insertImage }
label={ __( 'Insert image block' ) }
>
{ __( 'Image' ) }
</IconButton>
{ mostFrequentlyUsedBlocks && mostFrequentlyUsedBlocks.map(
( block ) =>
<IconButton
key={ 'frequently_used_' + block.name }
icon={ block.icon }
className="editor-inserter__block"
onClick={ () => this.insertBlock( block.name ) }
label={ sprintf( __( 'Insert %s' ), block.title ) }
>
{ block.title }
</IconButton>
) }
</div>
);
}
}

export default connect(
null,
( state ) => {
return {
mostFrequentlyUsedBlocks: getMostFrequentlyUsedBlocks( state ),
};
},
{ onInsertBlock: insertBlock },
)( VisualEditorInserter );
30 changes: 14 additions & 16 deletions editor/modes/visual-editor/test/inserter.js
Expand Up @@ -3,6 +3,11 @@
*/
import { shallow } from 'enzyme';

/**
* WordPress dependencies
*/
import { getBlockType } from '@wordpress/blocks';

/**
* Internal dependencies
*/
Expand All @@ -26,11 +31,18 @@ describe( 'VisualEditorInserter', () => {
expect( wrapper.state( 'isShowingControls' ) ).toBe( false );
} );

it( 'should insert paragraph block', () => {
it( 'should insert frequently used blocks', () => {
const onInsertBlock = jest.fn();
const mostFrequentlyUsedBlocks = [ getBlockType( 'core/paragraph' ), getBlockType( 'core/image' ) ];
const wrapper = shallow(
<VisualEditorInserter onInsertBlock={ onInsertBlock } />
<VisualEditorInserter onInsertBlock={ onInsertBlock } mostFrequentlyUsedBlocks={ mostFrequentlyUsedBlocks } />
);
wrapper.state.preferences = {
blockUsage: {
'core/paragraph': 42,
'core/image': 34,
},
};

wrapper
.findWhere( ( node ) => node.prop( 'children' ) === 'Paragraph' )
Expand All @@ -39,18 +51,4 @@ describe( 'VisualEditorInserter', () => {
expect( onInsertBlock ).toHaveBeenCalled();
expect( onInsertBlock.mock.calls[ 0 ][ 0 ].name ).toBe( 'core/paragraph' );
} );

it( 'should insert image block', () => {
const onInsertBlock = jest.fn();
const wrapper = shallow(
<VisualEditorInserter onInsertBlock={ onInsertBlock } />
);

wrapper
.findWhere( ( node ) => node.prop( 'children' ) === 'Image' )
.simulate( 'click' );

expect( onInsertBlock ).toHaveBeenCalled();
expect( onInsertBlock.mock.calls[ 0 ][ 0 ].name ).toBe( 'core/image' );
} );
} );
27 changes: 27 additions & 0 deletions editor/selectors.js
Expand Up @@ -11,6 +11,8 @@ import {
reduce,
some,
values,
keys,
without,
} from 'lodash';
import createSelector from 'rememo';

Expand All @@ -21,6 +23,11 @@ import { serialize, getBlockType } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import { addQueryArgs } from '@wordpress/url';

/***
* Module constants
*/
const MAX_FREQUENT_BLOCKS = 3;

/**
* Returns the current editing mode.
*
Expand Down Expand Up @@ -854,3 +861,23 @@ export function getRecentlyUsedBlocks( state ) {
// resolves the block names in the state to the block type settings
return state.preferences.recentlyUsedBlocks.map( blockType => getBlockType( blockType ) );
}

/**
* Resolves the block usage stats into a list of the most frequently used blocks.
* Memoized so we're not generating block lists every time we render the list
* in the inserter.
*
* @param {Object} state Global application state
* @return {Array} List of block type settings
*/
export const getMostFrequentlyUsedBlocks = createSelector(
( state ) => {
const { blockUsage } = state.preferences;
const orderedByUsage = keys( blockUsage ).sort( ( a, b ) => blockUsage[ b ] - blockUsage[ a ] );
// add in paragraph and image blocks if they're not already in the usage data
return [ ...orderedByUsage, ...without( [ 'core/paragraph', 'core/image' ], ...orderedByUsage ) ]
.slice( 0, MAX_FREQUENT_BLOCKS )
.map( blockType => getBlockType( blockType ) );
},
( state ) => state.preferences.blockUsage
);
29 changes: 29 additions & 0 deletions editor/test/selectors.js
Expand Up @@ -62,6 +62,7 @@ import {
didPostSaveRequestFail,
getSuggestedPostFormat,
getNotices,
getMostFrequentlyUsedBlocks,
} from '../selectors';

describe( 'selectors', () => {
Expand Down Expand Up @@ -1727,4 +1728,32 @@ describe( 'selectors', () => {
] );
} );
} );

describe( 'getMostFrequentlyUsedBlocks', () => {
it( 'should have paragraph and image to bring frequently used blocks up to three blocks', () => {
const noUsage = { preferences: { blockUsage: {} } };
const someUsage = { preferences: { blockUsage: { 'core/paragraph': 1 } } };

expect( getMostFrequentlyUsedBlocks( noUsage ).map( ( block ) => block.name ) )
.toEqual( [ 'core/paragraph', 'core/image' ] );

expect( getMostFrequentlyUsedBlocks( someUsage ).map( ( block ) => block.name ) )
.toEqual( [ 'core/paragraph', 'core/image' ] );
} );
it( 'should return the top 3 most recently used blocks', () => {
const state = {
preferences: {
blockUsage: {
'core/paragraph': 4,
'core/image': 11,
'core/quote': 2,
'core/gallery': 1,
},
},
};

expect( getMostFrequentlyUsedBlocks( state ).map( ( block ) => block.name ) )
.toEqual( [ 'core/image', 'core/paragraph', 'core/quote' ] );
} );
} );
} );