Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use block naming for marking blocks as overridable in patterns #59268

Merged
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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( () => {
talldan marked this conversation as resolved.
Show resolved Hide resolved
// 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';