diff --git a/editor/reducer.js b/editor/reducer.js index 76fd7ec566e4b0..49d6e05ddfd419 100644 --- a/editor/reducer.js +++ b/editor/reducer.js @@ -4,6 +4,8 @@ import optimist from 'redux-optimist'; import { combineReducers } from 'redux'; import { + flow, + partialRight, difference, get, reduce, @@ -26,7 +28,7 @@ import { getBlockTypes, getBlockType } from '@wordpress/blocks'; /** * Internal dependencies */ -import { combineUndoableReducers } from './utils/undoable-reducer'; +import undoableReducer from './utils/undoable-reducer'; import { STORE_DEFAULTS } from './store-defaults'; import saveState from './state/save-state'; @@ -64,7 +66,12 @@ export function getPostRawValue( value ) { * @param {Object} action Dispatched action * @return {Object} Updated state */ -export const editor = combineUndoableReducers( { +export const editor = flow( [ + combineReducers, + + // Track undo history, starting at editor initialization. + partialRight( undoableReducer, { resetTypes: [ 'SETUP_EDITOR' ] } ), +] )( { edits( state = {}, action ) { switch ( action.type ) { case 'EDIT_POST': @@ -262,7 +269,7 @@ export const editor = combineUndoableReducers( { return state; }, -}, { resetTypes: [ 'SETUP_EDITOR' ] } ); +} ); /** * Reducer returning the last-known state of the current post, in the format diff --git a/editor/selectors.js b/editor/selectors.js index 7dc4e371beec88..d8c80125d6a37f 100644 --- a/editor/selectors.js +++ b/editor/selectors.js @@ -147,7 +147,7 @@ export function isEditorSidebarPanelOpened( state, panel ) { * @return {Boolean} Whether undo history exists */ export function hasEditorUndo( state ) { - return state.editor.history.past.length > 0; + return state.editor.past.length > 0; } /** @@ -158,7 +158,7 @@ export function hasEditorUndo( state ) { * @return {Boolean} Whether redo history exists */ export function hasEditorRedo( state ) { - return state.editor.history.future.length > 0; + return state.editor.future.length > 0; } /** @@ -256,7 +256,7 @@ export function getCurrentPostLastRevisionId( state ) { * @return {Object} Object of key value pairs comprising unsaved edits */ export function getPostEdits( state ) { - return state.editor.edits; + return state.editor.present.edits; } /** @@ -269,9 +269,9 @@ export function getPostEdits( state ) { * @return {*} Post attribute value */ export function getEditedPostAttribute( state, attributeName ) { - return state.editor.edits[ attributeName ] === undefined ? + return state.editor.present.edits[ attributeName ] === undefined ? state.currentPost[ attributeName ] : - state.editor.edits[ attributeName ]; + state.editor.present.edits[ attributeName ]; } /** @@ -390,9 +390,9 @@ export function getDocumentTitle( state ) { * @return {String} Raw post excerpt */ export function getEditedPostExcerpt( state ) { - return state.editor.edits.excerpt === undefined ? + return state.editor.present.edits.excerpt === undefined ? state.currentPost.excerpt : - state.editor.edits.excerpt; + state.editor.present.edits.excerpt; } /** @@ -422,7 +422,7 @@ export function getEditedPostPreviewLink( state ) { */ export const getBlock = createSelector( ( state, uid ) => { - const block = state.editor.blocksByUid[ uid ]; + const block = state.editor.present.blocksByUid[ uid ]; if ( ! block ) { return null; } @@ -475,11 +475,11 @@ function getPostMeta( state, key ) { */ export const getBlocks = createSelector( ( state ) => { - return state.editor.blockOrder.map( ( uid ) => getBlock( state, uid ) ); + return state.editor.present.blockOrder.map( ( uid ) => getBlock( state, uid ) ); }, ( state ) => [ - state.editor.blockOrder, - state.editor.blocksByUid, + state.editor.present.blockOrder, + state.editor.present.blocksByUid, ] ); @@ -533,7 +533,7 @@ export function getSelectedBlock( state ) { */ export const getMultiSelectedBlockUids = createSelector( ( state ) => { - const { blockOrder } = state.editor; + const { blockOrder } = state.editor.present; const { start, end } = state.blockSelection; if ( start === end ) { return []; @@ -549,7 +549,7 @@ export const getMultiSelectedBlockUids = createSelector( return blockOrder.slice( startIndex, endIndex + 1 ); }, ( state ) => [ - state.editor.blockOrder, + state.editor.present.blockOrder, state.blockSelection.start, state.blockSelection.end, ], @@ -565,11 +565,11 @@ export const getMultiSelectedBlockUids = createSelector( export const getMultiSelectedBlocks = createSelector( ( state ) => getMultiSelectedBlockUids( state ).map( ( uid ) => getBlock( state, uid ) ), ( state ) => [ - state.editor.blockOrder, + state.editor.present.blockOrder, state.blockSelection.start, state.blockSelection.end, - state.editor.blocksByUid, - state.editor.edits.meta, + state.editor.present.blocksByUid, + state.editor.present.edits.meta, state.currentPost.meta, ] ); @@ -665,7 +665,7 @@ export function getMultiSelectedBlocksEndUid( state ) { * @return {Array} Ordered unique IDs of post blocks */ export function getBlockUids( state ) { - return state.editor.blockOrder; + return state.editor.present.blockOrder; } /** @@ -677,7 +677,7 @@ export function getBlockUids( state ) { * @return {Number} Index at which block exists in order */ export function getBlockIndex( state, uid ) { - return state.editor.blockOrder.indexOf( uid ); + return state.editor.present.blockOrder.indexOf( uid ); } /** @@ -689,7 +689,7 @@ export function getBlockIndex( state, uid ) { * @return {Boolean} Whether block is first in post */ export function isFirstBlock( state, uid ) { - return first( state.editor.blockOrder ) === uid; + return first( state.editor.present.blockOrder ) === uid; } /** @@ -701,7 +701,7 @@ export function isFirstBlock( state, uid ) { * @return {Boolean} Whether block is last in post */ export function isLastBlock( state, uid ) { - return last( state.editor.blockOrder ) === uid; + return last( state.editor.present.blockOrder ) === uid; } /** @@ -714,7 +714,7 @@ export function isLastBlock( state, uid ) { */ export function getPreviousBlock( state, uid ) { const order = getBlockIndex( state, uid ); - return state.editor.blocksByUid[ state.editor.blockOrder[ order - 1 ] ] || null; + return state.editor.present.blocksByUid[ state.editor.present.blockOrder[ order - 1 ] ] || null; } /** @@ -727,7 +727,7 @@ export function getPreviousBlock( state, uid ) { */ export function getNextBlock( state, uid ) { const order = getBlockIndex( state, uid ); - return state.editor.blocksByUid[ state.editor.blockOrder[ order + 1 ] ] || null; + return state.editor.present.blocksByUid[ state.editor.present.blockOrder[ order + 1 ] ] || null; } /** @@ -818,7 +818,7 @@ export function isTyping( state ) { */ export function getBlockInsertionPoint( state ) { if ( getEditorMode( state ) !== 'visual' ) { - return state.editor.blockOrder.length; + return state.editor.present.blockOrder.length; } const position = getBlockSiblingInserterPosition( state ); @@ -836,7 +836,7 @@ export function getBlockInsertionPoint( state ) { return getBlockIndex( state, selectedBlock.uid ) + 1; } - return state.editor.blockOrder.length; + return state.editor.present.blockOrder.length; } /** @@ -906,20 +906,20 @@ export function didPostSaveRequestFail( state ) { * @return {?String} Suggested post format */ export function getSuggestedPostFormat( state ) { - const blocks = state.editor.blockOrder; + const blocks = state.editor.present.blockOrder; let name; // If there is only one block in the content of the post grab its name // so we can derive a suitable post format from it. if ( blocks.length === 1 ) { - name = state.editor.blocksByUid[ blocks[ 0 ] ].name; + name = state.editor.present.blocksByUid[ blocks[ 0 ] ].name; } // If there are two blocks in the content and the last one is a text blocks // grab the name of the first one to also suggest a post format from it. if ( blocks.length === 2 ) { - if ( state.editor.blocksByUid[ blocks[ 1 ] ].name === 'core/paragraph' ) { - name = state.editor.blocksByUid[ blocks[ 0 ] ].name; + if ( state.editor.present.blocksByUid[ blocks[ 1 ] ].name === 'core/paragraph' ) { + name = state.editor.present.blocksByUid[ blocks[ 0 ] ].name; } } @@ -962,9 +962,9 @@ export const getEditedPostContent = createSelector( return serialize( getBlocks( state ) ); }, ( state ) => [ - state.editor.edits.content, - state.editor.blocksByUid, - state.editor.blockOrder, + state.editor.present.edits.content, + state.editor.present.blocksByUid, + state.editor.present.blockOrder, ], ); diff --git a/editor/test/reducer.js b/editor/test/reducer.js index cf5985c400ec43..cb7eed18114b8e 100644 --- a/editor/test/reducer.js +++ b/editor/test/reducer.js @@ -56,12 +56,12 @@ describe( 'state', () => { unregisterBlockType( 'core/test-block' ); } ); - it( 'should return empty blocksByUid, blockOrder, history by default', () => { + it( 'should return empty edits, blocksByUid, blockOrder by default', () => { const state = editor( undefined, {} ); - expect( state.blocksByUid ).toEqual( {} ); - expect( state.blockOrder ).toEqual( [] ); - expect( state ).toHaveProperty( 'history' ); + expect( state.present.edits ).toEqual( {} ); + expect( state.present.blocksByUid ).toEqual( {} ); + expect( state.present.blockOrder ).toEqual( [] ); } ); it( 'should key by replaced blocks uid', () => { @@ -71,9 +71,9 @@ describe( 'state', () => { blocks: [ { uid: 'bananas' } ], } ); - expect( Object.keys( state.blocksByUid ) ).toHaveLength( 1 ); - expect( values( state.blocksByUid )[ 0 ].uid ).toBe( 'bananas' ); - expect( state.blockOrder ).toEqual( [ 'bananas' ] ); + expect( Object.keys( state.present.blocksByUid ) ).toHaveLength( 1 ); + expect( values( state.present.blocksByUid )[ 0 ].uid ).toBe( 'bananas' ); + expect( state.present.blockOrder ).toEqual( [ 'bananas' ] ); } ); it( 'should insert block', () => { @@ -93,9 +93,9 @@ describe( 'state', () => { } ], } ); - expect( Object.keys( state.blocksByUid ) ).toHaveLength( 2 ); - expect( values( state.blocksByUid )[ 1 ].uid ).toBe( 'ribs' ); - expect( state.blockOrder ).toEqual( [ 'chicken', 'ribs' ] ); + expect( Object.keys( state.present.blocksByUid ) ).toHaveLength( 2 ); + expect( values( state.present.blocksByUid )[ 1 ].uid ).toBe( 'ribs' ); + expect( state.present.blockOrder ).toEqual( [ 'chicken', 'ribs' ] ); } ); it( 'should replace the block', () => { @@ -116,10 +116,10 @@ describe( 'state', () => { } ], } ); - expect( Object.keys( state.blocksByUid ) ).toHaveLength( 1 ); - expect( values( state.blocksByUid )[ 0 ].name ).toBe( 'core/freeform' ); - expect( values( state.blocksByUid )[ 0 ].uid ).toBe( 'wings' ); - expect( state.blockOrder ).toEqual( [ 'wings' ] ); + expect( Object.keys( state.present.blocksByUid ) ).toHaveLength( 1 ); + expect( values( state.present.blocksByUid )[ 0 ].name ).toBe( 'core/freeform' ); + expect( values( state.present.blocksByUid )[ 0 ].uid ).toBe( 'wings' ); + expect( state.present.blockOrder ).toEqual( [ 'wings' ] ); } ); it( 'should update the block', () => { @@ -141,7 +141,7 @@ describe( 'state', () => { }, } ); - expect( state.blocksByUid.chicken ).toEqual( { + expect( state.present.blocksByUid.chicken ).toEqual( { uid: 'chicken', name: 'core/test-block', attributes: { content: 'ribs' }, @@ -167,7 +167,7 @@ describe( 'state', () => { uids: [ 'ribs' ], } ); - expect( state.blockOrder ).toEqual( [ 'ribs', 'chicken' ] ); + expect( state.present.blockOrder ).toEqual( [ 'ribs', 'chicken' ] ); } ); it( 'should move multiple blocks up', () => { @@ -192,7 +192,7 @@ describe( 'state', () => { uids: [ 'ribs', 'veggies' ], } ); - expect( state.blockOrder ).toEqual( [ 'ribs', 'veggies', 'chicken' ] ); + expect( state.present.blockOrder ).toEqual( [ 'ribs', 'veggies', 'chicken' ] ); } ); it( 'should not move the first block up', () => { @@ -213,7 +213,7 @@ describe( 'state', () => { uids: [ 'chicken' ], } ); - expect( state.blockOrder ).toBe( original.blockOrder ); + expect( state.present.blockOrder ).toBe( original.present.blockOrder ); } ); it( 'should move the block down', () => { @@ -234,7 +234,7 @@ describe( 'state', () => { uids: [ 'chicken' ], } ); - expect( state.blockOrder ).toEqual( [ 'ribs', 'chicken' ] ); + expect( state.present.blockOrder ).toEqual( [ 'ribs', 'chicken' ] ); } ); it( 'should move multiple blocks down', () => { @@ -259,7 +259,7 @@ describe( 'state', () => { uids: [ 'chicken', 'ribs' ], } ); - expect( state.blockOrder ).toEqual( [ 'veggies', 'chicken', 'ribs' ] ); + expect( state.present.blockOrder ).toEqual( [ 'veggies', 'chicken', 'ribs' ] ); } ); it( 'should not move the last block down', () => { @@ -280,7 +280,7 @@ describe( 'state', () => { uids: [ 'ribs' ], } ); - expect( state.blockOrder ).toBe( original.blockOrder ); + expect( state.present.blockOrder ).toBe( original.present.blockOrder ); } ); it( 'should remove the block', () => { @@ -301,8 +301,8 @@ describe( 'state', () => { uids: [ 'chicken' ], } ); - expect( state.blockOrder ).toEqual( [ 'ribs' ] ); - expect( state.blocksByUid ).toEqual( { + expect( state.present.blockOrder ).toEqual( [ 'ribs' ] ); + expect( state.present.blocksByUid ).toEqual( { ribs: { uid: 'ribs', name: 'core/test-block', @@ -333,8 +333,8 @@ describe( 'state', () => { uids: [ 'chicken', 'veggies' ], } ); - expect( state.blockOrder ).toEqual( [ 'ribs' ] ); - expect( state.blocksByUid ).toEqual( { + expect( state.present.blockOrder ).toEqual( [ 'ribs' ] ); + expect( state.present.blocksByUid ).toEqual( { ribs: { uid: 'ribs', name: 'core/test-block', @@ -366,8 +366,8 @@ describe( 'state', () => { } ], } ); - expect( Object.keys( state.blocksByUid ) ).toHaveLength( 3 ); - expect( state.blockOrder ).toEqual( [ 'kumquat', 'persimmon', 'loquat' ] ); + expect( Object.keys( state.present.blocksByUid ) ).toHaveLength( 3 ); + expect( state.present.blockOrder ).toEqual( [ 'kumquat', 'persimmon', 'loquat' ] ); } ); describe( 'edits()', () => { @@ -387,7 +387,7 @@ describe( 'state', () => { }, } ); - expect( state.edits ).toEqual( { + expect( state.present.edits ).toEqual( { status: 'draft', title: 'post title', tags: [ 1 ], @@ -410,7 +410,7 @@ describe( 'state', () => { }, } ); - expect( state.edits ).toBe( original.edits ); + expect( state.present.edits ).toBe( original.present.edits ); } ); it( 'should save modified properties', () => { @@ -431,7 +431,7 @@ describe( 'state', () => { }, } ); - expect( state.edits ).toEqual( { + expect( state.present.edits ).toEqual( { status: 'draft', title: 'modified title', tags: [ 2 ], @@ -447,7 +447,7 @@ describe( 'state', () => { }, } ); - expect( state.edits ).toEqual( { + expect( state.present.edits ).toEqual( { status: 'draft', title: 'post title', } ); @@ -465,7 +465,7 @@ describe( 'state', () => { }, } ); - expect( state.edits ).toHaveProperty( 'content' ); + expect( state.present.edits ).toHaveProperty( 'content' ); state = editor( original, { type: 'RESET_BLOCKS', @@ -480,7 +480,7 @@ describe( 'state', () => { } ], } ); - expect( state.edits ).not.toHaveProperty( 'content' ); + expect( state.present.edits ).not.toHaveProperty( 'content' ); } ); } ); @@ -501,7 +501,7 @@ describe( 'state', () => { }, } ); - expect( state.blocksByUid.kumquat.attributes.updated ).toBe( true ); + expect( state.present.blocksByUid.kumquat.attributes.updated ).toBe( true ); } ); it( 'should accumulate attribute block updates', () => { @@ -522,7 +522,7 @@ describe( 'state', () => { }, } ); - expect( state.blocksByUid.kumquat.attributes ).toEqual( { + expect( state.present.blocksByUid.kumquat.attributes ).toEqual( { updated: true, moreUpdated: true, } ); @@ -541,7 +541,7 @@ describe( 'state', () => { }, } ); - expect( state.blocksByUid ).toBe( original.blocksByUid ); + expect( state.present.blocksByUid ).toBe( original.present.blocksByUid ); } ); it( 'should return with same reference if no changes in updates', () => { @@ -562,7 +562,7 @@ describe( 'state', () => { }, } ); - expect( state.blocksByUid ).toBe( state.blocksByUid ); + expect( state.present.blocksByUid ).toBe( state.present.blocksByUid ); } ); } ); } ); diff --git a/editor/test/selectors.js b/editor/test/selectors.js index b396a18bf8f99b..8553efcd1db36b 100644 --- a/editor/test/selectors.js +++ b/editor/test/selectors.js @@ -360,11 +360,9 @@ describe( 'selectors', () => { it( 'should return true when the past history is not empty', () => { const state = { editor: { - history: { - past: [ - {}, - ], - }, + past: [ + {}, + ], }, }; @@ -374,9 +372,7 @@ describe( 'selectors', () => { it( 'should return false when the past history is empty', () => { const state = { editor: { - history: { - past: [], - }, + past: [], }, }; @@ -388,11 +384,9 @@ describe( 'selectors', () => { it( 'should return true when the future history is not empty', () => { const state = { editor: { - history: { - future: [ - {}, - ], - }, + future: [ + {}, + ], }, }; @@ -402,9 +396,7 @@ describe( 'selectors', () => { it( 'should return false when the future history is empty', () => { const state = { editor: { - history: { - future: [], - }, + future: [], }, }; @@ -419,7 +411,9 @@ describe( 'selectors', () => { status: 'auto-draft', }, editor: { - edits: {}, + present: { + edits: {}, + }, }, }; @@ -432,7 +426,9 @@ describe( 'selectors', () => { status: 'draft', }, editor: { - edits: {}, + present: { + edits: {}, + }, }, }; @@ -638,7 +634,9 @@ describe( 'selectors', () => { it( 'should return the post edits', () => { const state = { editor: { - edits: { title: 'terga' }, + present: { + edits: { title: 'terga' }, + }, }, }; @@ -653,7 +651,9 @@ describe( 'selectors', () => { title: 'sassel', }, editor: { - edits: { status: 'private' }, + present: { + edits: { status: 'private' }, + }, }, }; @@ -666,7 +666,9 @@ describe( 'selectors', () => { title: 'sassel', }, editor: { - edits: { title: 'youcha' }, + present: { + edits: { title: 'youcha' }, + }, }, }; @@ -686,7 +688,11 @@ describe( 'selectors', () => { title: 'The Title', }, editor: { - edits: {}, + present: { + edits: {}, + blocksByUid: {}, + blockOrder: [], + }, }, metaBoxes, }; @@ -701,8 +707,10 @@ describe( 'selectors', () => { title: 'The Title', }, editor: { - edits: { - title: 'Modified Title', + present: { + edits: { + title: 'Modified Title', + }, }, }, metaBoxes, @@ -722,7 +730,11 @@ describe( 'selectors', () => { title: '', }, editor: { - edits: {}, + present: { + edits: {}, + blocksByUid: {}, + blockOrder: [], + }, }, metaBoxes, }; @@ -741,7 +753,11 @@ describe( 'selectors', () => { title: '', }, editor: { - edits: {}, + present: { + edits: {}, + blocksByUid: {}, + blockOrder: [], + }, }, metaBoxes, }; @@ -757,7 +773,9 @@ describe( 'selectors', () => { excerpt: 'sassel', }, editor: { - edits: { status: 'private' }, + present: { + edits: { status: 'private' }, + }, }, }; @@ -770,7 +788,9 @@ describe( 'selectors', () => { excerpt: 'sassel', }, editor: { - edits: { excerpt: 'youcha' }, + present: { + edits: { excerpt: 'youcha' }, + }, }, }; @@ -785,7 +805,9 @@ describe( 'selectors', () => { status: 'draft', }, editor: { - edits: {}, + present: { + edits: {}, + }, }, }; @@ -798,7 +820,9 @@ describe( 'selectors', () => { status: 'private', }, editor: { - edits: {}, + present: { + edits: {}, + }, }, }; @@ -812,7 +836,9 @@ describe( 'selectors', () => { password: 'chicken', }, editor: { - edits: {}, + present: { + edits: {}, + }, }, }; @@ -826,9 +852,11 @@ describe( 'selectors', () => { password: 'chicken', }, editor: { - edits: { - status: 'private', - password: null, + present: { + edits: { + status: 'private', + password: null, + }, }, }, }; @@ -989,9 +1017,11 @@ describe( 'selectors', () => { it( 'should return false if the post has no title, excerpt, content', () => { const state = { editor: { - blocksByUid: {}, - blockOrder: [], - edits: {}, + present: { + blocksByUid: {}, + blockOrder: [], + edits: {}, + }, }, currentPost: {}, }; @@ -1002,9 +1032,11 @@ describe( 'selectors', () => { it( 'should return true if the post has a title', () => { const state = { editor: { - blocksByUid: {}, - blockOrder: [], - edits: {}, + present: { + blocksByUid: {}, + blockOrder: [], + edits: {}, + }, }, currentPost: { title: 'sassel', @@ -1017,9 +1049,11 @@ describe( 'selectors', () => { it( 'should return true if the post has an excerpt', () => { const state = { editor: { - blocksByUid: {}, - blockOrder: [], - edits: {}, + present: { + blocksByUid: {}, + blockOrder: [], + edits: {}, + }, }, currentPost: { excerpt: 'sassel', @@ -1032,17 +1066,19 @@ describe( 'selectors', () => { it( 'should return true if the post has content', () => { const state = { editor: { - blocksByUid: { - 123: { - uid: 123, - name: 'core/test-block', - attributes: { - text: '', + present: { + blocksByUid: { + 123: { + uid: 123, + name: 'core/test-block', + attributes: { + text: '', + }, }, }, + blockOrder: [ 123 ], + edits: {}, }, - blockOrder: [ 123 ], - edits: {}, }, currentPost: {}, }; @@ -1055,7 +1091,9 @@ describe( 'selectors', () => { it( 'should return true for posts with a future date', () => { const state = { editor: { - edits: { date: moment().add( 7, 'days' ).format( '' ) }, + present: { + edits: { date: moment().add( 7, 'days' ).format( '' ) }, + }, }, }; @@ -1065,7 +1103,9 @@ describe( 'selectors', () => { it( 'should return false for posts with an old date', () => { const state = { editor: { - edits: { date: '2016-05-30T17:21:39' }, + present: { + edits: { date: '2016-05-30T17:21:39' }, + }, }, }; @@ -1098,10 +1138,12 @@ describe( 'selectors', () => { const state = { currentPost: {}, editor: { - blocksByUid: { - 123: { uid: 123, name: 'core/paragraph' }, + present: { + blocksByUid: { + 123: { uid: 123, name: 'core/paragraph' }, + }, + edits: {}, }, - edits: {}, }, }; @@ -1112,8 +1154,10 @@ describe( 'selectors', () => { const state = { currentPost: {}, editor: { - blocksByUid: {}, - edits: {}, + present: { + blocksByUid: {}, + edits: {}, + }, }, }; @@ -1140,10 +1184,12 @@ describe( 'selectors', () => { }, }, editor: { - blocksByUid: { - 123: { uid: 123, name: 'core/meta-block' }, + present: { + blocksByUid: { + 123: { uid: 123, name: 'core/meta-block' }, + }, + edits: {}, }, - edits: {}, }, }; @@ -1162,12 +1208,14 @@ describe( 'selectors', () => { const state = { currentPost: {}, editor: { - blocksByUid: { - 23: { uid: 23, name: 'core/heading' }, - 123: { uid: 123, name: 'core/paragraph' }, + present: { + blocksByUid: { + 23: { uid: 23, name: 'core/heading' }, + 123: { uid: 123, name: 'core/paragraph' }, + }, + blockOrder: [ 123, 23 ], + edits: {}, }, - blockOrder: [ 123, 23 ], - edits: {}, }, }; @@ -1182,11 +1230,13 @@ describe( 'selectors', () => { it( 'should return the number of blocks in the post', () => { const state = { editor: { - blocksByUid: { - 23: { uid: 23, name: 'core/heading' }, - 123: { uid: 123, name: 'core/paragraph' }, + present: { + blocksByUid: { + 23: { uid: 23, name: 'core/heading' }, + 123: { uid: 123, name: 'core/paragraph' }, + }, + blockOrder: [ 123, 23 ], }, - blockOrder: [ 123, 23 ], }, }; @@ -1199,11 +1249,13 @@ describe( 'selectors', () => { const state = { currentPost: {}, editor: { - blocksByUid: { - 23: { uid: 23, name: 'core/heading' }, - 123: { uid: 123, name: 'core/paragraph' }, + present: { + blocksByUid: { + 23: { uid: 23, name: 'core/heading' }, + 123: { uid: 123, name: 'core/paragraph' }, + }, + edits: {}, }, - edits: {}, }, blockSelection: { start: null, end: null }, }; @@ -1214,9 +1266,11 @@ describe( 'selectors', () => { it( 'should return null if there is multi selection', () => { const state = { editor: { - blocksByUid: { - 23: { uid: 23, name: 'core/heading' }, - 123: { uid: 123, name: 'core/paragraph' }, + present: { + blocksByUid: { + 23: { uid: 23, name: 'core/heading' }, + 123: { uid: 123, name: 'core/paragraph' }, + }, }, }, blockSelection: { start: 23, end: 123 }, @@ -1228,15 +1282,17 @@ describe( 'selectors', () => { it( 'should return the selected block', () => { const state = { editor: { - blocksByUid: { - 23: { uid: 23, name: 'core/heading' }, - 123: { uid: 123, name: 'core/paragraph' }, + present: { + blocksByUid: { + 23: { uid: 23, name: 'core/heading' }, + 123: { uid: 123, name: 'core/paragraph' }, + }, }, }, blockSelection: { start: 23, end: 23 }, }; - expect( getSelectedBlock( state ) ).toBe( state.editor.blocksByUid[ 23 ] ); + expect( getSelectedBlock( state ) ).toBe( state.editor.present.blocksByUid[ 23 ] ); } ); } ); @@ -1244,7 +1300,9 @@ describe( 'selectors', () => { it( 'should return empty if there is no multi selection', () => { const state = { editor: { - blockOrder: [ 123, 23 ], + present: { + blockOrder: [ 123, 23 ], + }, }, blockSelection: { start: null, end: null }, }; @@ -1255,7 +1313,9 @@ describe( 'selectors', () => { it( 'should return selected block uids if there is multi selection', () => { const state = { editor: { - blockOrder: [ 5, 4, 3, 2, 1 ], + present: { + blockOrder: [ 5, 4, 3, 2, 1 ], + }, }, blockSelection: { start: 2, end: 4 }, }; @@ -1268,7 +1328,9 @@ describe( 'selectors', () => { it( 'returns null if there is no multi selection', () => { const state = { editor: { - blockOrder: [ 123, 23 ], + present: { + blockOrder: [ 123, 23 ], + }, }, blockSelection: { start: null, end: null }, }; @@ -1279,7 +1341,9 @@ describe( 'selectors', () => { it( 'returns multi selection start', () => { const state = { editor: { - blockOrder: [ 5, 4, 3, 2, 1 ], + present: { + blockOrder: [ 5, 4, 3, 2, 1 ], + }, }, blockSelection: { start: 2, end: 4 }, }; @@ -1292,7 +1356,9 @@ describe( 'selectors', () => { it( 'returns null if there is no multi selection', () => { const state = { editor: { - blockOrder: [ 123, 23 ], + present: { + blockOrder: [ 123, 23 ], + }, }, blockSelection: { start: null, end: null }, }; @@ -1303,7 +1369,9 @@ describe( 'selectors', () => { it( 'returns multi selection end', () => { const state = { editor: { - blockOrder: [ 5, 4, 3, 2, 1 ], + present: { + blockOrder: [ 5, 4, 3, 2, 1 ], + }, }, blockSelection: { start: 2, end: 4 }, }; @@ -1316,7 +1384,9 @@ describe( 'selectors', () => { it( 'should return the ordered block UIDs', () => { const state = { editor: { - blockOrder: [ 123, 23 ], + present: { + blockOrder: [ 123, 23 ], + }, }, }; @@ -1328,7 +1398,9 @@ describe( 'selectors', () => { it( 'should return the block order', () => { const state = { editor: { - blockOrder: [ 123, 23 ], + present: { + blockOrder: [ 123, 23 ], + }, }, }; @@ -1340,7 +1412,9 @@ describe( 'selectors', () => { it( 'should return true when the block is first', () => { const state = { editor: { - blockOrder: [ 123, 23 ], + present: { + blockOrder: [ 123, 23 ], + }, }, }; @@ -1350,7 +1424,9 @@ describe( 'selectors', () => { it( 'should return false when the block is not first', () => { const state = { editor: { - blockOrder: [ 123, 23 ], + present: { + blockOrder: [ 123, 23 ], + }, }, }; @@ -1362,7 +1438,9 @@ describe( 'selectors', () => { it( 'should return true when the block is last', () => { const state = { editor: { - blockOrder: [ 123, 23 ], + present: { + blockOrder: [ 123, 23 ], + }, }, }; @@ -1372,7 +1450,9 @@ describe( 'selectors', () => { it( 'should return false when the block is not last', () => { const state = { editor: { - blockOrder: [ 123, 23 ], + present: { + blockOrder: [ 123, 23 ], + }, }, }; @@ -1384,11 +1464,13 @@ describe( 'selectors', () => { it( 'should return the previous block', () => { const state = { editor: { - blocksByUid: { - 23: { uid: 23, name: 'core/heading' }, - 123: { uid: 123, name: 'core/paragraph' }, + present: { + blocksByUid: { + 23: { uid: 23, name: 'core/heading' }, + 123: { uid: 123, name: 'core/paragraph' }, + }, + blockOrder: [ 123, 23 ], }, - blockOrder: [ 123, 23 ], }, }; @@ -1398,11 +1480,13 @@ describe( 'selectors', () => { it( 'should return null for the first block', () => { const state = { editor: { - blocksByUid: { - 23: { uid: 23, name: 'core/heading' }, - 123: { uid: 123, name: 'core/paragraph' }, + present: { + blocksByUid: { + 23: { uid: 23, name: 'core/heading' }, + 123: { uid: 123, name: 'core/paragraph' }, + }, + blockOrder: [ 123, 23 ], }, - blockOrder: [ 123, 23 ], }, }; @@ -1414,11 +1498,13 @@ describe( 'selectors', () => { it( 'should return the following block', () => { const state = { editor: { - blocksByUid: { - 23: { uid: 23, name: 'core/heading' }, - 123: { uid: 123, name: 'core/paragraph' }, + present: { + blocksByUid: { + 23: { uid: 23, name: 'core/heading' }, + 123: { uid: 123, name: 'core/paragraph' }, + }, + blockOrder: [ 123, 23 ], }, - blockOrder: [ 123, 23 ], }, }; @@ -1428,11 +1514,13 @@ describe( 'selectors', () => { it( 'should return null for the last block', () => { const state = { editor: { - blocksByUid: { - 23: { uid: 23, name: 'core/heading' }, - 123: { uid: 123, name: 'core/paragraph' }, + present: { + blocksByUid: { + 23: { uid: 23, name: 'core/heading' }, + 123: { uid: 123, name: 'core/paragraph' }, + }, + blockOrder: [ 123, 23 ], }, - blockOrder: [ 123, 23 ], }, }; @@ -1469,7 +1557,9 @@ describe( 'selectors', () => { describe( 'isBlockMultiSelected', () => { const state = { editor: { - blockOrder: [ 5, 4, 3, 2, 1 ], + present: { + blockOrder: [ 5, 4, 3, 2, 1 ], + }, }, blockSelection: { start: 2, end: 4 }, }; @@ -1486,7 +1576,9 @@ describe( 'selectors', () => { describe( 'isFirstMultiSelectedBlock', () => { const state = { editor: { - blockOrder: [ 5, 4, 3, 2, 1 ], + present: { + blockOrder: [ 5, 4, 3, 2, 1 ], + }, }, blockSelection: { start: 2, end: 4 }, }; @@ -1616,11 +1708,13 @@ describe( 'selectors', () => { end: 2, }, editor: { - blocksByUid: { - 2: { uid: 2 }, + present: { + blocksByUid: { + 2: { uid: 2 }, + }, + blockOrder: [ 1, 2, 3 ], + edits: {}, }, - blockOrder: [ 1, 2, 3 ], - edits: {}, }, blockInsertionPoint: {}, }; @@ -1633,7 +1727,9 @@ describe( 'selectors', () => { preferences: { mode: 'visual' }, blockSelection: {}, editor: { - blockOrder: [ 1, 2, 3 ], + present: { + blockOrder: [ 1, 2, 3 ], + }, }, blockInsertionPoint: { position: 2, @@ -1651,7 +1747,9 @@ describe( 'selectors', () => { end: 2, }, editor: { - blockOrder: [ 1, 2, 3 ], + present: { + blockOrder: [ 1, 2, 3 ], + }, }, blockInsertionPoint: {}, }; @@ -1664,7 +1762,9 @@ describe( 'selectors', () => { preferences: { mode: 'visual' }, blockSelection: { start: null, end: null }, editor: { - blockOrder: [ 1, 2, 3 ], + present: { + blockOrder: [ 1, 2, 3 ], + }, }, blockInsertionPoint: {}, }; @@ -1677,7 +1777,9 @@ describe( 'selectors', () => { preferences: { mode: 'text' }, blockSelection: { start: 2, end: 2 }, editor: { - blockOrder: [ 1, 2, 3 ], + present: { + blockOrder: [ 1, 2, 3 ], + }, }, blockInsertionPoint: {}, }; @@ -1788,8 +1890,10 @@ describe( 'selectors', () => { it( 'returns null if cannot be determined', () => { const state = { editor: { - blockOrder: [], - blocksByUid: {}, + present: { + blockOrder: [], + blocksByUid: {}, + }, }, }; @@ -1799,10 +1903,12 @@ describe( 'selectors', () => { it( 'returns null if there is more than one block in the post', () => { const state = { editor: { - blockOrder: [ 123, 456 ], - blocksByUid: { - 123: { uid: 123, name: 'core/image' }, - 456: { uid: 456, name: 'core/quote' }, + present: { + blockOrder: [ 123, 456 ], + blocksByUid: { + 123: { uid: 123, name: 'core/image' }, + 456: { uid: 456, name: 'core/quote' }, + }, }, }, }; @@ -1813,9 +1919,11 @@ describe( 'selectors', () => { it( 'returns Image if the first block is of type `core/image`', () => { const state = { editor: { - blockOrder: [ 123 ], - blocksByUid: { - 123: { uid: 123, name: 'core/image' }, + present: { + blockOrder: [ 123 ], + blocksByUid: { + 123: { uid: 123, name: 'core/image' }, + }, }, }, }; @@ -1826,9 +1934,11 @@ describe( 'selectors', () => { it( 'returns Quote if the first block is of type `core/quote`', () => { const state = { editor: { - blockOrder: [ 456 ], - blocksByUid: { - 456: { uid: 456, name: 'core/quote' }, + present: { + blockOrder: [ 456 ], + blocksByUid: { + 456: { uid: 456, name: 'core/quote' }, + }, }, }, }; @@ -1839,9 +1949,11 @@ describe( 'selectors', () => { it( 'returns Video if the first block is of type `core-embed/youtube`', () => { const state = { editor: { - blockOrder: [ 567 ], - blocksByUid: { - 567: { uid: 567, name: 'core-embed/youtube' }, + present: { + blockOrder: [ 567 ], + blocksByUid: { + 567: { uid: 567, name: 'core-embed/youtube' }, + }, }, }, }; @@ -1852,10 +1964,12 @@ describe( 'selectors', () => { it( 'returns Quote if the first block is of type `core/quote` and second is of type `core/paragraph`', () => { const state = { editor: { - blockOrder: [ 456, 789 ], - blocksByUid: { - 456: { uid: 456, name: 'core/quote' }, - 789: { uid: 789, name: 'core/paragraph' }, + present: { + blockOrder: [ 456, 789 ], + blocksByUid: { + 456: { uid: 456, name: 'core/quote' }, + 789: { uid: 789, name: 'core/paragraph' }, + }, }, }, }; diff --git a/editor/utils/test/undoable-reducer.js b/editor/utils/test/undoable-reducer.js index 9423cd8fe4f3ba..9bd3e7568db4f3 100644 --- a/editor/utils/test/undoable-reducer.js +++ b/editor/utils/test/undoable-reducer.js @@ -1,142 +1,113 @@ /** * Internal dependencies */ -import { undoable, combineUndoableReducers } from '../undoable-reducer'; +import undoableReducer from '../undoable-reducer'; describe( 'undoableReducer', () => { - describe( 'undoable()', () => { - const counter = ( state = 0, { type } ) => ( - type === 'INCREMENT' ? state + 1 : state - ); - - it( 'should return a new reducer', () => { - const reducer = undoable( counter ); - - expect( typeof reducer ).toBe( 'function' ); - expect( reducer( undefined, {} ) ).toEqual( { - past: [], - present: 0, - future: [], - } ); + const counter = ( state = 0, { type } ) => ( + type === 'INCREMENT' ? state + 1 : state + ); + + it( 'should return a new reducer', () => { + const reducer = undoableReducer( counter ); + + expect( typeof reducer ).toBe( 'function' ); + expect( reducer( undefined, {} ) ).toEqual( { + past: [], + present: 0, + future: [], } ); + } ); - it( 'should track history', () => { - const reducer = undoable( counter ); + it( 'should track history', () => { + const reducer = undoableReducer( counter ); - let state; - state = reducer( undefined, {} ); - state = reducer( state, { type: 'INCREMENT' } ); + let state; + state = reducer( undefined, {} ); + state = reducer( state, { type: 'INCREMENT' } ); - expect( state ).toEqual( { - past: [ 0 ], - present: 1, - future: [], - } ); + expect( state ).toEqual( { + past: [ 0 ], + present: 1, + future: [], } ); + } ); - it( 'should perform undo', () => { - const reducer = undoable( counter ); + it( 'should perform undo', () => { + const reducer = undoableReducer( counter ); - let state; - state = reducer( undefined, {} ); - state = reducer( state, { type: 'INCREMENT' } ); - state = reducer( state, { type: 'UNDO' } ); + let state; + state = reducer( undefined, {} ); + state = reducer( state, { type: 'INCREMENT' } ); + state = reducer( state, { type: 'UNDO' } ); - expect( state ).toEqual( { - past: [], - present: 0, - future: [ 1 ], - } ); + expect( state ).toEqual( { + past: [], + present: 0, + future: [ 1 ], } ); + } ); - it( 'should not perform undo on empty past', () => { - const reducer = undoable( counter ); + it( 'should not perform undo on empty past', () => { + const reducer = undoableReducer( counter ); - let state; - state = reducer( undefined, {} ); - state = reducer( state, { type: 'INCREMENT' } ); - state = reducer( state, { type: 'UNDO' } ); + let state; + state = reducer( undefined, {} ); + state = reducer( state, { type: 'INCREMENT' } ); + state = reducer( state, { type: 'UNDO' } ); - expect( state ).toEqual( { - past: [], - present: 0, - future: [ 1 ], - } ); + expect( state ).toEqual( { + past: [], + present: 0, + future: [ 1 ], } ); + } ); - it( 'should perform redo', () => { - const reducer = undoable( counter ); + it( 'should perform redo', () => { + const reducer = undoableReducer( counter ); - let state; - state = reducer( undefined, {} ); - state = reducer( state, { type: 'INCREMENT' } ); - state = reducer( state, { type: 'UNDO' } ); - state = reducer( state, { type: 'UNDO' } ); + let state; + state = reducer( undefined, {} ); + state = reducer( state, { type: 'INCREMENT' } ); + state = reducer( state, { type: 'UNDO' } ); + state = reducer( state, { type: 'UNDO' } ); - expect( state ).toEqual( { - past: [], - present: 0, - future: [ 1 ], - } ); + expect( state ).toEqual( { + past: [], + present: 0, + future: [ 1 ], } ); + } ); - it( 'should not perform redo on empty future', () => { - const reducer = undoable( counter ); - - let state; - state = reducer( undefined, {} ); - state = reducer( state, { type: 'INCREMENT' } ); - state = reducer( state, { type: 'REDO' } ); + it( 'should not perform redo on empty future', () => { + const reducer = undoableReducer( counter ); - expect( state ).toEqual( { - past: [ 0 ], - present: 1, - future: [], - } ); - } ); + let state; + state = reducer( undefined, {} ); + state = reducer( state, { type: 'INCREMENT' } ); + state = reducer( state, { type: 'REDO' } ); - it( 'should reset history by options.resetTypes', () => { - const reducer = undoable( counter, { resetTypes: [ 'RESET_HISTORY' ] } ); - - let state; - state = reducer( undefined, {} ); - state = reducer( state, { type: 'INCREMENT' } ); - state = reducer( state, { type: 'RESET_HISTORY' } ); - state = reducer( state, { type: 'INCREMENT' } ); - state = reducer( state, { type: 'INCREMENT' } ); - - expect( state ).toEqual( { - past: [ 1, 2 ], - present: 3, - future: [], - } ); + expect( state ).toEqual( { + past: [ 0 ], + present: 1, + future: [], } ); } ); - describe( 'combineUndoableReducers()', () => { - const reducer = combineUndoableReducers( { - count: ( state = 0 ) => state, - } ); - - it( 'should return a combined reducer with getters', () => { - const state = reducer( undefined, {} ); - - expect( typeof reducer ).toBe( 'function' ); - expect( state.count ).toBe( 0 ); - expect( state.history ).toEqual( { - past: [], - present: { - count: 0, - }, - future: [], - } ); - } ); + it( 'should reset history by options.resetTypes', () => { + const reducer = undoableReducer( counter, { resetTypes: [ 'RESET_HISTORY' ] } ); - it( 'should return same reference if state has not changed', () => { - const original = reducer( undefined, {} ); - const state = reducer( original, {} ); + let state; + state = reducer( undefined, {} ); + state = reducer( state, { type: 'INCREMENT' } ); + state = reducer( state, { type: 'RESET_HISTORY' } ); + state = reducer( state, { type: 'INCREMENT' } ); + state = reducer( state, { type: 'INCREMENT' } ); - expect( state ).toBe( original ); + expect( state ).toEqual( { + past: [ 1, 2 ], + present: 3, + future: [], } ); } ); } ); diff --git a/editor/utils/undoable-reducer.js b/editor/utils/undoable-reducer.js index a7d2e97681df9b..e4c9e0368503a5 100644 --- a/editor/utils/undoable-reducer.js +++ b/editor/utils/undoable-reducer.js @@ -1,7 +1,6 @@ /** * External dependencies */ -import { combineReducers } from 'redux'; import { includes } from 'lodash'; /** @@ -13,7 +12,7 @@ import { includes } from 'lodash'; * @param {?Array} options.resetTypes Action types upon which to clear past * @return {Function} Enhanced reducer */ -export function undoable( reducer, options = {} ) { +export default function undoableReducer( reducer, options = {} ) { const initialState = { past: [], present: reducer( undefined, {} ), @@ -70,46 +69,3 @@ export function undoable( reducer, options = {} ) { }; }; } - -/** - * A wrapper for combineReducers which applies an undo history to the combined - * reducer. As a convenience, properties of the reducers object are accessible - * via object getters, with history assigned to a nested history property. - * - * @see undoable - * - * @param {Object} reducers Object of reducers - * @param {?Object} options Optional options - * @return {Function} Combined reducer - */ -export function combineUndoableReducers( reducers, options ) { - const reducer = undoable( combineReducers( reducers ), options ); - - function withGetters( history ) { - const state = { history }; - const keys = Object.getOwnPropertyNames( history.present ); - const getters = keys.reduce( ( memo, key ) => { - memo[ key ] = { - get: function() { - return this.history.present[ key ]; - }, - }; - - return memo; - }, {} ); - Object.defineProperties( state, getters ); - - return state; - } - - const initialState = withGetters( reducer( undefined, {} ) ); - - return ( state = initialState, action ) => { - const nextState = reducer( state.history, action ); - if ( nextState === state.history ) { - return state; - } - - return withGetters( nextState ); - }; -}