Skip to content

Commit c53b018

Browse files
talldanaaronrobertshawkevin940726michalczaplinskiyouknowriad
authored
Use block naming for marking blocks as overridable in patterns (#59268)
Co-authored-by: talldan <[email protected]> Co-authored-by: aaronrobertshaw <[email protected]> Co-authored-by: kevin940726 <[email protected]> Co-authored-by: michalczaplinski <[email protected]> Co-authored-by: youknowriad <[email protected]> Co-authored-by: gziolo <[email protected]>
1 parent 9957b85 commit c53b018

21 files changed

+412
-213
lines changed

lib/compat/wordpress-6.5/block-bindings/pattern-overrides.php

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,30 @@
1515
* @return mixed The value computed for the source.
1616
*/
1717
function gutenberg_block_bindings_pattern_overrides_callback( $source_attrs, $block_instance, $attribute_name ) {
18-
if ( empty( $block_instance->attributes['metadata']['id'] ) ) {
18+
if ( ! isset( $block_instance->context['pattern/overrides'] ) ) {
1919
return null;
2020
}
21-
$block_id = $block_instance->attributes['metadata']['id'];
22-
return _wp_array_get( $block_instance->context, array( 'pattern/overrides', $block_id, 'values', $attribute_name ), null );
21+
22+
$override_content = $block_instance->context['pattern/overrides'];
23+
24+
// Back compat. Pattern overrides previously used a metadata `id` instead of `name`.
25+
// We check first for the name, and if it exists, use that value.
26+
if ( isset( $block_instance->attributes['metadata']['name'] ) ) {
27+
$metadata_name = $block_instance->attributes['metadata']['name'];
28+
if ( array_key_exists( $metadata_name, $override_content ) ) {
29+
return _wp_array_get( $override_content, array( $metadata_name, $attribute_name ), null );
30+
}
31+
}
32+
33+
// Next check for the `id`.
34+
if ( isset( $block_instance->attributes['metadata']['id'] ) ) {
35+
$metadata_id = $block_instance->attributes['metadata']['id'];
36+
if ( array_key_exists( $metadata_id, $override_content ) ) {
37+
return _wp_array_get( $override_content, array( $metadata_id, $attribute_name ), null );
38+
}
39+
}
40+
41+
return null;
2342
}
2443

2544
/**

package-lock.json

Lines changed: 2 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/block-library/src/block/deprecated.js

Lines changed: 76 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,75 @@
1-
// v1: Migrate and rename the `overrides` attribute to the `content` attribute.
1+
const isObject = ( obj ) =>
2+
typeof obj === 'object' && ! Array.isArray( obj ) && obj !== null;
3+
4+
// v2: Migrate to a more condensed version of the 'content' attribute attribute.
5+
const v2 = {
6+
attributes: {
7+
ref: {
8+
type: 'number',
9+
},
10+
content: {
11+
type: 'object',
12+
},
13+
},
14+
supports: {
15+
customClassName: false,
16+
html: false,
17+
inserter: false,
18+
renaming: false,
19+
},
20+
// Force this deprecation to run whenever there's a values sub-property that's an object.
21+
//
22+
// This could fail in the future if a block ever has binding to a `values` attribute.
23+
// Some extra protection is added to ensure `values` is an object, but this only reduces
24+
// the likelihood, it doesn't solve it completely.
25+
isEligible( { content } ) {
26+
return (
27+
!! content &&
28+
Object.keys( content ).every(
29+
( contentKey ) =>
30+
content[ contentKey ].values &&
31+
isObject( content[ contentKey ].values )
32+
)
33+
);
34+
},
35+
/*
36+
* Old attribute format:
37+
* content: {
38+
* "V98q_x": {
39+
* // The attribute values are now stored as a 'values' sub-property.
40+
* values: { content: 'My content value' },
41+
* // ... additional metadata, like the block name can be stored here.
42+
* }
43+
* }
44+
*
45+
* New attribute format:
46+
* content: {
47+
* "V98q_x": {
48+
* content: 'My content value',
49+
* }
50+
* }
51+
*/
52+
migrate( attributes ) {
53+
const { content, ...retainedAttributes } = attributes;
54+
55+
if ( content && Object.keys( content ).length ) {
56+
const updatedContent = { ...content };
57+
58+
for ( const contentKey in content ) {
59+
updatedContent[ contentKey ] = content[ contentKey ].values;
60+
}
61+
62+
return {
63+
...retainedAttributes,
64+
content: updatedContent,
65+
};
66+
}
67+
68+
return attributes;
69+
},
70+
};
71+
72+
// v1: Rename the `overrides` attribute to the `content` attribute.
273
const v1 = {
374
attributes: {
475
ref: {
@@ -23,16 +94,12 @@ const v1 = {
2394
* overrides: {
2495
* // An key is an id that represents a block.
2596
* // The values are the attribute values of the block.
26-
* "V98q_x": { content: 'dwefwefwefwe' }
97+
* "V98q_x": { content: 'My content value' }
2798
* }
2899
*
29100
* New attribute format:
30101
* content: {
31-
* "V98q_x": {
32-
* // The attribute values are now stored as a 'values' sub-property.
33-
* values: { content: 'dwefwefwefwe' },
34-
* // ... additional metadata, like the block name can be stored here.
35-
* }
102+
* "V98q_x": { content: 'My content value' }
36103
* }
37104
*
38105
*/
@@ -42,9 +109,7 @@ const v1 = {
42109
const content = {};
43110

44111
Object.keys( overrides ).forEach( ( id ) => {
45-
content[ id ] = {
46-
values: overrides[ id ],
47-
};
112+
content[ id ] = overrides[ id ];
48113
} );
49114

50115
return {
@@ -54,4 +119,4 @@ const v1 = {
54119
},
55120
};
56121

57-
export default [ v1 ];
122+
export default [ v2, v1 ];

packages/block-library/src/block/edit.js

Lines changed: 64 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,25 @@ const { PARTIAL_SYNCING_SUPPORTED_BLOCKS } = unlock( patternsPrivateApis );
4242

4343
const fullAlignments = [ 'full', 'wide', 'left', 'right' ];
4444

45+
function getLegacyIdMap( blocks, content, nameCount = {} ) {
46+
let idToClientIdMap = {};
47+
for ( const block of blocks ) {
48+
if ( block?.innerBlocks?.length ) {
49+
idToClientIdMap = {
50+
...idToClientIdMap,
51+
...getLegacyIdMap( block.innerBlocks, content, nameCount ),
52+
};
53+
}
54+
55+
const id = block.attributes.metadata?.id;
56+
const clientId = block.clientId;
57+
if ( id && content?.[ id ] ) {
58+
idToClientIdMap[ clientId ] = id;
59+
}
60+
}
61+
return idToClientIdMap;
62+
}
63+
4564
const useInferredLayout = ( blocks, parentLayout ) => {
4665
const initialInferredAlignmentRef = useRef();
4766

@@ -101,25 +120,31 @@ function getOverridableAttributes( block ) {
101120
function applyInitialContentValuesToInnerBlocks(
102121
blocks,
103122
content = {},
104-
defaultValues
123+
defaultValues,
124+
legacyIdMap
105125
) {
106126
return blocks.map( ( block ) => {
107127
const innerBlocks = applyInitialContentValuesToInnerBlocks(
108128
block.innerBlocks,
109129
content,
110-
defaultValues
130+
defaultValues,
131+
legacyIdMap
111132
);
112-
const blockId = block.attributes.metadata?.id;
113-
if ( ! hasOverridableAttributes( block ) || ! blockId )
133+
const metadataName =
134+
legacyIdMap?.[ block.clientId ] ?? block.attributes.metadata?.name;
135+
136+
if ( ! metadataName || ! hasOverridableAttributes( block ) ) {
114137
return { ...block, innerBlocks };
138+
}
139+
115140
const attributes = getOverridableAttributes( block );
116141
const newAttributes = { ...block.attributes };
117142
for ( const attributeKey of attributes ) {
118-
defaultValues[ blockId ] ??= {};
119-
defaultValues[ blockId ][ attributeKey ] =
143+
defaultValues[ metadataName ] ??= {};
144+
defaultValues[ metadataName ][ attributeKey ] =
120145
block.attributes[ attributeKey ];
121146

122-
const contentValues = content[ blockId ]?.values;
147+
const contentValues = content[ metadataName ];
123148
if ( contentValues?.[ attributeKey ] !== undefined ) {
124149
newAttributes[ attributeKey ] = contentValues[ attributeKey ];
125150
}
@@ -142,29 +167,40 @@ function isAttributeEqual( attribute1, attribute2 ) {
142167
return attribute1 === attribute2;
143168
}
144169

145-
function getContentValuesFromInnerBlocks( blocks, defaultValues ) {
170+
function getContentValuesFromInnerBlocks( blocks, defaultValues, legacyIdMap ) {
146171
/** @type {Record<string, { values: Record<string, unknown>}>} */
147172
const content = {};
148173
for ( const block of blocks ) {
149174
if ( block.name === patternBlockName ) continue;
150-
Object.assign(
151-
content,
152-
getContentValuesFromInnerBlocks( block.innerBlocks, defaultValues )
153-
);
154-
const blockId = block.attributes.metadata?.id;
155-
if ( ! hasOverridableAttributes( block ) || ! blockId ) continue;
175+
if ( block.innerBlocks.length ) {
176+
Object.assign(
177+
content,
178+
getContentValuesFromInnerBlocks(
179+
block.innerBlocks,
180+
defaultValues,
181+
legacyIdMap
182+
)
183+
);
184+
}
185+
const metadataName =
186+
legacyIdMap?.[ block.clientId ] ?? block.attributes.metadata?.name;
187+
if ( ! metadataName || ! hasOverridableAttributes( block ) ) {
188+
continue;
189+
}
190+
156191
const attributes = getOverridableAttributes( block );
192+
157193
for ( const attributeKey of attributes ) {
158194
if (
159195
! isAttributeEqual(
160196
block.attributes[ attributeKey ],
161-
defaultValues[ blockId ][ attributeKey ]
197+
defaultValues?.[ metadataName ]?.[ attributeKey ]
162198
)
163199
) {
164-
content[ blockId ] ??= { values: {}, blockName: block.name };
200+
content[ metadataName ] ??= {};
165201
// TODO: We need a way to represent `undefined` in the serialized overrides.
166202
// Also see: https://github.com/WordPress/gutenberg/pull/57249#discussion_r1452987871
167-
content[ blockId ].values[ attributeKey ] =
203+
content[ metadataName ][ attributeKey ] =
168204
block.attributes[ attributeKey ] === undefined
169205
? // TODO: We use an empty string to represent undefined for now until
170206
// we support a richer format for overrides and the block binding API.
@@ -278,8 +314,15 @@ export default function ReusableBlockEdit( {
278314
[ editedRecord.blocks, editedRecord.content ]
279315
);
280316

317+
const legacyIdMap = useRef( {} );
318+
281319
// Apply the initial overrides from the pattern block to the inner blocks.
282320
useEffect( () => {
321+
// Build a map of clientIds to the old nano id system to provide back compat.
322+
legacyIdMap.current = getLegacyIdMap(
323+
initialBlocks,
324+
initialContent.current
325+
);
283326
defaultContent.current = {};
284327
const originalEditingMode = getBlockEditingMode( patternClientId );
285328
// Replace the contents of the blocks with the overrides.
@@ -291,7 +334,8 @@ export default function ReusableBlockEdit( {
291334
applyInitialContentValuesToInnerBlocks(
292335
initialBlocks,
293336
initialContent.current,
294-
defaultContent.current
337+
defaultContent.current,
338+
legacyIdMap.current
295339
)
296340
);
297341
} );
@@ -343,7 +387,8 @@ export default function ReusableBlockEdit( {
343387
setAttributes( {
344388
content: getContentValuesFromInnerBlocks(
345389
blocks,
346-
defaultContent.current
390+
defaultContent.current,
391+
legacyIdMap.current
347392
),
348393
} );
349394
} );

packages/block-library/src/block/index.php

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,26 +48,35 @@ function render_block_core_block( $attributes ) {
4848
$content = $wp_embed->run_shortcode( $reusable_block->post_content );
4949
$content = $wp_embed->autoembed( $content );
5050

51-
// Back compat, the content attribute was previously named overrides and
52-
// had a slightly different format. For blocks that have not been migrated,
53-
// also convert the format here so that the provided `pattern/overrides`
54-
// context is correct.
55-
if ( isset( $attributes['overrides'] ) && ! isset( $attributes['content'] ) ) {
56-
$migrated_content = array();
57-
foreach ( $attributes['overrides'] as $id => $values ) {
58-
$migrated_content[ $id ] = array(
59-
'values' => $values,
60-
);
51+
// Back compat.
52+
// For blocks that have not been migrated in the editor, add some back compat
53+
// so that front-end rendering continues to work.
54+
55+
// This matches the `v2` deprecation. Removes the inner `values` property
56+
// from every item.
57+
if ( isset( $attributes['content'] ) ) {
58+
foreach ( $attributes['content'] as &$content_data ) {
59+
if ( isset( $content_data['values'] ) ) {
60+
$is_assoc_array = is_array( $content_data['values'] ) && ! wp_is_numeric_array( $content_data['values'] );
61+
62+
if ( $is_assoc_array ) {
63+
$content_data = $content_data['values'];
64+
}
65+
}
6166
}
62-
$attributes['content'] = $migrated_content;
6367
}
64-
$has_pattern_overrides = isset( $attributes['content'] );
68+
69+
// This matches the `v1` deprecation. Rename `overrides` to `content`.
70+
if ( isset( $attributes['overrides'] ) && ! isset( $attributes['content'] ) ) {
71+
$attributes['content'] = $attributes['overrides'];
72+
}
6573

6674
/**
6775
* We set the `pattern/overrides` context through the `render_block_context`
6876
* filter so that it is available when a pattern's inner blocks are
6977
* rendering via do_blocks given it only receives the inner content.
7078
*/
79+
$has_pattern_overrides = isset( $attributes['content'] );
7180
if ( $has_pattern_overrides ) {
7281
$filter_block_context = static function ( $context ) use ( $attributes ) {
7382
$context['pattern/overrides'] = $attributes['content'];

packages/editor/src/hooks/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
*/
44
import './custom-sources-backwards-compatibility';
55
import './default-autocompleters';
6-
import './pattern-partial-syncing';
6+
import './pattern-overrides';

0 commit comments

Comments
 (0)