diff --git a/docs/designers-developers/developers/data/data-core-editor.md b/docs/designers-developers/developers/data/data-core-editor.md index 6b08fa9b2187df..cc5f0a89928a7b 100644 --- a/docs/designers-developers/developers/data/data-core-editor.md +++ b/docs/designers-developers/developers/data/data-core-editor.md @@ -1054,6 +1054,7 @@ been edited. _Parameters_ - _edits_ `Object`: Post attributes to edit. +- _options_ `Object`: Options for the edit. # **enablePublishSidebar** diff --git a/docs/designers-developers/developers/data/data-core.md b/docs/designers-developers/developers/data/data-core.md index 658ba7dc930213..cc724c5d260003 100644 --- a/docs/designers-developers/developers/data/data-core.md +++ b/docs/designers-developers/developers/data/data-core.md @@ -450,6 +450,8 @@ _Parameters_ - _name_ `string`: Name of the edited entity record. - _recordId_ `number`: Record ID of the edited entity record. - _edits_ `Object`: The edits. +- _options_ `Object`: Options for the edit. +- _options.undoIgnore_ `boolean`: Whether to ignore the edit in undo history or not. _Returns_ diff --git a/packages/core-data/README.md b/packages/core-data/README.md index 7d931ab20883eb..27d3e812f46483 100644 --- a/packages/core-data/README.md +++ b/packages/core-data/README.md @@ -65,6 +65,8 @@ _Parameters_ - _name_ `string`: Name of the edited entity record. - _recordId_ `number`: Record ID of the edited entity record. - _edits_ `Object`: The edits. +- _options_ `Object`: Options for the edit. +- _options.undoIgnore_ `boolean`: Whether to ignore the edit in undo history or not. _Returns_ diff --git a/packages/core-data/src/actions.js b/packages/core-data/src/actions.js index 10c768e2aad23c..4c8027b65f9c7b 100644 --- a/packages/core-data/src/actions.js +++ b/packages/core-data/src/actions.js @@ -126,14 +126,16 @@ export function receiveEmbedPreview( url, preview ) { * Returns an action object that triggers an * edit to an entity record. * - * @param {string} kind Kind of the edited entity record. - * @param {string} name Name of the edited entity record. - * @param {number} recordId Record ID of the edited entity record. - * @param {Object} edits The edits. + * @param {string} kind Kind of the edited entity record. + * @param {string} name Name of the edited entity record. + * @param {number} recordId Record ID of the edited entity record. + * @param {Object} edits The edits. + * @param {Object} options Options for the edit. + * @param {boolean} options.undoIgnore Whether to ignore the edit in undo history or not. * * @return {Object} Action object. */ -export function* editEntityRecord( kind, name, recordId, edits ) { +export function* editEntityRecord( kind, name, recordId, edits, options = {} ) { const { transientEdits = {}, mergedEdits = {} } = yield select( 'getEntity', kind, @@ -167,7 +169,7 @@ export function* editEntityRecord( kind, name, recordId, edits ) { type: 'EDIT_ENTITY_RECORD', ...edit, meta: { - undo: { + undo: ! options.undoIgnore && { ...edit, // Send the current values for things like the first undo stack entry. edits: Object.keys( edits ).reduce( ( acc, key ) => { diff --git a/packages/core-data/src/reducer.js b/packages/core-data/src/reducer.js index 68c3ff7e51f135..6e40b518285d6b 100644 --- a/packages/core-data/src/reducer.js +++ b/packages/core-data/src/reducer.js @@ -313,6 +313,10 @@ export function undo( state = UNDO_INITIAL_STATE, action ) { return nextState; } + if ( ! action.meta.undo ) { + return state; + } + // Transient edits don't create an undo level, but are // reachable in the next meaningful edit to which they // are merged. They are defined in the entity's config. @@ -333,6 +337,8 @@ export function undo( state = UNDO_INITIAL_STATE, action ) { recordId: action.meta.undo.recordId, edits: { ...state.flattenedUndo, ...action.meta.undo.edits }, } ); + // When an edit is a function it's an optimization to avoid running some expensive operation. + // We can't rely on the function references being the same so we opt out of comparing them here. const comparisonUndoEdits = Object.values( action.meta.undo.edits ).filter( ( edit ) => typeof edit !== 'function' ); const comparisonEdits = Object.values( action.edits ).filter( ( edit ) => typeof edit !== 'function' ); if ( ! isShallowEqual( comparisonUndoEdits, comparisonEdits ) ) { diff --git a/packages/e2e-tests/specs/undo.test.js b/packages/e2e-tests/specs/undo.test.js index d1b24ce2c907c4..6d108c7cfdb466 100644 --- a/packages/e2e-tests/specs/undo.test.js +++ b/packages/e2e-tests/specs/undo.test.js @@ -9,6 +9,7 @@ import { selectBlockByClientId, getAllBlocks, saveDraft, + publishPost, disableNavigationMode, } from '@wordpress/e2e-test-utils'; @@ -109,6 +110,24 @@ describe( 'undo', () => { expect( visibleContent ).toBe( 'original' ); } ); + it( 'should not create undo levels when saving', async () => { + await clickBlockAppender(); + await page.keyboard.type( '1' ); + await saveDraft(); + await pressKeyWithModifier( 'primary', 'z' ); + + expect( await getEditedPostContent() ).toBe( '' ); + } ); + + it( 'should not create undo levels when publishing', async () => { + await clickBlockAppender(); + await page.keyboard.type( '1' ); + await publishPost(); + await pressKeyWithModifier( 'primary', 'z' ); + + expect( await getEditedPostContent() ).toBe( '' ); + } ); + it( 'should immediately create an undo level on typing', async () => { await clickBlockAppender(); diff --git a/packages/editor/src/components/post-publish-button/index.js b/packages/editor/src/components/post-publish-button/index.js index 90b689ff5a357b..1513dfbfbb414f 100644 --- a/packages/editor/src/components/post-publish-button/index.js +++ b/packages/editor/src/components/post-publish-button/index.js @@ -155,7 +155,7 @@ export default compose( [ withDispatch( ( dispatch ) => { const { editPost, savePost } = dispatch( 'core/editor' ); return { - onStatusChange: ( status ) => editPost( { status } ), + onStatusChange: ( status ) => editPost( { status }, { undoIgnore: true } ), onSave: savePost, }; } ), diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index fde5d0d28fe251..bd3076ef7c3b57 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -348,13 +348,22 @@ export function setupEditorState( post ) { * Returns an action object used in signalling that attributes of the post have * been edited. * - * @param {Object} edits Post attributes to edit. + * @param {Object} edits Post attributes to edit. + * @param {Object} options Options for the edit. * * @yield {Object} Action object or control. */ -export function* editPost( edits ) { +export function* editPost( edits, options ) { const { id, type } = yield select( STORE_KEY, 'getCurrentPost' ); - yield dispatch( 'core', 'editEntityRecord', 'postType', type, id, edits ); + yield dispatch( + 'core', + 'editEntityRecord', + 'postType', + type, + id, + edits, + options + ); } /** @@ -385,7 +394,7 @@ export function* savePost( options = {} ) { content: yield select( STORE_KEY, 'getEditedPostContent' ), }; if ( ! options.isAutosave ) { - yield dispatch( STORE_KEY, 'editPost', edits ); + yield dispatch( STORE_KEY, 'editPost', edits, { undoIgnore: true } ); } yield __experimentalRequestPostUpdateStart( options ); diff --git a/packages/editor/src/store/test/actions.js b/packages/editor/src/store/test/actions.js index aa6ae0d7fc5e64..3c34195bb35415 100644 --- a/packages/editor/src/store/test/actions.js +++ b/packages/editor/src/store/test/actions.js @@ -98,7 +98,9 @@ describe( 'Post generator actions', () => { if ( ! isAutosave ) { const edits = { content: currentPost().content }; const { value } = fulfillment.next( edits.content ); - expect( value ).toEqual( dispatch( STORE_KEY, 'editPost', edits ) ); + expect( value ).toEqual( + dispatch( STORE_KEY, 'editPost', edits, { undoIgnore: true } ) + ); } }, ], @@ -495,7 +497,8 @@ describe( 'Editor actions', () => { 'postType', post.type, post.id, - edits + edits, + undefined ), } ); expect( fulfillment.next() ).toEqual( {