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 a patch format and support linkTarget of core/button for Pattern Overrides #58165

Merged
merged 2 commits into from
Jan 24, 2024
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
19 changes: 17 additions & 2 deletions lib/experimental/block-bindings/sources/pattern.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,23 @@
if ( ! _wp_array_get( $block_instance->attributes, array( 'metadata', 'id' ), false ) ) {
return null;
}
$block_id = $block_instance->attributes['metadata']['id'];
return _wp_array_get( $block_instance->context, array( 'pattern/overrides', $block_id, $attribute_name ), null );
$block_id = $block_instance->attributes['metadata']['id'];
$attribute_override = _wp_array_get( $block_instance->context, array( 'pattern/overrides', $block_id, $attribute_name ), null );
if ( null === $attribute_override ) {
return null;
}
switch ( $attribute_override[0] ) {
case 0: // remove
/**
* TODO: This currently doesn't remove the attribute, but only set it to an empty string.
* It's a temporary solution until the block binding API supports different operations.
*/
return '';
case 1: // replace
return $attribute_override[1];
default:
return null;
}
};
wp_block_bindings_register_source(
'pattern_attributes',
Expand Down
2 changes: 1 addition & 1 deletion lib/experimental/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ function gutenberg_process_block_bindings( $block_content, $block, $block_instan
'core/paragraph' => array( 'content' ),
'core/heading' => array( 'content' ),
'core/image' => array( 'url', 'title', 'alt' ),
'core/button' => array( 'url', 'text' ),
'core/button' => array( 'url', 'text', 'linkTarget' ),
);

// If the block doesn't have the bindings property or isn't one of the allowed block types, return.
Expand Down
56 changes: 48 additions & 8 deletions packages/block-library/src/block/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,26 @@ const useInferredLayout = ( blocks, parentLayout ) => {
}, [ blocks, parentLayout ] );
};

/**
* Enum for patch operations.
* We use integers here to minimize the size of the serialized data.
* This has to be deserialized accordingly on the server side.
* See block-bindings/sources/pattern.php
*/
const PATCH_OPERATIONS = {
/** @type {0} */
Remove: 0,
/** @type {1} */
Replace: 1,
// Other operations are reserved for future use. (e.g. Add)
};

/**
* @typedef {[typeof PATCH_OPERATIONS.Remove]} RemovePatch
* @typedef {[typeof PATCH_OPERATIONS.Replace, unknown]} ReplacePatch
* @typedef {RemovePatch | ReplacePatch} OverridePatch
*/

