Skip to content

Commit

Permalink
Fix broken undo history stack for Pattern Overrides (#57088)
Browse files Browse the repository at this point in the history
* Fix broken undo history stack for Pattern Overrides

* Fix unit test

* Fix unit test

* Revert uneeded changes
  • Loading branch information
kevin940726 authored Dec 22, 2023
1 parent 51b81ef commit 0080b75
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -263,13 +263,13 @@ describe( 'useBlockSync hook', () => {

expect( onInput ).toHaveBeenCalledWith(
[ { clientId: 'a', innerBlocks: [], attributes: { foo: 2 } } ],
{
expect.objectContaining( {
selection: {
selectionEnd: {},
selectionStart: {},
initialPosition: null,
},
}
} )
);
expect( onChange ).not.toHaveBeenCalled();
} );
Expand Down Expand Up @@ -303,13 +303,13 @@ describe( 'useBlockSync hook', () => {

expect( onChange ).toHaveBeenCalledWith(
[ { clientId: 'a', innerBlocks: [], attributes: { foo: 2 } } ],
{
expect.objectContaining( {
selection: {
selectionEnd: {},
selectionStart: {},
initialPosition: null,
},
}
} )
);
expect( onInput ).not.toHaveBeenCalled();
} );
Expand Down Expand Up @@ -406,13 +406,13 @@ describe( 'useBlockSync hook', () => {
attributes: { foo: 2 },
},
],
{
expect.objectContaining( {
selection: {
selectionEnd: {},
selectionStart: {},
initialPosition: null,
},
}
} )
);
expect( onInput ).not.toHaveBeenCalled();
} );
Expand Down Expand Up @@ -447,13 +447,16 @@ describe( 'useBlockSync hook', () => {
{ clientId: 'a', innerBlocks: [], attributes: { foo: 2 } },
];

expect( onChange1 ).toHaveBeenCalledWith( updatedBlocks1, {
selection: {
initialPosition: null,
selectionEnd: {},
selectionStart: {},
},
} );
expect( onChange1 ).toHaveBeenCalledWith(
updatedBlocks1,
expect.objectContaining( {
selection: {
initialPosition: null,
selectionEnd: {},
selectionStart: {},
},
} )
);

const newBlocks = [
{ clientId: 'b', innerBlocks: [], attributes: { foo: 1 } },
Expand Down Expand Up @@ -485,13 +488,13 @@ describe( 'useBlockSync hook', () => {
// The second callback should be called with the new change.
expect( onChange2 ).toHaveBeenCalledWith(
[ { clientId: 'b', innerBlocks: [], attributes: { foo: 3 } } ],
{
expect.objectContaining( {
selection: {
selectionEnd: {},
selectionStart: {},
initialPosition: null,
},
}
} )
);
} );

Expand Down Expand Up @@ -544,13 +547,13 @@ describe( 'useBlockSync hook', () => {
// Only the new callback should be called.
expect( onChange2 ).toHaveBeenCalledWith(
[ { clientId: 'b', innerBlocks: [], attributes: { foo: 3 } } ],
{
expect.objectContaining( {
selection: {
selectionEnd: {},
selectionStart: {},
initialPosition: null,
},
}
} )
);
} );
} );
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { cloneBlock } from '@wordpress/blocks';
* Internal dependencies
*/
import { store as blockEditorStore } from '../../store';
import { undoIgnoreBlocks } from '../../store/undo-ignore';

const noop = () => {};

Expand Down Expand Up @@ -264,13 +265,18 @@ export default function useBlockSync( {
const updateParent = isPersistent
? onChangeRef.current
: onInputRef.current;
const undoIgnore = undoIgnoreBlocks.has( blocks );
if ( undoIgnore ) {
undoIgnoreBlocks.delete( blocks );
}
updateParent( blocks, {
selection: {
selectionStart: getSelectionStart(),
selectionEnd: getSelectionEnd(),
initialPosition:
getSelectedBlocksInitialCaretPosition(),
},
undoIgnore,
} );
}
previousAreBlocksDifferent = areBlocksDifferent;
Expand Down
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 );
} );
};
}
21 changes: 16 additions & 5 deletions packages/block-editor/src/store/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -453,14 +453,25 @@ 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 }
: nextState;
let nextIsPersistentChange;
if ( action.type === 'SET_EXPLICIT_PERSISTENT' ) {
explicitPersistent = action.isPersistentChange;
nextIsPersistentChange = state.isPersistentChange ?? true;
}

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

const isExplicitPersistentChange =
Expand All @@ -473,7 +484,7 @@ function withPersistentBlockChange( reducer ) {
markNextChangeAsNotPersistent =
action.type === 'MARK_NEXT_CHANGE_AS_NOT_PERSISTENT';

const nextIsPersistentChange = state?.isPersistentChange ?? true;
nextIsPersistentChange = state?.isPersistentChange ?? true;
if ( state.isPersistentChange === nextIsPersistentChange ) {
return state;
}
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 `useBlockSync`.
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,7 +155,9 @@ 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( () => {
const initialBlocks =
editedRecord.blocks ??
Expand All @@ -164,17 +167,19 @@ export default function ReusableBlockEdit( {

defaultValuesRef.current = {};
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 @@ -185,6 +190,7 @@ export default function ReusableBlockEdit( {
registry,
getBlockEditingMode,
setBlockEditingMode,
syncDerivedUpdates,
] );

const innerBlocks = useSelect(
Expand Down Expand Up @@ -220,29 +226,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
14 changes: 10 additions & 4 deletions packages/core-data/src/entity-provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ export function useEntityBlockEditor( kind, name, { id: _id } = {} ) {
if ( noChange ) {
return __unstableCreateUndoLevel( kind, name, id );
}
const { selection } = options;
const { selection, ...rest } = options;

// We create a new function here on every persistent edit
// to make sure the edit makes the post dirty and creates
Expand All @@ -208,7 +208,10 @@ export function useEntityBlockEditor( kind, name, { id: _id } = {} ) {
...updateFootnotes( newBlocks ),
};

editEntityRecord( kind, name, id, edits, { isCached: false } );
editEntityRecord( kind, name, id, edits, {
isCached: false,
...rest,
} );
},
[
kind,
Expand All @@ -223,11 +226,14 @@ export function useEntityBlockEditor( kind, name, { id: _id } = {} ) {

const onInput = useCallback(
( newBlocks, options ) => {
const { selection } = options;
const { selection, ...rest } = options;
const footnotesChanges = updateFootnotes( newBlocks );
const edits = { selection, ...footnotesChanges };

editEntityRecord( kind, name, id, edits, { isCached: true } );
editEntityRecord( kind, name, id, edits, {
isCached: true,
...rest,
} );
},
[ kind, name, id, updateFootnotes, editEntityRecord ]
);
Expand Down

1 comment on commit 0080b75

@github-actions
Copy link

Choose a reason for hiding this comment

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

Flaky tests detected in 0080b75.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/7298138811
📝 Reported issues:

Please sign in to comment.