Skip to content

Commit

Permalink
Use block naming for marking blocks as overridable in patterns (#59268)
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
7 people authored Mar 1, 2024
1 parent 9957b85 commit c53b018
Show file tree
Hide file tree
Showing 21 changed files with 412 additions and 213 deletions.
25 changes: 22 additions & 3 deletions lib/compat/wordpress-6.5/block-bindings/pattern-overrides.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,30 @@
* @return mixed The value computed for the source.
*/
function gutenberg_block_bindings_pattern_overrides_callback( $source_attrs, $block_instance, $attribute_name ) {
if ( empty( $block_instance->attributes['metadata']['id'] ) ) {
if ( ! isset( $block_instance->context['pattern/overrides'] ) ) {
return null;
}
$block_id = $block_instance->attributes['metadata']['id'];
return _wp_array_get( $block_instance->context, array( 'pattern/overrides', $block_id, 'values', $attribute_name ), null );

$override_content = $block_instance->context['pattern/overrides'];

// Back compat. Pattern overrides previously used a metadata `id` instead of `name`.
// We check first for the name, and if it exists, use that value.
if ( isset( $block_instance->attributes['metadata']['name'] ) ) {
$metadata_name = $block_instance->attributes['metadata']['name'];
if ( array_key_exists( $metadata_name, $override_content ) ) {
return _wp_array_get( $override_content, array( $metadata_name, $attribute_name ), null );
}
}

// Next check for the `id`.
if ( isset( $block_instance->attributes['metadata']['id'] ) ) {
$metadata_id = $block_instance->attributes['metadata']['id'];
if ( array_key_exists( $metadata_id, $override_content ) ) {
return _wp_array_get( $override_content, array( $metadata_id, $attribute_name ), null );
}
}

return null;
}

/**
Expand Down
6 changes: 2 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

87 changes: 76 additions & 11 deletions packages/block-library/src/block/deprecated.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,75 @@
// v1: Migrate and rename the `overrides` attribute to the `content` attribute.
const isObject = ( obj ) =>
typeof obj === 'object' && ! Array.isArray( obj ) && obj !== null;

// v2: Migrate to a more condensed version of the 'content' attribute attribute.
const v2 = {
attributes: {
ref: {
type: 'number',
},
content: {
type: 'object',
},
},
supports: {
customClassName: false,
html: false,
inserter: false,
renaming: false,
},
// Force this deprecation to run whenever there's a values sub-property that's an object.
//
// This could fail in the future if a block ever has binding to a `values` attribute.
// Some extra protection is added to ensure `values` is an object, but this only reduces
// the likelihood, it doesn't solve it completely.
isEligible( { content } ) {
return (
!! content &&
Object.keys( content ).every(
( contentKey ) =>
content[ contentKey ].values &&
isObject( content[ contentKey ].values )
)
);
},
/*
* Old attribute format:
* content: {
* "V98q_x": {
* // The attribute values are now stored as a 'values' sub-property.
* values: { content: 'My content value' },
* // ... additional metadata, like the block name can be stored here.
* }
* }
*
* New attribute format:
* content: {
* "V98q_x": {
* content: 'My content value',
* }
* }
*/
migrate( attributes ) {
const { content, ...retainedAttributes } = attributes;

if ( content && Object.keys( content ).length ) {
const updatedContent = { ...content };

for ( const contentKey in content ) {
updatedContent[ contentKey ] = content[ contentKey ].values;
}

return {
...retainedAttributes,
content: updatedContent,
};
}

return attributes;
},
};

// v1: Rename the `overrides` attribute to the `content` attribute.
const v1 = {
attributes: {
ref: {
Expand All @@ -23,16 +94,12 @@ const v1 = {
* overrides: {
* // An key is an id that represents a block.
* // The values are the attribute values of the block.
* "V98q_x": { content: 'dwefwefwefwe' }
* "V98q_x": { content: 'My content value' }
* }
*
* New attribute format:
* content: {
* "V98q_x": {
* // The attribute values are now stored as a 'values' sub-property.
* values: { content: 'dwefwefwefwe' },
* // ... additional metadata, like the block name can be stored here.
* }
* "V98q_x": { content: 'My content value' }
* }
*
*/
Expand All @@ -42,9 +109,7 @@ const v1 = {
const content = {};

Object.keys( overrides ).forEach( ( id ) => {
content[ id ] = {
values: overrides[ id ],
};
content[ id ] = overrides[ id ];
} );

return {
Expand All @@ -54,4 +119,4 @@ const v1 = {
},
};

export default [ v1 ];
export default [ v2, v1 ];
83 changes: 64 additions & 19 deletions packages/block-library/src/block/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,25 @@ const { PARTIAL_SYNCING_SUPPORTED_BLOCKS } = unlock( patternsPrivateApis );

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

function getLegacyIdMap( blocks, content, nameCount = {} ) {
let idToClientIdMap = {};
for ( const block of blocks ) {
if ( block?.innerBlocks?.length ) {
idToClientIdMap = {
...idToClientIdMap,
...getLegacyIdMap( block.innerBlocks, content, nameCount ),
};
}

const id = block.attributes.metadata?.id;
const clientId = block.clientId;
if ( id && content?.[ id ] ) {
idToClientIdMap[ clientId ] = id;
}
}
return idToClientIdMap;
}

const useInferredLayout = ( blocks, parentLayout ) => {
const initialInferredAlignmentRef = useRef();

Expand Down Expand Up @@ -101,25 +120,31 @@ function getOverridableAttributes( block ) {
function applyInitialContentValuesToInnerBlocks(
blocks,
content = {},
defaultValues
defaultValues,
legacyIdMap
) {
return blocks.map( ( block ) => {
const innerBlocks = applyInitialContentValuesToInnerBlocks(
block.innerBlocks,
content,
defaultValues
defaultValues,
legacyIdMap
);
const blockId = block.attributes.metadata?.id;
if ( ! hasOverridableAttributes( block ) || ! blockId )
const metadataName =
legacyIdMap?.[ block.clientId ] ?? block.attributes.metadata?.name;

if ( ! metadataName || ! hasOverridableAttributes( block ) ) {
return { ...block, innerBlocks };
}

const attributes = getOverridableAttributes( block );
const newAttributes = { ...block.attributes };
for ( const attributeKey of attributes ) {
defaultValues[ blockId ] ??= {};
defaultValues[ blockId ][ attributeKey ] =
defaultValues[ metadataName ] ??= {};
defaultValues[ metadataName ][ attributeKey ] =
block.attributes[ attributeKey ];

const contentValues = content[ blockId ]?.values;
const contentValues = content[ metadataName ];
if ( contentValues?.[ attributeKey ] !== undefined ) {
newAttributes[ attributeKey ] = contentValues[ attributeKey ];
}
Expand All @@ -142,29 +167,40 @@ function isAttributeEqual( attribute1, attribute2 ) {
return attribute1 === attribute2;
}

function getContentValuesFromInnerBlocks( blocks, defaultValues ) {
function getContentValuesFromInnerBlocks( blocks, defaultValues, legacyIdMap ) {
/** @type {Record<string, { values: Record<string, unknown>}>} */
const content = {};
for ( const block of blocks ) {
if ( block.name === patternBlockName ) continue;
Object.assign(
content,
getContentValuesFromInnerBlocks( block.innerBlocks, defaultValues )
);
const blockId = block.attributes.metadata?.id;
if ( ! hasOverridableAttributes( block ) || ! blockId ) continue;
if ( block.innerBlocks.length ) {
Object.assign(
content,
getContentValuesFromInnerBlocks(
block.innerBlocks,
defaultValues,
legacyIdMap
)
);
}
const metadataName =
legacyIdMap?.[ block.clientId ] ?? block.attributes.metadata?.name;
if ( ! metadataName || ! hasOverridableAttributes( block ) ) {
continue;
}

const attributes = getOverridableAttributes( block );

for ( const attributeKey of attributes ) {
if (
! isAttributeEqual(
block.attributes[ attributeKey ],
defaultValues[ blockId ][ attributeKey ]
defaultValues?.[ metadataName ]?.[ attributeKey ]
)
) {
content[ blockId ] ??= { values: {}, blockName: block.name };
content[ metadataName ] ??= {};
// TODO: We need a way to represent `undefined` in the serialized overrides.
// Also see: https://github.com/WordPress/gutenberg/pull/57249#discussion_r1452987871
content[ blockId ].values[ attributeKey ] =
content[ metadataName ][ attributeKey ] =
block.attributes[ attributeKey ] === undefined
? // TODO: We use an empty string to represent undefined for now until
// we support a richer format for overrides and the block binding API.
Expand Down Expand Up @@ -278,8 +314,15 @@ export default function ReusableBlockEdit( {
[ editedRecord.blocks, editedRecord.content ]
);

const legacyIdMap = useRef( {} );

// Apply the initial overrides from the pattern block to the inner blocks.
useEffect( () => {
// Build a map of clientIds to the old nano id system to provide back compat.
legacyIdMap.current = getLegacyIdMap(
initialBlocks,
initialContent.current
);
defaultContent.current = {};
const originalEditingMode = getBlockEditingMode( patternClientId );
// Replace the contents of the blocks with the overrides.
Expand All @@ -291,7 +334,8 @@ export default function ReusableBlockEdit( {
applyInitialContentValuesToInnerBlocks(
initialBlocks,
initialContent.current,
defaultContent.current
defaultContent.current,
legacyIdMap.current
)
);
} );
Expand Down Expand Up @@ -343,7 +387,8 @@ export default function ReusableBlockEdit( {
setAttributes( {
content: getContentValuesFromInnerBlocks(
blocks,
defaultContent.current
defaultContent.current,
legacyIdMap.current
),
} );
} );
Expand Down
33 changes: 21 additions & 12 deletions packages/block-library/src/block/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,26 +48,35 @@ function render_block_core_block( $attributes ) {
$content = $wp_embed->run_shortcode( $reusable_block->post_content );
$content = $wp_embed->autoembed( $content );

// Back compat, the content attribute was previously named overrides and
// had a slightly different format. For blocks that have not been migrated,
// also convert the format here so that the provided `pattern/overrides`
// context is correct.
if ( isset( $attributes['overrides'] ) && ! isset( $attributes['content'] ) ) {
$migrated_content = array();
foreach ( $attributes['overrides'] as $id => $values ) {
$migrated_content[ $id ] = array(
'values' => $values,
);
// Back compat.
// For blocks that have not been migrated in the editor, add some back compat
// so that front-end rendering continues to work.

// This matches the `v2` deprecation. Removes the inner `values` property
// from every item.
if ( isset( $attributes['content'] ) ) {
foreach ( $attributes['content'] as &$content_data ) {
if ( isset( $content_data['values'] ) ) {
$is_assoc_array = is_array( $content_data['values'] ) && ! wp_is_numeric_array( $content_data['values'] );

if ( $is_assoc_array ) {
$content_data = $content_data['values'];
}
}
}
$attributes['content'] = $migrated_content;
}
$has_pattern_overrides = isset( $attributes['content'] );

// This matches the `v1` deprecation. Rename `overrides` to `content`.
if ( isset( $attributes['overrides'] ) && ! isset( $attributes['content'] ) ) {
$attributes['content'] = $attributes['overrides'];
}

/**
* We set the `pattern/overrides` context through the `render_block_context`
* filter so that it is available when a pattern's inner blocks are
* rendering via do_blocks given it only receives the inner content.
*/
$has_pattern_overrides = isset( $attributes['content'] );
if ( $has_pattern_overrides ) {
$filter_block_context = static function ( $context ) use ( $attributes ) {
$context['pattern/overrides'] = $attributes['content'];
Expand Down
2 changes: 1 addition & 1 deletion packages/editor/src/hooks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
*/
import './custom-sources-backwards-compatibility';
import './default-autocompleters';
import './pattern-partial-syncing';
import './pattern-overrides';
Loading

0 comments on commit c53b018

Please sign in to comment.