Skip to content

Commit

Permalink
Block Hooks: Set ignoredHookedBlocks metada attr upon insertion (#58553)
Browse files Browse the repository at this point in the history
In WP 6.5, we'll be enabling insertion of hooked blocks into modified templates/parts/patterns. This is implemented by adding a `metadata.ignoredHookedBlocks` attribute to the hooked block's anchor block upon editing.

As a consequence, upon insertion of an anchor block, this attribute also needs to be added to that newly inserted instance.

_A note on UX: It could be argued that ideally, insertion of a new anchor block should cause its hooked blocks to be inserted alongside with it. This could be a future addition but seems to be a bit more complicated to do. Regardless, the `metadata.ignoredHookedBlocks` attribute also would need to be set in that case, in order to avoid double insertion -- meaning that adding that attribute is required as a baseline anyway._

Co-authored-by: ockham <bernhard-reiter@git.wordpress.org>
Co-authored-by: gziolo <gziolo@git.wordpress.org>
  • Loading branch information
3 people committed Feb 8, 2024
1 parent 55e838f commit 7449074
Show file tree
Hide file tree
Showing 9 changed files with 364 additions and 3 deletions.
48 changes: 48 additions & 0 deletions docs/reference-guides/data/data-core-blocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,54 @@ _Returns_

- `string?`: Name of the block for handling the grouping of blocks.

### getHookedBlocks

Returns the hooked blocks for a given anchor block.

Given an anchor block name, returns an object whose keys are relative positions, and whose values are arrays of block names that are hooked to the anchor block at that relative position.

_Usage_

```js
import { store as blocksStore } from '@wordpress/blocks';
import { useSelect } from '@wordpress/data';

const ExampleComponent = () => {
const hookedBlockNames = useSelect(
( select ) =>
select( blocksStore ).getHookedBlocks( 'core/navigation' ),
[]
);

return (
<ul>
{ Object.keys( hookedBlockNames ).length &&
Object.keys( hookedBlockNames ).map( ( relativePosition ) => (
<li key={ relativePosition }>
{ relativePosition }>
<ul>
{ hookedBlockNames[ relativePosition ].map(
( hookedBlock ) => (
<li key={ hookedBlock }>{ hookedBlock }</li>
)
) }
</ul>
</li>
) ) }
</ul>
);
};
```

_Parameters_

- _state_ `Object`: Data state.
- _blockName_ `string`: Anchor block type name.

_Returns_

- `Object`: Lists of hooked block names for each relative position.

### getUnregisteredFallbackBlockName

Returns the name of the block for handling unregistered blocks.
Expand Down
10 changes: 9 additions & 1 deletion packages/block-editor/src/store/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
getBlockType,
getBlockTypes,
getBlockVariations,
getHookedBlocks,
hasBlockSupport,
getPossibleBlockTransformations,
parse,
Expand Down Expand Up @@ -1936,9 +1937,16 @@ const buildBlockTypeItem =
blockType.name,
'inserter'
);

const ignoredHookedBlocks = [
...new Set( Object.values( getHookedBlocks( id ) ).flat() ),
];

