-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Changes from 1 commit
78f9601
17b31b8
63c6b6d
46063db
f1f78ae
8121723
ec4daec
35aa0ab
a71ba38
29afc1f
31bf94c
6c60eb3
49b062a
766a762
49286e1
ef29ab3
ba8b618
e695fb4
d9d9fd6
4a4465f
1101667
a95749f
e2a1b8a
9d444a3
e28da53
ae534d8
4ec7315
3a7311d
e013f6d
950aab9
d99ecb7
9ec9bba
7d72a34
4ebdb09
44b51dd
5975f74
10fa4e8
3cfdd44
8c5f574
3866446
33bfb86
d80aba7
6b97321
c122b79
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, | ||
|
@@ -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 | ||
|
@@ -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(); | ||
|
||
|
@@ -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 = | ||
|
@@ -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: | ||
|
@@ -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 ); | ||
} ); | ||
}, [ | ||
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( { | ||
|
@@ -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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this a typo? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ( There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
|
||
} ); | ||
|
||
// 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, | ||
|
@@ -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
|
||
} | ||
}; | ||
|
||
|
There was a problem hiding this comment.
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)