function applyInitialOverrides( blocks, overrides = {}, defaultValues ) {
return blocks.map( ( block ) => {
const innerBlocks = applyInitialOverrides(
Expand All @@ -104,9 +124,15 @@ function applyInitialOverrides( blocks, overrides = {}, defaultValues ) {
defaultValues[ blockId ] ??= {};
defaultValues[ blockId ][ attributeKey ] =
block.attributes[ attributeKey ];
if ( overrides[ blockId ]?.[ attributeKey ] !== undefined ) {
newAttributes[ attributeKey ] =
overrides[ blockId ][ attributeKey ];
/** @type {OverridePatch} */
const overrideAttribute = overrides[ blockId ]?.[ attributeKey ];
if ( ! overrideAttribute ) {
continue;
}
if ( overrideAttribute[ 0 ] === PATCH_OPERATIONS.Remove ) {
delete newAttributes[ attributeKey ];
} else if ( overrideAttribute[ 0 ] === PATCH_OPERATIONS.Replace ) {
newAttributes[ attributeKey ] = overrideAttribute[ 1 ];
}
}
return {
Expand All @@ -118,13 +144,14 @@ function applyInitialOverrides( blocks, overrides = {}, defaultValues ) {
}

function getOverridesFromBlocks( blocks, defaultValues ) {
/** @type {Record<string, Record<string, unknown>>} */
/** @type {Record<string, Record<string, OverridePatch>>} */
const overrides = {};
for ( const block of blocks ) {
Object.assign(
overrides,
getOverridesFromBlocks( block.innerBlocks, defaultValues )
);
/** @type {string} */
const blockId = block.attributes.metadata?.id;
if ( ! isPartiallySynced( block ) || ! blockId ) continue;
const attributes = getPartiallySyncedAttributes( block );
Expand All @@ -134,10 +161,23 @@ function getOverridesFromBlocks( blocks, defaultValues ) {
defaultValues[ blockId ][ attributeKey ]
) {
overrides[ blockId ] ??= {};
// TODO: We need a way to represent `undefined` in the serialized overrides.
// Also see: https://github.com/WordPress/gutenberg/pull/57249#discussion_r1452987871
overrides[ blockId ][ attributeKey ] =
block.attributes[ attributeKey ];
/**
* Create a patch operation for the binding attribute.
* We use a tuple here to minimize the size of the serialized data.
* The first item is the operation type, the second item is the value if any.
*/
if ( block.attributes[ attributeKey ] === undefined ) {
/** @type {RemovePatch} */
overrides[ blockId ][ attributeKey ] = [
PATCH_OPERATIONS.Remove,
];
} else {
/** @type {ReplacePatch} */
overrides[ blockId ][ attributeKey ] = [
PATCH_OPERATIONS.Replace,
block.attributes[ attributeKey ],
];
}
}
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/patterns/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const PARTIAL_SYNCING_SUPPORTED_BLOCKS = {
'core/button': {
text: __( 'Text' ),
url: __( 'URL' ),
linkTarget: __( 'Link Target' ),
},
'core/image': {
url: __( 'URL' ),
Expand Down
66 changes: 64 additions & 2 deletions test/e2e/specs/editor/various/pattern-overrides.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ test.describe( 'Pattern Overrides', () => {
ref: patternId,
overrides: {
[ editableParagraphId ]: {
content: 'I would word it this way',
content: [ 1, 'I would word it this way' ],
},
},
},
Expand All @@ -187,7 +187,7 @@ test.describe( 'Pattern Overrides', () => {
ref: patternId,
overrides: {
[ editableParagraphId ]: {
content: 'This one is different',
content: [ 1, 'This one is different' ],
},
},
},
Expand Down Expand Up @@ -263,4 +263,66 @@ test.describe( 'Pattern Overrides', () => {
},
] );
} );

test( 'Supports `undefined` attribute values in patterns', async ( {
page,
admin,
editor,
requestUtils,
} ) => {
const buttonId = 'button-id';
const { id } = await requestUtils.createBlock( {
title: 'Pattern with overrides',
content: `<!-- wp:buttons -->
<div class="wp-block-buttons"><!-- wp:button {"metadata":{"id":"${ buttonId }","bindings":{"text":{"source":{"name":"pattern_attributes"}},"url":{"source":{"name":"pattern_attributes"}},"linkTarget":{"source":{"name":"pattern_attributes"}}}}} -->
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button" href="http://wp.org" target="_blank" rel="noreferrer noopener">wp.org</a></div>
<!-- /wp:button --></div>
<!-- /wp:buttons -->`,
status: 'publish',
} );

await admin.createNewPost();

await editor.insertBlock( {
name: 'core/block',
attributes: { ref: id },
} );

await editor.canvas
.getByRole( 'document', { name: 'Block: Button' } )
.getByRole( 'textbox', { name: 'Button text' } )
.focus();

await expect(
page.getByRole( 'link', { name: 'wp.org' } )
).toContainText( 'opens in a new tab' );

const openInNewTabCheckbox = page.getByRole( 'checkbox', {
name: 'Open in new tab',
} );
await expect( openInNewTabCheckbox ).toBeChecked();

await openInNewTabCheckbox.setChecked( false );

await expect.poll( editor.getBlocks ).toMatchObject( [
{
name: 'core/block',
attributes: {
ref: id,
overrides: {
[ buttonId ]: {
linkTarget: [ 0 ],
},
},
},
},
] );

const postId = await editor.publishPost();
await page.goto( `/?p=${ postId }` );

const link = page.getByRole( 'link', { name: 'wp.org' } );
await expect( link ).toHaveAttribute( 'href', 'http://wp.org' );
await expect( link ).toHaveAttribute( 'target', '' );
} );
} );
Loading