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

Pattern overrides: use block binding editing API #60721

Merged
merged 44 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
78f9601
Pattern overrides: use block binding editing API
ellatrix May 9, 2024
17b31b8
Update only the needed attributes with bindings
SantosGuillamot May 9, 2024
63c6b6d
Keep blocks as normal when editing pattern
SantosGuillamot May 9, 2024
46063db
Use inner blocks for set edit mode
SantosGuillamot May 9, 2024
f1f78ae
Change indiviual reset logic
SantosGuillamot May 9, 2024
8121723
Move variable after early return
SantosGuillamot May 9, 2024
ec4daec
Add inline comment for bindings setAttributes
SantosGuillamot May 9, 2024
35aa0ab
Change variable to isOverriden
SantosGuillamot May 9, 2024
a71ba38
Change missing isOverriden
SantosGuillamot May 9, 2024
29afc1f
Fix detaching of synced patterns
SantosGuillamot May 9, 2024
31bf94c
Sync values of bound attributes
SantosGuillamot May 9, 2024
6c60eb3
Adapt synced pattern tests
SantosGuillamot May 9, 2024
49b062a
Use overriden values when detaching pattern
SantosGuillamot May 9, 2024
766a762
Adapt pattern-overrides tests
SantosGuillamot May 9, 2024
49286e1
Add workaround for pattern overrides
SantosGuillamot May 9, 2024
ef29ab3
Don't update non-bound attributes when using patterns
SantosGuillamot May 9, 2024
ba8b618
Add test for blocks with a shared name
SantosGuillamot May 9, 2024
e695fb4
Support button rel in bindings editor logic
SantosGuillamot May 9, 2024
d9d9fd6
Solve undo/redo issues
SantosGuillamot May 9, 2024
4a4465f
Stop using syncDerivedUpdates
SantosGuillamot May 9, 2024
1101667
Ensure change is marked as persistent in pattern overrides
SantosGuillamot May 9, 2024
a95749f
Try to solve undo/redo issues
SantosGuillamot May 9, 2024
e2a1b8a
Manage undefined attrs in pattern overrides
SantosGuillamot May 9, 2024
9d444a3
Remove `content` pattern property if empty
SantosGuillamot May 9, 2024
e28da53
Remove pattern-overrides specific code from `withBlockBindingSupport`…
talldan May 13, 2024
ae534d8
Fix persistent issues with setValues API
ellatrix May 13, 2024
4ec7315
Fall back to ''
ellatrix May 13, 2024
3a7311d
Make sure any previous changes are persisted before resetting.
ellatrix May 13, 2024
e013f6d
Add undo/redo test for resetting
kevin940726 May 13, 2024
950aab9
Fix test... 🤦
kevin940726 May 13, 2024
d99ecb7
Fix remaining undo test
ellatrix May 13, 2024
9ec9bba
Fix individual reset
ellatrix May 13, 2024
7d72a34
Add test for undoing loses the focus
kevin940726 May 13, 2024
4ebdb09
Fix reset button test
ellatrix May 13, 2024
44b51dd
Simplify pattern synced blocks test
SantosGuillamot May 13, 2024
5975f74
Remove focus test
SantosGuillamot May 13, 2024
10fa4e8
Use getBlockParentsByBlockName
SantosGuillamot May 14, 2024
3cfdd44
Add early return if blockName doesn't exist
SantosGuillamot May 14, 2024
8c5f574
Add comment for undefined values
SantosGuillamot May 14, 2024
3866446
Remove comment typo
SantosGuillamot May 14, 2024
33bfb86
Remove unnecessary includeHidden from tests
SantosGuillamot May 14, 2024
d80aba7
Check the frontend in patterns test
SantosGuillamot May 14, 2024
6b97321
Check frontend in updated pattern
SantosGuillamot May 14, 2024
c122b79
Stop using `getPatternRecord`
SantosGuillamot May 14, 2024
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
Next Next commit
Pattern overrides: use block binding editing API
  • Loading branch information
