Skip to content

Commit

Permalink
Merge pull request #3458 from WordPress/update/added-block-transforms…
Browse files Browse the repository at this point in the history
…-to-block-side-menu

Show transformations in the ellipsis menu
  • Loading branch information
Tammie Lister committed Dec 14, 2017
2 parents fc830db + 05d2e6b commit e859b0f
Show file tree
Hide file tree
Showing 7 changed files with 333 additions and 35 deletions.
81 changes: 80 additions & 1 deletion blocks/api/factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ import {
reduce,
castArray,
findIndex,
includes,
isObjectLike,
filter,
find,
first,
flatMap,
uniqueId,
} from 'lodash';

Expand All @@ -21,7 +25,7 @@ import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import { getBlockType } from './registration';
import { getBlockType, getBlockTypes } from './registration';

/**
* Returns a block object given its type and attributes.
Expand Down Expand Up @@ -57,6 +61,81 @@ export function createBlock( name, blockAttributes = {} ) {
};
}

/**
* Returns a predicate that receives a transformation and returns true if the given
* transformation is able to execute in the situation specified in the params
*
* @param {String} sourceName Block name
* @param {Boolean} isMultiBlock Array of possible block transformations
* @return {Function} Predicate that receives a block type.
*/
const isTransformForBlockSource = ( sourceName, isMultiBlock = false ) => ( transform ) => (
transform.type === 'block' &&
transform.blocks.indexOf( sourceName ) !== -1 &&
( ! isMultiBlock || transform.isMultiBlock )
);

/**
* Returns a predicate that receives a block type and returns true if the given block type contains a
* transformation able to execute in the situation specified in the params
*
* @param {String} sourceName Block name
* @param {Boolean} isMultiBlock Array of possible block transformations
* @return {Function} Predicate that receives a block type.
*/
const createIsTypeTransformableFrom = ( sourceName, isMultiBlock = false ) => ( type ) => (
!! find(
get( type, 'transforms.from', [] ),
isTransformForBlockSource( sourceName, isMultiBlock ),
)
);

/**
* Returns an array of possible block transformations that could happen on the set of blocks received as argument.
*
* @param {Array} blocks Blocks array
* @return {Array} Array of possible block transformations
*/
export function getPossibleBlockTransformations( blocks ) {
const sourceBlock = first( blocks );
if ( ! blocks || ! sourceBlock ) {
return [];
}
const isMultiBlock = blocks.length > 1;
const sourceBlockName = sourceBlock.name;

if ( isMultiBlock && ! every( blocks, { name: sourceBlockName } ) ) {
return [];
}

//compute the block that have a from transformation able to transfer blocks passed as argument.
const blocksToBeTransformedFrom = filter(
getBlockTypes(),
createIsTypeTransformableFrom( sourceBlockName, isMultiBlock ),
).map( type => type.name );

const blockType = getBlockType( sourceBlockName );
const transformsTo = get( blockType, 'transforms.to', [] );

//computes a list of blocks that source block can be transformed into using the "to transformations" implemented in it.
const blocksToBeTransformedTo = flatMap(
isMultiBlock ? filter( transformsTo, 'isMultiBlock' ) : transformsTo,
transformation => transformation.blocks
);

//returns a unique list of blocks that blocks passed as argument can transform into
return reduce( [
...blocksToBeTransformedFrom,
...blocksToBeTransformedTo,
], ( result, name ) => {
const transformBlockType = getBlockType( name );
if ( transformBlockType && ! includes( result, transformBlockType ) ) {
result.push( transformBlockType );
}
return result;
}, [] );
}

