Skip to content

Commit

Permalink
Fix broken undo history stack for Pattern Overrides
Browse files Browse the repository at this point in the history
  • Loading branch information
kevin940726 committed Dec 15, 2023
1 parent d9ea8d0 commit 1789027
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 31 deletions.
2 changes: 2 additions & 0 deletions packages/block-editor/src/private-apis.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
import { usesContextKey } from './components/rich-text/format-edit';
import { ExperimentalBlockCanvas } from './components/block-canvas';
import { getDuotoneFilter } from './components/duotone/utils';
import { undoIgnoreBlocks } from './store/undo-ignore';

/**
* Private @wordpress/block-editor APIs.
Expand Down Expand Up @@ -52,4 +53,5 @@ lock( privateApis, {
ReusableBlocksRenameHint,
useReusableBlocksRenameHint,
usesContextKey,
undoIgnoreBlocks,
} );
35 changes: 30 additions & 5 deletions packages/block-editor/src/store/private-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
*/
import { Platform } from '@wordpress/element';

/**
* Internal dependencies
*/
import { undoIgnoreBlocks } from './undo-ignore';

const castArray = ( maybeArray ) =>
Array.isArray( maybeArray ) ? maybeArray : [ maybeArray ];

Expand Down Expand Up @@ -291,10 +296,30 @@ export function deleteStyleOverride( id ) {
};
}