ellatrix authored and SantosGuillamot committed May 9, 2024
commit 78f960185df3dc1c3d26f084f9375b4431444fab
276 changes: 26 additions & 250 deletions packages/block-library/src/block/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ import clsx from 'clsx';
/**
* WordPress dependencies
*/
import { useRegistry, useSelect, useDispatch } from '@wordpress/data';
import { useSelect, useDispatch } from '@wordpress/data';
import { useRef, useMemo, useEffect } from '@wordpress/element';
import { useEntityRecord, store as coreStore } from '@wordpress/core-data';
import {
useEntityRecord,
store as coreStore,
useEntityBlockEditor,
} from '@wordpress/core-data';
import {
Placeholder,
Spinner,
Expand All @@ -20,16 +24,14 @@ import {
useInnerBlocksProps,
RecursionProvider,
useHasRecursion,
InnerBlocks,
useBlockProps,
Warning,
privateApis as blockEditorPrivateApis,
store as blockEditorStore,
BlockControls,
} from '@wordpress/block-editor';
import { privateApis as patternsPrivateApis } from '@wordpress/patterns';
import { parse, cloneBlock, store as blocksStore } from '@wordpress/blocks';
import { RichTextData } from '@wordpress/rich-text';
import { store as blocksStore } from '@wordpress/blocks';

/**
* Internal dependencies
Expand All @@ -42,25 +44,6 @@ const { isOverridableBlock } = 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 @@ -99,112 +82,6 @@ function hasOverridableBlocks( blocks ) {
} );
}

function getOverridableAttributes( block ) {
return Object.entries( block.attributes.metadata.bindings )
.filter(
( [ , binding ] ) => binding.source === 'core/pattern-overrides'
)
.map( ( [ attributeKey ] ) => attributeKey );
}

function applyInitialContentValuesToInnerBlocks(
blocks,
content = {},
defaultValues,
legacyIdMap
) {
return blocks.map( ( block ) => {
const innerBlocks = applyInitialContentValuesToInnerBlocks(
block.innerBlocks,
content,
defaultValues,
legacyIdMap
);
const metadataName =
legacyIdMap?.[ block.clientId ] ?? block.attributes.metadata?.name;

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

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

const contentValues = content[ metadataName ];
if ( contentValues?.[ attributeKey ] !== undefined ) {
newAttributes[ attributeKey ] = contentValues[ attributeKey ];
}
}
return {
...block,
attributes: newAttributes,
innerBlocks,
};
} );
}

function isAttributeEqual( attribute1, attribute2 ) {
if (
attribute1 instanceof RichTextData &&
attribute2 instanceof RichTextData
) {
return attribute1.toString() === attribute2.toString();
}
return attribute1 === attribute2;
}

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

const attributes = getOverridableAttributes( block );

for ( const attributeKey of attributes ) {
if (
! isAttributeEqual(
block.attributes[ attributeKey ],
defaultValues?.[ metadataName ]?.[ attributeKey ]
)
) {
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[ 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.
// Currently only the `linkTarget` attribute of `core/button` is affected.
''
: block.attributes[ attributeKey ];
}
}
}
return Object.keys( content ).length > 0 ? content : undefined;
}

function setBlockEditMode( setEditMode, blocks, mode ) {
blocks.forEach( ( block ) => {
const editMode =
Expand Down Expand Up @@ -257,51 +134,35 @@ function ReusableBlockEdit( {
clientId: patternClientId,
setAttributes,
} ) {
const registry = useRegistry();
const { record, editedRecord, hasResolved } = useEntityRecord(
const { record, hasResolved } = useEntityRecord(
'postType',
'wp_block',
ref
);
const [ blocks, onInput, onChange ] = useEntityBlockEditor(
'postType',
'wp_block',
{ id: ref }
);
const isMissing = hasResolved && ! record;

// The value of the `content` attribute, stored in a `ref` to avoid triggering the effect
// that runs `applyInitialContentValuesToInnerBlocks` unnecessarily.
const contentRef = useRef( content );

// The default content values from the original pattern for overridable attributes.
// Set by the `applyInitialContentValuesToInnerBlocks` function.
const defaultContent = useRef( {} );

const {
replaceInnerBlocks,
__unstableMarkNextChangeAsNotPersistent,
setBlockEditingMode,
} = useDispatch( blockEditorStore );
const { syncDerivedUpdates } = unlock( useDispatch( blockEditorStore ) );
const { setBlockEditingMode } = useDispatch( blockEditorStore );

const {
innerBlocks,
userCanEdit,
getBlockEditingMode,
onNavigateToEntityRecord,
editingMode,
hasPatternOverridesSource,
} = useSelect(
( select ) => {
const { canUser } = select( coreStore );
const {
getBlocks,
getSettings,
getBlockEditingMode: _getBlockEditingMode,
} = select( blockEditorStore );
const { getSettings, getBlockEditingMode: _getBlockEditingMode } =
select( blockEditorStore );
const { getBlockBindingsSource } = unlock( select( blocksStore ) );
const blocks = getBlocks( patternClientId );
const canEdit = canUser( 'update', 'blocks', ref );

// For editing link to the site editor if the theme and user permissions support it.
return {
innerBlocks: blocks,
userCanEdit: canEdit,
getBlockEditingMode: _getBlockEditingMode,
onNavigateToEntityRecord:
Expand All @@ -319,78 +180,25 @@ function ReusableBlockEdit( {
useEffect( () => {
setBlockEditMode(
setBlockEditingMode,
innerBlocks,
blocks,
// Disable editing if the pattern itself is disabled.
editingMode === 'disabled' || ! hasPatternOverridesSource
? 'disabled'
: undefined
);
}, [
editingMode,
innerBlocks,
blocks,
setBlockEditingMode,
hasPatternOverridesSource,
] );

const canOverrideBlocks = useMemo(
() => hasPatternOverridesSource && hasOverridableBlocks( innerBlocks ),
[ hasPatternOverridesSource, innerBlocks ]
() => hasPatternOverridesSource && hasOverridableBlocks( blocks ),
[ hasPatternOverridesSource, blocks ]
);

const initialBlocks = useMemo(
() =>
// Clone the blocks to generate new client IDs.
editedRecord.blocks?.map( ( block ) => cloneBlock( block ) ) ??
( editedRecord.content && typeof editedRecord.content !== 'function'
? parse( editedRecord.content )
: [] ),
[ 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,
contentRef.current
);
defaultContent.current = {};
const originalEditingMode = getBlockEditingMode( patternClientId );
// Replace the contents of the blocks with the overrides.
registry.batch( () => {
setBlockEditingMode( patternClientId, 'default' );
syncDerivedUpdates( () => {
const blocks = hasPatternOverridesSource
? applyInitialContentValuesToInnerBlocks(
initialBlocks,
contentRef.current,
defaultContent.current,
legacyIdMap.current
)
: initialBlocks;

replaceInnerBlocks( patternClientId, blocks );
} );
setBlockEditingMode( patternClientId, originalEditingMode );
Copy link
Contributor

Choose a reason for hiding this comment

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

The fact that we were making these calls here in trunk could have "hidden" the race conditions. It doesn't mean the race conditions didn't exist on trunk, they did, but they were hidden or maybe they resulted in different bugs (like things being editable in the template while in "template-lock" mode)

} );
}, [
hasPatternOverridesSource,
__unstableMarkNextChangeAsNotPersistent,
patternClientId,
initialBlocks,
replaceInnerBlocks,
registry,
getBlockEditingMode,
setBlockEditingMode,
syncDerivedUpdates,
] );

const { alignment, layout } = useInferredLayout(
innerBlocks,
parentLayout
);
const { alignment, layout } = useInferredLayout( blocks, parentLayout );
const layoutClasses = useLayoutClasses( { layout }, name );

const blockProps = useBlockProps( {
Expand All @@ -404,44 +212,12 @@ function ReusableBlockEdit( {
const innerBlocksProps = useInnerBlocksProps( blockProps, {
templateLock: 'contentOnly',
layout,
renderAppender: innerBlocks?.length
? undefined
: InnerBlocks.ButtonBlockAppender,
value: blocks,
onInput,
onChange,
renderAppender: blocks?.length ? undefined : blocks.ButtonBlockAppender,
Copy link
Member Author

Choose a reason for hiding this comment

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

Is this a typo?

Copy link
Member Author

Choose a reason for hiding this comment

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

(blocks.ButtonBlockAppender will never exist)

Copy link
Contributor

Choose a reason for hiding this comment

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

It seems that change has been there since the first commit, so I assume we just replaced innerBlocks by blocks. Maybe we should go back to the previous logic?

renderAppender: innerBlocks?.length
    ? undefined
    : InnerBlocks.ButtonBlockAppender,

} );

// Sync the `content` attribute from the updated blocks to the pattern block.
// `syncDerivedUpdates` is used here to avoid creating an additional undo level.
useEffect( () => {
if ( ! hasPatternOverridesSource ) {
return;
}
const { getBlocks } = registry.select( blockEditorStore );
let prevBlocks = getBlocks( patternClientId );
return registry.subscribe( () => {
const blocks = getBlocks( patternClientId );
if ( blocks !== prevBlocks ) {
prevBlocks = blocks;
syncDerivedUpdates( () => {
const updatedContent = getContentValuesFromInnerBlocks(
blocks,
defaultContent.current,
legacyIdMap.current
);
setAttributes( {
content: updatedContent,
} );
contentRef.current = updatedContent;
} );
}
}, blockEditorStore );
}, [
hasPatternOverridesSource,
syncDerivedUpdates,
patternClientId,
registry,
setAttributes,
] );

const handleEditOriginal = () => {
onNavigateToEntityRecord( {
postId: ref,
Expand All @@ -451,7 +227,7 @@ function ReusableBlockEdit( {

const resetContent = () => {
if ( content ) {
replaceInnerBlocks( patternClientId, initialBlocks );
setAttributes( { content: undefined } );
ellatrix marked this conversation as resolved.
Show resolved Hide resolved
}
};

Expand Down
Loading