/**
* Switch one or more blocks into one or more blocks of the new block type.
*
Expand Down
2 changes: 1 addition & 1 deletion blocks/api/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { createBlock, switchToBlockType, createReusableBlock } from './factory';
export { createBlock, getPossibleBlockTransformations, switchToBlockType, createReusableBlock } from './factory';
export { default as parse, getBlockAttributes } from './parser';
export { default as rawHandler } from './raw-handling';
export { default as serialize, getBlockDefaultClassname, getBlockContent } from './serializer';
Expand Down
170 changes: 169 additions & 1 deletion blocks/api/test/factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { noop } from 'lodash';
/**
* Internal dependencies
*/
import { createBlock, switchToBlockType, createReusableBlock } from '../factory';
import { createBlock, getPossibleBlockTransformations, switchToBlockType, createReusableBlock } from '../factory';
import { getBlockTypes, unregisterBlockType, setUnknownTypeHandlerName, registerBlockType } from '../registration';

describe( 'block factory', () => {
Expand Down Expand Up @@ -98,6 +98,174 @@ describe( 'block factory', () => {
} );
} );

describe( 'getPossibleBlockTransformations()', () => {
it( 'should should show as available a simple "from" transformation"', () => {
registerBlockType( 'core/updated-text-block', {
attributes: {
value: {
type: 'string',
},
},
transforms: {
from: [ {
type: 'block',
blocks: [ 'core/text-block' ],
transform: noop,
} ],
},
save: noop,
category: 'common',
title: 'updated text block',
} );
registerBlockType( 'core/text-block', defaultBlockSettings );

const block = createBlock( 'core/text-block', {
value: 'chicken',
} );

const availableBlocks = getPossibleBlockTransformations( [ block ] );

expect( availableBlocks ).toHaveLength( 1 );
expect( availableBlocks[ 0 ].name ).toBe( 'core/updated-text-block' );
} );

it( 'should show as available a simple "to" transformation"', () => {
registerBlockType( 'core/updated-text-block', {
attributes: {
value: {
type: 'string',
},
},
transforms: {
to: [ {
type: 'block',
blocks: [ 'core/text-block' ],
transform: noop,
} ],
},
save: noop,
category: 'common',
title: 'updated text block',
} );
registerBlockType( 'core/text-block', defaultBlockSettings );

const block = createBlock( 'core/updated-text-block', {
value: 'ribs',
} );

const availableBlocks = getPossibleBlockTransformations( [ block ] );

expect( availableBlocks ).toHaveLength( 1 );
expect( availableBlocks[ 0 ].name ).toBe( 'core/text-block' );
} );

it( 'should not show a transformation if multiple blocks are passed and the transformation is not multi block', () => {
registerBlockType( 'core/updated-text-block', {
attributes: {
value: {
type: 'string',
},
},
transforms: {
from: [ {
type: 'block',
blocks: [ 'core/text-block' ],
transform: noop,
} ],
},
save: noop,
category: 'common',
title: 'updated text block',
} );
registerBlockType( 'core/text-block', defaultBlockSettings );

const block1 = createBlock( 'core/text-block', {
value: 'chicken',
} );

const block2 = createBlock( 'core/text-block', {
value: 'ribs',
} );

const availableBlocks = getPossibleBlockTransformations( [ block1, block2 ] );

expect( availableBlocks ).toEqual( [] );
} );

it( 'should show a transformation as available if multiple blocks are passed and the transformation accepts multiple blocks', () => {
registerBlockType( 'core/updated-text-block', {
attributes: {
value: {
type: 'string',
},
},
transforms: {
from: [ {
type: 'block',
blocks: [ 'core/text-block' ],
transform: noop,
isMultiBlock: true,
} ],
},
save: noop,
category: 'common',
title: 'updated text block',
} );
registerBlockType( 'core/text-block', defaultBlockSettings );

const block1 = createBlock( 'core/text-block', {
value: 'chicken',
} );

const block2 = createBlock( 'core/text-block', {
value: 'ribs',
} );

const availableBlocks = getPossibleBlockTransformations( [ block1, block2 ] );

expect( availableBlocks ).toHaveLength( 1 );
expect( availableBlocks[ 0 ].name ).toBe( 'core/updated-text-block' );
} );

it( 'should show multiple possible transformations"', () => {
registerBlockType( 'core/updated-text-block', {
attributes: {
value: {
type: 'string',
},
},
transforms: {
to: [ {
type: 'block',
blocks: [ 'core/text-block' ],
transform: noop,
isMultiBlock: true,
}, {
type: 'block',
blocks: [ 'core/another-text-block' ],
transform: noop,
isMultiBlock: true,
} ],
},
save: noop,
category: 'common',
title: 'updated text block',
} );
registerBlockType( 'core/text-block', defaultBlockSettings );
registerBlockType( 'core/another-text-block', defaultBlockSettings );

const block = createBlock( 'core/updated-text-block', {
value: 'chicken',
} );

const availableBlocks = getPossibleBlockTransformations( [ block ] );

expect( availableBlocks ).toHaveLength( 2 );
expect( availableBlocks[ 0 ].name ).toBe( 'core/text-block' );
expect( availableBlocks[ 1 ].name ).toBe( 'core/another-text-block' );
} );
} );