export function syncDerivedBlockAttributes( clientId, attributes ) {
return {
type: 'SYNC_DERIVED_BLOCK_ATTRIBUTES',
clientIds: [ clientId ],
attributes,
/**
* A higher-order action that mark every change inside a callback as "non-persistent"
* and ignore pushing to the undo history stack. It's primarily used for synchronized
* derived updates from the block editor without affecting the undo history.
*
* @param {() => void} callback The synchronous callback to derive updates.
*/
export function syncDerivedUpdates( callback ) {
return ( { dispatch, select, registry } ) => {
registry.batch( () => {
// Mark every change in the `callback` as non-persistent.
dispatch( {
type: 'SET_EXPLICIT_PERSISTENT',
isPersistentChange: false,
} );
callback();
dispatch( {
type: 'SET_EXPLICIT_PERSISTENT',
isPersistentChange: undefined,
} );

// Ignore pushing undo stack for the updated blocks.
const updatedBlocks = select.getBlocks();
undoIgnoreBlocks.add( updatedBlocks );
} );
};
}
14 changes: 11 additions & 3 deletions packages/block-editor/src/store/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -453,13 +453,21 @@ const withBlockTree =
function withPersistentBlockChange( reducer ) {
let lastAction;
let markNextChangeAsNotPersistent = false;
let explicitPersistent;

return ( state, action ) => {
let nextState = reducer( state, action );

if ( action.type === 'SYNC_DERIVED_BLOCK_ATTRIBUTES' ) {
return nextState.isPersistentChange
? { ...nextState, isPersistentChange: false }
if ( action.type === 'SET_EXPLICIT_PERSISTENT' ) {
explicitPersistent = action.isPersistentChange;
}

if ( explicitPersistent !== undefined ) {
return explicitPersistent !== nextState.isPersistentChange
? {
...nextState,
isPersistentChange: explicitPersistent,
}
: nextState;
}

Expand Down
4 changes: 4 additions & 0 deletions packages/block-editor/src/store/undo-ignore.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Keep track of the blocks that should not be pushing an additional
// undo stack when editing the entity.
// See the implementation of `syncDerivedUpdates` and `editEntityRecord`.
export const undoIgnoreBlocks = new WeakSet();
47 changes: 25 additions & 22 deletions packages/block-library/src/block/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export default function ReusableBlockEdit( {
attributes: { ref, overrides },
__unstableParentLayout: parentLayout,
clientId: patternClientId,
setAttributes,
} ) {
const registry = useRegistry();
const hasAlreadyRendered = useHasRecursion( ref );
Expand All @@ -154,23 +155,27 @@ export default function ReusableBlockEdit( {
setBlockEditingMode,
} = useDispatch( blockEditorStore );
const { getBlockEditingMode } = useSelect( blockEditorStore );
const { syncDerivedUpdates } = unlock( useDispatch( blockEditorStore ) );

// Apply the initial overrides from the pattern block to the inner blocks.
useEffect( () => {
if ( ! record?.content?.raw ) return;
const initialBlocks = parse( record.content.raw );

const editingMode = getBlockEditingMode( patternClientId );
// Replace the contents of the blocks with the overrides.
registry.batch( () => {
setBlockEditingMode( patternClientId, 'default' );
__unstableMarkNextChangeAsNotPersistent();
replaceInnerBlocks(
patternClientId,
applyInitialOverrides(
initialBlocks,
initialOverrides.current,
defaultValuesRef.current
)
);
syncDerivedUpdates( () => {
replaceInnerBlocks(
patternClientId,
applyInitialOverrides(
initialBlocks,
initialOverrides.current,
defaultValuesRef.current
)
);
} );
setBlockEditingMode( patternClientId, editingMode );
} );
}, [
Expand All @@ -181,6 +186,7 @@ export default function ReusableBlockEdit( {
registry,
getBlockEditingMode,
setBlockEditingMode,
syncDerivedUpdates,
] );

const innerBlocks = useSelect(
Expand Down Expand Up @@ -216,29 +222,26 @@ export default function ReusableBlockEdit( {
: InnerBlocks.ButtonBlockAppender,
} );

// Sync the `overrides` attribute from the updated blocks.
// `syncDerivedBlockAttributes` is an action that just like `updateBlockAttributes`
// but won't create an undo level.
// This can be abstracted into a `useSyncDerivedAttributes` hook if needed.
// Sync the `overrides` attribute from the updated blocks to the pattern block.
// `syncDerivedUpdates` is used here to avoid creating an additional undo level.
useEffect( () => {
const { getBlocks } = registry.select( blockEditorStore );
const { syncDerivedBlockAttributes } = unlock(
registry.dispatch( blockEditorStore )
);
let prevBlocks = getBlocks( patternClientId );
return registry.subscribe( () => {
const blocks = getBlocks( patternClientId );
if ( blocks !== prevBlocks ) {
prevBlocks = blocks;
syncDerivedBlockAttributes( patternClientId, {
overrides: getOverridesFromBlocks(
blocks,
defaultValuesRef.current
),
syncDerivedUpdates( () => {
setAttributes( {
overrides: getOverridesFromBlocks(
blocks,
defaultValuesRef.current
),
} );
} );
}
}, blockEditorStore );
}, [ patternClientId, registry ] );
}, [ syncDerivedUpdates, patternClientId, registry, setAttributes ] );

let children = null;

Expand Down
9 changes: 8 additions & 1 deletion packages/core-data/src/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { v4 as uuid } from 'uuid';
import apiFetch from '@wordpress/api-fetch';
import { addQueryArgs } from '@wordpress/url';
import deprecated from '@wordpress/deprecated';
import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';

/**
* Internal dependencies
Expand All @@ -20,6 +21,9 @@ import { getOrLoadEntitiesConfig, DEFAULT_ENTITY_KEY } from './entities';
import { createBatch } from './batch';
import { STORE_NAME } from './name';
import { getSyncProvider } from './sync';
import { unlock } from './private-apis';

const { undoIgnoreBlocks } = unlock( blockEditorPrivateApis );

/**
* Returns an action object used in signalling that authors have been received.
Expand Down Expand Up @@ -405,7 +409,10 @@ export const editEntityRecord =
);
}
} else {
if ( ! options.undoIgnore ) {
if (
! options.undoIgnore &&
! undoIgnoreBlocks.has( edit.edits.blocks )
) {
select.getUndoManager().addRecord(
[
{
Expand Down

0 comments on commit 1789027

Please sign in to comment.