return {
...blockItemBase,
initialAttributes: {},
initialAttributes: ignoredHookedBlocks.length
? { metadata: { ignoredHookedBlocks } }
: {},
description: blockType.description,
category: blockType.category,
keywords: blockType.keywords,
Expand Down
14 changes: 14 additions & 0 deletions packages/blocks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,20 @@ _Returns_

- `?string`: Block name.

### getHookedBlocks

Returns the hooked blocks for a given anchor block.

Given an anchor block name, returns an object whose keys are relative positions, and whose values are arrays of block names that are hooked to the anchor block at that relative position.

_Parameters_

- _name_ `string`: Anchor block name.

_Returns_

- `Object`: Lists of hooked block names for each relative position.

### getPhrasingContentSchema

Undocumented declaration.
Expand Down
1 change: 1 addition & 0 deletions packages/blocks/src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export {
getBlockTypes,
getBlockSupport,
hasBlockSupport,
getHookedBlocks,
getBlockVariations,
isReusableBlock,
isTemplatePart,
Expand Down
15 changes: 15 additions & 0 deletions packages/blocks/src/api/registration.js
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,21 @@ export function hasBlockSupport( nameOrType, feature, defaultSupports ) {
);
}

/**
* Returns the hooked blocks for a given anchor block.
*
* Given an anchor block name, returns an object whose keys are relative positions,
* and whose values are arrays of block names that are hooked to the anchor block
* at that relative position.
*
* @param {string} name Anchor block name.
*
* @return {Object} Lists of hooked block names for each relative position.
*/
export function getHookedBlocks( name ) {
return select( blocksStore ).getHookedBlocks( name );
}

/**
* Determines whether or not the given block is a reusable block. This is a
* special block type that is used to point to a global block stored via the
Expand Down
31 changes: 30 additions & 1 deletion packages/blocks/src/api/templates.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { renderToString } from '@wordpress/element';
*/
import { convertLegacyBlockNameAndAttributes } from './parser/convert-legacy-block';
import { createBlock } from './factory';
import { getBlockType } from './registration';
import { getBlockType, getHookedBlocks } from './registration';

/**
* Checks whether a list of blocks matches a template by comparing the block names.
Expand Down Expand Up @@ -115,6 +115,35 @@ export function synchronizeBlocksWithTemplate( blocks = [], template ) {
normalizedAttributes
);

const ignoredHookedBlocks = [
...new Set(
Object.values( getHookedBlocks( blockName ) ).flat()
),
];

if ( ignoredHookedBlocks.length ) {
const { metadata = {}, ...otherAttributes } = blockAttributes;
const {
ignoredHookedBlocks: ignoredHookedBlocksFromTemplate = [],
...otherMetadata
} = metadata;

const newIgnoredHookedBlocks = [
...new Set( [
...ignoredHookedBlocks,
...ignoredHookedBlocksFromTemplate,
] ),
];

blockAttributes = {
metadata: {
ignoredHookedBlocks: newIgnoredHookedBlocks,
...otherMetadata,
},
...otherAttributes,
};
}

// If a Block is undefined at this point, use the core/missing block as
// a placeholder for a better user experience.
if ( undefined === getBlockType( blockName ) ) {
Expand Down
80 changes: 79 additions & 1 deletion packages/blocks/src/api/test/templates.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ describe( 'templates', () => {

beforeEach( () => {
registerBlockType( 'core/test-block', {
attributes: {},
attributes: {
metadata: {
type: 'object',
},
},
save: noop,
category: 'text',
title: 'test block',
Expand Down Expand Up @@ -132,6 +136,80 @@ describe( 'templates', () => {
] );
} );

it( 'should set ignoredHookedBlocks metadata if a block has hooked blocks', () => {
registerBlockType( 'core/hooked-block', {
attributes: {},
save: noop,
category: 'text',
title: 'hooked block',
blockHooks: { 'core/test-block': 'after' },
} );

const template = [
[ 'core/test-block' ],
[ 'core/test-block-2' ],
[ 'core/test-block-2' ],
];
const blockList = [];

expect(
synchronizeBlocksWithTemplate( blockList, template )
).toMatchObject( [
{
name: 'core/test-block',
attributes: {
metadata: {
ignoredHookedBlocks: [ 'core/hooked-block' ],
},
},
},
{ name: 'core/test-block-2' },
{ name: 'core/test-block-2' },
] );
} );

it( 'retains previously set ignoredHookedBlocks metadata', () => {
registerBlockType( 'core/hooked-block', {
attributes: {},
save: noop,
category: 'text',
title: 'hooked block',
blockHooks: { 'core/test-block': 'after' },
} );

const template = [
[
'core/test-block',
{
metadata: {
ignoredHookedBlocks: [ 'core/other-hooked-block' ],
},
},
],
[ 'core/test-block-2' ],
[ 'core/test-block-2' ],
];
const blockList = [];

expect(
synchronizeBlocksWithTemplate( blockList, template )
).toMatchObject( [
{
name: 'core/test-block',
attributes: {
metadata: {
ignoredHookedBlocks: [
'core/hooked-block',
'core/other-hooked-block',
],
},
},
},
{ name: 'core/test-block-2' },
{ name: 'core/test-block-2' },
] );
} );

it( 'should create nested blocks', () => {
const template = [
[ 'core/test-block', {}, [ [ 'core/test-block-2' ] ] ],
Expand Down
62 changes: 62 additions & 0 deletions packages/blocks/src/store/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,68 @@ export function getBlockType( state, name ) {
return state.blockTypes[ name ];
}

/**
* Returns the hooked blocks for a given anchor block.
*
* Given an anchor block name, returns an object whose keys are relative positions,
* and whose values are arrays of block names that are hooked to the anchor block
* at that relative position.
*
* @param {Object} state Data state.
* @param {string} blockName Anchor block type name.
*
* @example
* ```js
* import { store as blocksStore } from '@wordpress/blocks';
* import { useSelect } from '@wordpress/data';
*
* const ExampleComponent = () => {
* const hookedBlockNames = useSelect( ( select ) =>
* select( blocksStore ).getHookedBlocks( 'core/navigation' ),
* []
* );
*
* return (
* <ul>
* { Object.keys( hookedBlockNames ).length &&
* Object.keys( hookedBlockNames ).map( ( relativePosition ) => (
* <li key={ relativePosition }>{ relativePosition }>
* <ul>
* { hookedBlockNames[ relativePosition ].map( ( hookedBlock ) => (
* <li key={ hookedBlock }>{ hookedBlock }</li>
* ) ) }
* </ul>
* </li>
* ) ) }
* </ul>
* );
* };
* ```
*
* @return {Object} Lists of hooked block names for each relative position.
*/
export const getHookedBlocks = createSelector(
( state, blockName ) => {
const hookedBlockTypes = getBlockTypes( state ).filter(
( { blockHooks } ) => blockHooks && blockName in blockHooks
);

let hookedBlocks = {};
for ( const blockType of hookedBlockTypes ) {
const relativePosition = blockType.blockHooks[ blockName ];
hookedBlocks = {
...hookedBlocks,
[ relativePosition ]: [
...( hookedBlocks[ relativePosition ] ?? [] ),
blockType.name,
],
};
}
return hookedBlocks;
},
( state ) => [ state.blockTypes ]
);

/**
* Returns block styles by block name.
*
Expand Down
Loading

1 comment on commit 7449074

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flaky tests detected in 7449074.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/7833559465
📝 Reported issues:

Please sign in to comment.