describe( 'switchToBlockType()', () => {
it( 'should switch the blockType of a block using the "transform form"', () => {
registerBlockType( 'core/updated-text-block', {
Expand Down
73 changes: 73 additions & 0 deletions editor/components/block-settings-menu/block-transformations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* External dependencies
*/
import { connect } from 'react-redux';
import { noop } from 'lodash';

/**
* WordPress dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { IconButton, withContext } from '@wordpress/components';
import { getPossibleBlockTransformations, switchToBlockType } from '@wordpress/blocks';
import { compose } from '@wordpress/element';

/**
* Internal dependencies
*/
import './style.scss';
import { getBlock } from '../../selectors';
import { replaceBlocks } from '../../actions';

function BlockTransformations( { blocks, small = false, onTransform, onClick = noop, isLocked } ) {
const possibleBlockTransformations = getPossibleBlockTransformations( blocks );
if ( isLocked || ! possibleBlockTransformations.length ) {
return null;
}
return (
<div className="editor-block-settings-menu__block-transformations">
{ possibleBlockTransformations.map( ( { name, title, icon } ) => {
/* translators: label indicating the transformation of a block into another block */
const shownText = sprintf( __( 'Turn into %s' ), title );
return (
<IconButton
key={ name }
className="editor-block-settings-menu__control"
onClick={ ( event ) => {
onTransform( blocks, name );
onClick( event );
} }
icon={ icon }
label={ small ? shownText : undefined }
>
{ ! small && shownText }
</IconButton>
);
} ) }
</div>
);
}
export default compose(
connect(
( state, ownProps ) => {
return {
blocks: ownProps.uids.map( ( uid ) => getBlock( state, uid ) ),
};
},
( dispatch, ownProps ) => ( {
onTransform( blocks, name ) {
dispatch( replaceBlocks(
ownProps.uids,
switchToBlockType( blocks, name )
) );
},
} )
),
withContext( 'editor' )( ( settings ) => {
const { templateLock } = settings;

return {
isLocked: !! templateLock,
};
} ),
)( BlockTransformations );
2 changes: 2 additions & 0 deletions editor/components/block-settings-menu/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import './style.scss';
import BlockInspectorButton from './block-inspector-button';
import BlockModeToggle from './block-mode-toggle';
import BlockDeleteButton from './block-delete-button';
import BlockTransformations from './block-transformations';
import ReusableBlockToggle from './reusable-block-toggle';
import UnknownConverter from './unknown-converter';
import { selectBlock } from '../../actions';
Expand Down Expand Up @@ -58,6 +59,7 @@ function BlockSettingsMenu( { uids, onSelect, focus } ) {
{ count === 1 && <UnknownConverter uid={ uids[ 0 ] } /> }
<BlockDeleteButton uids={ uids } />
{ count === 1 && <ReusableBlockToggle uid={ uids[ 0 ] } onToggle={ onClose } /> }
<BlockTransformations uids={ uids } onClick={ onClose } />
</NavigableMenu>
) }
/>
Expand Down
4 changes: 4 additions & 0 deletions editor/components/block-settings-menu/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,7 @@
margin-right: 5px;
}
}

.editor-block-settings-menu__block-transformations {
border-top: 1px solid $dark-gray-500;
}
Loading

0 comments on commit e859b0f

Please sign in to comment.