diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index 6bfb468cd7157..9768ddeb0ed43 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -101,6 +101,28 @@ function getFlattenedBlocks( blocks ) { return flattenedBlocks; } +/** + * Given a block order map object, returns *all* of the block client IDs that are + * a descendant of the given root client ID. + * + * Calling this with `rootClientId` set to `''` results in a list of client IDs + * that are in the post. That is, it excludes blocks like fetched reusable + * blocks which are stored into state but not visible. + * + * @param {Object} blocksOrder Object that maps block client IDs to a list of + * nested block client IDs. + * @param {?string} rootClientId The root client ID to search. Defaults to ''. + * + * @return {Array} List of descendant client IDs. + */ +function getNestedBlockClientIds( blocksOrder, rootClientId = '' ) { + return reduce( blocksOrder[ rootClientId ], ( result, clientId ) => [ + ...result, + clientId, + ...getNestedBlockClientIds( blocksOrder, clientId ), + ], [] ); +} + /** * Returns an object against which it is safe to perform mutating operations, * given the original object and its current working copy. @@ -211,6 +233,35 @@ const withInnerBlocksRemoveCascade = ( reducer ) => ( state, action ) => { return reducer( state, action ); }; +/** + * Higher-order reducer which targets the combined blocks reducer and handles + * the `RESET_BLOCKS` action. When dispatched, this action will replace all + * blocks that exist in the post, leaving blocks that exist only in state (e.g. + * reusable blocks) alone. + * + * @param {Function} reducer Original reducer function. + * + * @return {Function} Enhanced reducer function. + */ +const withBlockReset = ( reducer ) => ( state, action ) => { + if ( state && action.type === 'RESET_BLOCKS' ) { + const visibleClientIds = getNestedBlockClientIds( state.order ); + return { + ...state, + byClientId: { + ...omit( state.byClientId, visibleClientIds ), + ...getFlattenedBlocks( action.blocks ), + }, + order: { + ...omit( state.order, visibleClientIds ), + ...mapBlockOrder( action.blocks ), + }, + }; + } + + return reducer( state, action ); +}; + /** * Undoable reducer returning the editor post state, including blocks parsed * from current HTML markup. @@ -280,6 +331,8 @@ export const editor = flow( [ blocks: flow( [ combineReducers, + withBlockReset, + // Track whether changes exist, resetting at each post save. Relies on // editor initialization firing post reset as an effect. withChangeDetection( { @@ -289,7 +342,6 @@ export const editor = flow( [ ] )( { byClientId( state = {}, action ) { switch ( action.type ) { - case 'RESET_BLOCKS': case 'SETUP_EDITOR_STATE': return getFlattenedBlocks( action.blocks ); @@ -392,7 +444,6 @@ export const editor = flow( [ order( state = {}, action ) { switch ( action.type ) { - case 'RESET_BLOCKS': case 'SETUP_EDITOR_STATE': return mapBlockOrder( action.blocks ); diff --git a/packages/editor/src/store/test/reducer.js b/packages/editor/src/store/test/reducer.js index d40b07b0b36bd..0d9561c15aaa8 100644 --- a/packages/editor/src/store/test/reducer.js +++ b/packages/editor/src/store/test/reducer.js @@ -1074,6 +1074,58 @@ describe( 'state', () => { } ); describe( 'blocks', () => { + it( 'should not reset any blocks that are not in the post', () => { + const actions = [ + { + type: 'RESET_BLOCKS', + blocks: [ + { + clientId: 'block1', + innerBlocks: [ + { clientId: 'block11', innerBlocks: [] }, + { clientId: 'block12', innerBlocks: [] }, + ], + }, + ], + }, + { + type: 'RECEIVE_BLOCKS', + blocks: [ + { + clientId: 'block2', + innerBlocks: [ + { clientId: 'block21', innerBlocks: [] }, + { clientId: 'block22', innerBlocks: [] }, + ], + }, + ], + }, + ]; + const original = deepFreeze( actions.reduce( editor, undefined ) ); + + const state = editor( original, { + type: 'RESET_BLOCKS', + blocks: [ + { + clientId: 'block3', + innerBlocks: [ + { clientId: 'block31', innerBlocks: [] }, + { clientId: 'block32', innerBlocks: [] }, + ], + }, + ], + } ); + + expect( state.present.blocks.byClientId ).toEqual( { + block2: { clientId: 'block2' }, + block21: { clientId: 'block21' }, + block22: { clientId: 'block22' }, + block3: { clientId: 'block3' }, + block31: { clientId: 'block31' }, + block32: { clientId: 'block32' }, + } ); + } ); + describe( 'byClientId', () => { it( 'should return with attribute block updates', () => { const original = deepFreeze( editor( undefined, {