Skip to content
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
1 change: 1 addition & 0 deletions docs/reference-guides/block-api/block-transforms.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ A transformation of type `block` is an object that takes the following parameter
- **type** _(string)_: the value `block`.
- **blocks** _(array)_: a list of known block types. It also accepts the wildcard value (`"*"`), meaning that the transform is available to _all_ block types (eg: all blocks can transform into `core/group`).
- **transform** _(function)_: a callback that receives the attributes and inner blocks of the block being processed. It should return a block object or an array of block objects.
- **variationName** _(string, optional)_: the name of the target block variation when the transform creates a variation of the transformed block type. This lets the transform UI show the variation title and icon instead of the base block type.
- **isMatch** _(function, optional)_: a callback that receives the block attributes as the first argument and the block object as the second argument and should return a boolean. Returning `false` from this function will prevent the transform from being available and displayed as an option to the user.
- **isMultiBlock** _(boolean, optional)_: whether the transformation can be applied when multiple blocks are selected. If true, the `transform` function's first parameter will be an array containing each selected block's attributes, and the second an array of each selected block's inner blocks. False by default.
- **priority** _(number, optional)_: controls the priority with which a transformation is applied, where a lower value will take precedence over higher values. This behaves much like a [WordPress hook](https://developer.wordpress.org/reference/#Hook_to_WordPress). Like hooks, the default priority is `10` when not otherwise set.
Expand Down
1 change: 1 addition & 0 deletions docs/reference-guides/data/data-core-block-editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,7 @@ _Properties_

- _id_ `string`: Unique identifier for the item.
- _name_ `string`: The type of block to create.
- _variationName_ `?string`: The target block variation name.
- _title_ `string`: Title of the item, as it appears in the inserter.
- _icon_ `string`: Dashicon for the item, as it appears in the inserter.
- _isDisabled_ `boolean`: Whether or not the user should be prevented from inserting this item.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,7 @@ const BlockTransformationsMenu = ( {
onSelectVariation,
blocks,
} ) => {
const [ hoveredTransformItemName, setHoveredTransformItemName ] =
useState();
const [ hoveredTransformItem, setHoveredTransformItem ] = useState();

const { priorityTextTransformations, restTransformations } =
useGroupedTransforms( possibleBlockTransformations );
Expand All @@ -101,17 +100,18 @@ const BlockTransformationsMenu = ( {
<RestTransformationItems
restTransformations={ restTransformations }
onSelect={ onSelect }
setHoveredTransformItemName={ setHoveredTransformItemName }
setHoveredTransformItem={ setHoveredTransformItem }
/>
);
return (
<>
<MenuGroup label={ __( 'Transform to' ) } className={ className }>
{ hoveredTransformItemName && (
{ hoveredTransformItem && (
<PreviewBlockPopover
blocks={ switchToBlockType(
blocks,
hoveredTransformItemName
hoveredTransformItem.name,
hoveredTransformItem.variationName
) }
/>
) }
Expand All @@ -126,12 +126,10 @@ const BlockTransformationsMenu = ( {
) }
{ priorityTextTransformations.map( ( item ) => (
<BlockTransformationItem
key={ item.name }
key={ item.id || item.name }
item={ item }
onSelect={ onSelect }
setHoveredTransformItemName={
setHoveredTransformItemName
}
setHoveredTransformItem={ setHoveredTransformItem }
/>
) ) }
{ ! hasBothContentTransformations && restTransformItems }
Expand All @@ -148,36 +146,36 @@ const BlockTransformationsMenu = ( {
function RestTransformationItems( {
restTransformations,
onSelect,
setHoveredTransformItemName,
setHoveredTransformItem,
} ) {
return restTransformations.map( ( item ) => (
<BlockTransformationItem
key={ item.name }
key={ item.id || item.name }
item={ item }
onSelect={ onSelect }
setHoveredTransformItemName={ setHoveredTransformItemName }
setHoveredTransformItem={ setHoveredTransformItem }
/>
) );
}

function BlockTransformationItem( {
item,
onSelect,
setHoveredTransformItemName,
setHoveredTransformItem,
} ) {
const { name, icon, title, isDisabled } = item;
return (
<MenuItem
className={ getBlockMenuDefaultClassName( name ) }
onClick={ ( event ) => {
event.preventDefault();
onSelect( name );
onSelect( name, item.variationName );
} }
disabled={ isDisabled }
onMouseLeave={ () => setHoveredTransformItemName( null ) }
onMouseEnter={ () => setHoveredTransformItemName( name ) }
onFocus={ () => setHoveredTransformItemName( name ) }
onBlur={ () => setHoveredTransformItemName( null ) }
onMouseLeave={ () => setHoveredTransformItem( null ) }
onMouseEnter={ () => setHoveredTransformItem( item ) }
onFocus={ () => setHoveredTransformItem( item ) }
onBlur={ () => setHoveredTransformItem( null ) }
>
<BlockIcon icon={ icon } showColors />
{ title }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,28 @@ const BlockTransformationsMenu = ( {
anchorNodeRef ? findNodeHandle( anchorNodeRef ) : undefined;

function onPickerSelect( value ) {
const selectedItem = possibleTransformations.find(
( item ) => item.id === value
);
if ( ! selectedItem ) {
return;
}
replaceBlocks(
selectedBlockClientId,
switchToBlockType( selectedBlock, value )
switchToBlockType(
selectedBlock,
selectedItem.name,
selectedItem.variationName
)
);

const selectedItem = pickerOptions().find(
const selectedOption = pickerOptions().find(
( item ) => item.value === value
);
const successNotice = sprintf(
/* translators: 1: From block title, e.g. Paragraph. 2: To block title, e.g. Header. */
__( '%1$s transformed to %2$s' ),
blockTitle,
selectedItem.label
selectedOption?.label || selectedItem.title
);
createSuccessNotice( successNotice );
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ function BlockSwitcherDropdownMenuContents( { onClose, clientIds } ) {
}
}
// Simple block transformation based on the `Block Transforms` API.
function onBlockTransform( name ) {
const newBlocks = switchToBlockType( blocks, name );
function onBlockTransform( name, variationName ) {
const newBlocks = switchToBlockType( blocks, name, variationName );
replaceBlocks( clientIds, newBlocks );
selectForMultipleBlocks( newBlocks );
}
Expand Down Expand Up @@ -166,8 +166,8 @@ function BlockSwitcherDropdownMenuContents( { onClose, clientIds } ) {
blockVariationTransformations
}
blocks={ blocks }
onSelect={ ( name ) => {
onBlockTransform( name );
onSelect={ ( name, variationName ) => {
onBlockTransform( name, variationName );
onClose();
} }
onSelectVariation={ ( name ) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ const getTransformCommands = () =>
}

// Simple block transformation based on the `Block Transforms` API.
function onBlockTransform( name ) {
const newBlocks = switchToBlockType( blocks, name );
function onBlockTransform( name, variationName ) {
const newBlocks = switchToBlockType( blocks, name, variationName );
replaceBlocks( clientIds, newBlocks );
selectForMultipleBlocks( newBlocks );
}
Expand All @@ -117,7 +117,7 @@ const getTransformCommands = () =>

const commands = possibleBlockTransformations.map(
( transformation ) => {
const { name, title, icon } = transformation;
const { id, name, title, icon, variationName } = transformation;
/*
* Command menu uses Icon from @wordpress/icons, which expects a ReactElement
* (cloneElement). Normalize to blockDefaultIcon to avoid crash. See #55668 / PR #55676.
Expand All @@ -132,13 +132,13 @@ const getTransformCommands = () =>
return {
name:
'core/block-editor/transform-to-' +
name.replace( '/', '-' ),
( id || name ).replace( /\//g, '-' ),
/* translators: %s: Block or block variation name. */
label: sprintf( __( 'Transform to %s' ), title ),
icon: blockIcon?.src,
category: 'command',
callback: ( { close } ) => {
onBlockTransform( name );
onBlockTransform( name, variationName );
close();
},
};
Expand Down
63 changes: 50 additions & 13 deletions packages/block-editor/src/store/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -2294,6 +2294,19 @@ const buildBlockTypeItem =
};
};

const buildBlockVariationItem = ( state, item ) => ( variation ) => {
const variationId = `${ item.id }/${ variation.name }`;
const { time, count = 0 } = getInsertUsage( state, variationId ) || {};
return {
...item,
id: variationId,
icon: variation.icon || item.icon,
title: variation.title || item.title,
frecency: calculateFrecency( time, count ),
variationName: variation.name,
};
};

/**
* Determines the items that appear in the inserter. Includes both static
* items (e.g. a regular block type) and dynamic items (e.g. a reusable block).
Expand Down Expand Up @@ -2474,20 +2487,21 @@ export const getInserterItems = createRegistrySelector( ( select ) =>
*
* Items are returned ordered descendingly by their 'frecency'.
*
* @param {Object} state Editor state.
* @param {Object|Object[]} blocks Block object or array objects.
* @param {?string} rootClientId Optional root client ID of block list.
* @param {Object} state Editor state.
* @param {Object|Object[]} blocks Block object or array objects.
* @param {?string} rootClientId Optional root client ID of block list.
*
* @return {WPEditorTransformItem[]} Items that appear in inserter.
*
* @typedef {Object} WPEditorTransformItem
* @property {string} id Unique identifier for the item.
* @property {string} name The type of block to create.
* @property {string} title Title of the item, as it appears in the inserter.
* @property {string} icon Dashicon for the item, as it appears in the inserter.
* @property {boolean} isDisabled Whether or not the user should be prevented from inserting
* this item.
* @property {number} frecency Heuristic that combines frequency and recency.
* @property {string} id Unique identifier for the item.
* @property {string} name The type of block to create.
* @property {?string} variationName The target block variation name.
* @property {string} title Title of the item, as it appears in the inserter.
* @property {string} icon Dashicon for the item, as it appears in the inserter.
* @property {boolean} isDisabled Whether or not the user should be prevented from inserting
* this item.
* @property {number} frecency Heuristic that combines frequency and recency.
*/
export const getBlockTransformItems = createRegistrySelector( ( select ) =>
createSelector(
Expand Down Expand Up @@ -2517,14 +2531,37 @@ export const getBlockTransformItems = createRegistrySelector( ( select ) =>
const possibleTransforms = getPossibleBlockTransformations(
normalizedBlocks
).reduce( ( accumulator, block ) => {
if ( itemsByName[ block?.name ] ) {
accumulator.push( itemsByName[ block.name ] );
const item = itemsByName[ block?.name ];

if ( ! item ) {
return accumulator;
}

const { variationName } = block;

if ( ! variationName ) {
accumulator.push( item );
return accumulator;
}

const variation = getBlockVariations(
item.name,
'transform'
)?.find( ( { name } ) => name === variationName );

if ( ! variation ) {
accumulator.push( item );
return accumulator;
}

accumulator.push(
buildBlockVariationItem( state, item )( variation )
);
return accumulator;
}, [] );
return orderBy(
possibleTransforms,
( block ) => itemsByName[ block.name ].frecency,
( block ) => block.frecency,
'desc'
);
},
Expand Down
76 changes: 76 additions & 0 deletions packages/block-editor/src/store/test/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -4031,6 +4031,82 @@ describe( 'selectors', () => {
} )
);
} );

it( 'should use variation metadata for transformation items', () => {
registerBlockType( 'core/variation-transform-source', {
apiVersion: 3,
category: 'text',
title: 'Variation Transform Source',
edit: () => {},
save: () => {},
transforms: {
to: [
{
type: 'block',
blocks: [ 'core/variation-transform-target' ],
variationName: 'grid',
transform: () => {},
},
],
},
} );
registerBlockType( 'core/variation-transform-target', {
apiVersion: 3,
category: 'design',
title: 'Group',
icon: 'group',
edit: () => {},
save: () => {},
variations: [
{
name: 'grid',
title: 'Grid',
icon: 'grid',
attributes: { layout: { type: 'grid' } },
scope: [ 'transform' ],
},
],
} );

const state = {
blocks: {
byClientId: new Map(),
attributes: new Map(),
order: new Map(),
parents: new Map(),
cache: {},
blockEditingModes: new Map(),
},
preferences: {
insertUsage: {
'core/variation-transform-target/grid': {
count: 10,
time: 1000,
},
},
},
blockListSettings: new Map(),
settings: {},
};
const blocks = [ { name: 'core/variation-transform-source' } ];

try {
const items = getBlockTransformItems( state, blocks );

expect( items ).toHaveLength( 1 );
expect( items[ 0 ] ).toMatchObject( {
id: 'core/variation-transform-target/grid',
name: 'core/variation-transform-target',
variationName: 'grid',
title: 'Grid',
icon: 'grid',
frecency: 2.5,
} );
} finally {
unregisterBlockType( 'core/variation-transform-source' );
unregisterBlockType( 'core/variation-transform-target' );
}
} );
} );

describe( 'isValidTemplate', () => {
Expand Down
Loading
Loading