Skip to content

Commit

Permalink
Store: Reset change detection on post update (#6209)
Browse files Browse the repository at this point in the history
* Store: Preserve change detection isDirty for ignored types

* Store: Reset change detection on post update

* Editor: Refactor dirtiness condition on pending transactions

* Testing: Add pressWithModifier E2E test utility
  • Loading branch information
aduth authored May 1, 2018
1 parent 5a5486b commit ac89ff3
Show file tree
Hide file tree
Showing 10 changed files with 344 additions and 34 deletions.
4 changes: 2 additions & 2 deletions editor/store/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,8 @@ export const editor = flow( [
// Track whether changes exist, resetting at each post save. Relies on
// editor initialization firing post reset as an effect.
withChangeDetection( {
resetTypes: [ 'SETUP_EDITOR_STATE', 'RESET_POST' ],
ignoreTypes: [ 'RECEIVE_BLOCKS' ],
resetTypes: [ 'SETUP_EDITOR_STATE', 'UPDATE_POST' ],
ignoreTypes: [ 'RECEIVE_BLOCKS', 'RESET_POST' ],
} ),
] )( {
edits( state = {}, action ) {
Expand Down
24 changes: 23 additions & 1 deletion editor/store/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export function isEditedPostNew( state ) {
* @return {boolean} Whether unsaved values exist.
*/
export function isEditedPostDirty( state ) {
return state.editor.isDirty;
return state.editor.isDirty || inSomeHistory( state, isEditedPostDirty );
}

/**
Expand Down Expand Up @@ -1568,3 +1568,25 @@ export function getPermalinkParts( state ) {
suffix,
};
}

/**
* Returns true if an optimistic transaction is pending commit, for which the
* before state satisfies the given predicate function.
*
* @param {Object} state Editor state.
* @param {Function} predicate Function given state, returning true if match.
*
* @return {boolean} Whether predicate matches for some history.
*/
export function inSomeHistory( state, predicate ) {
const { optimist } = state;

// In recursion, optimist state won't exist. Assume exhausted options.
if ( ! optimist ) {
return false;
}

return optimist.some( ( { beforeState } ) => (
beforeState && predicate( beforeState )
) );
}
67 changes: 67 additions & 0 deletions editor/store/test/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,28 @@ describe( 'selectors', () => {
editor: {
isDirty: true,
},
saving: {
requesting: false,
},
};

expect( isEditedPostDirty( state ) ).toBe( true );
} );

it( 'should return true if pending transaction with dirty state', () => {
const state = {
optimist: [
{
beforeState: {
editor: {
isDirty: true,
},
},
},
],
editor: {
isDirty: false,
},
};

expect( isEditedPostDirty( state ) ).toBe( true );
Expand All @@ -214,6 +236,9 @@ describe( 'selectors', () => {
editor: {
isDirty: false,
},
saving: {
requesting: false,
},
};

expect( isEditedPostDirty( state ) ).toBe( false );
Expand All @@ -230,6 +255,9 @@ describe( 'selectors', () => {
id: 1,
status: 'auto-draft',
},
saving: {
requesting: false,
},
};

expect( isCleanNewPost( state ) ).toBe( true );
Expand All @@ -244,6 +272,9 @@ describe( 'selectors', () => {
id: 1,
status: 'draft',
},
saving: {
requesting: false,
},
};

expect( isCleanNewPost( state ) ).toBe( false );
Expand All @@ -258,6 +289,9 @@ describe( 'selectors', () => {
id: 1,
status: 'auto-draft',
},
saving: {
requesting: false,
},
};

expect( isCleanNewPost( state ) ).toBe( false );
Expand Down Expand Up @@ -432,6 +466,9 @@ describe( 'selectors', () => {
},
isDirty: false,
},
saving: {
requesting: false,
},
};

expect( getDocumentTitle( state ) ).toBe( 'The Title' );
Expand All @@ -450,6 +487,9 @@ describe( 'selectors', () => {
},
},
},
saving: {
requesting: false,
},
};

expect( getDocumentTitle( state ) ).toBe( 'Modified Title' );
Expand All @@ -470,6 +510,9 @@ describe( 'selectors', () => {
},
isDirty: false,
},
saving: {
requesting: false,
},
};

expect( getDocumentTitle( state ) ).toBe( __( 'New post' ) );
Expand All @@ -490,6 +533,9 @@ describe( 'selectors', () => {
},
isDirty: true,
},
saving: {
requesting: false,
},
};

expect( getDocumentTitle( state ) ).toBe( __( '(Untitled)' ) );
Expand Down Expand Up @@ -719,6 +765,9 @@ describe( 'selectors', () => {
currentPost: {
status: 'pending',
},
saving: {
requesting: false,
},
};

expect( isEditedPostPublishable( state ) ).toBe( true );
Expand All @@ -732,6 +781,9 @@ describe( 'selectors', () => {
currentPost: {
status: 'draft',
},
saving: {
requesting: false,
},
};

expect( isEditedPostPublishable( state ) ).toBe( true );
Expand All @@ -745,6 +797,9 @@ describe( 'selectors', () => {
currentPost: {
status: 'publish',
},
saving: {
requesting: false,
},
};

expect( isEditedPostPublishable( state ) ).toBe( false );
Expand All @@ -758,6 +813,9 @@ describe( 'selectors', () => {
currentPost: {
status: 'publish',
},
saving: {
requesting: false,
},
};

expect( isEditedPostPublishable( state ) ).toBe( true );
Expand All @@ -771,6 +829,9 @@ describe( 'selectors', () => {
currentPost: {
status: 'private',
},
saving: {
requesting: false,
},
};

expect( isEditedPostPublishable( state ) ).toBe( false );
Expand All @@ -784,6 +845,9 @@ describe( 'selectors', () => {
currentPost: {
status: 'future',
},
saving: {
requesting: false,
},
};

expect( isEditedPostPublishable( state ) ).toBe( false );
Expand All @@ -797,6 +861,9 @@ describe( 'selectors', () => {
editor: {
isDirty: true,
},
saving: {
requesting: false,
},
};

expect( isEditedPostPublishable( state ) ).toBe( true );
Expand Down
31 changes: 18 additions & 13 deletions editor/utils/with-change-detection/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,7 @@ import { includes } from 'lodash';
*/
const withChangeDetection = ( options = {} ) => ( reducer ) => {
return ( state, action ) => {
const nextState = reducer( state, action );

if ( includes( options.ignoreTypes, action.type ) ) {
return nextState;
}
let nextState = reducer( state, action );

// Reset at:
// - Initial state
Expand All @@ -30,17 +26,26 @@ const withChangeDetection = ( options = {} ) => ( reducer ) => {
includes( options.resetTypes, action.type )
);

if ( isReset ) {
return {
...nextState,
isDirty: false,
};
const isChanging = state !== nextState;

// If not intending to update dirty flag, return early and avoid clone.
if ( ! isChanging && ! isReset ) {
return state;
}

const isChanging = state !== nextState;
// Avoid mutating state, unless it's already changing by original
// reducer and not initial.
if ( ! isChanging || state === undefined ) {
nextState = { ...nextState };
}

const isIgnored = includes( options.ignoreTypes, action.type );

if ( isChanging ) {
nextState.isDirty = true;
if ( isIgnored ) {
// Preserve the original value if ignored.
nextState.isDirty = state.isDirty;
} else {
nextState.isDirty = ! isReset && isChanging;
}

return nextState;
Expand Down
13 changes: 9 additions & 4 deletions editor/utils/with-change-detection/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,14 @@ describe( 'withChangeDetection()', () => {
function originalReducer( state = initialState, action ) {
switch ( action.type ) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
return {
count: state.count + 1,
};

case 'RESET_AND_CHANGE_REFERENCE':
return { ...state };
return {
count: state.count,
};
}

return state;
Expand Down Expand Up @@ -72,8 +76,9 @@ describe( 'withChangeDetection()', () => {
state = reducer( deepFreeze( state ), { type: 'INCREMENT' } );
expect( state ).toEqual( { count: 1, isDirty: true } );

state = reducer( deepFreeze( state ), {} );
expect( state ).toEqual( { count: 1, isDirty: true } );
const afterState = reducer( deepFreeze( state ), {} );
expect( afterState ).toEqual( { count: 1, isDirty: true } );
expect( afterState ).toBe( state );
} );

it( 'should maintain separate states', () => {
Expand Down
6 changes: 2 additions & 4 deletions test/e2e/specs/a11y.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Internal dependencies
*/
import '../support/bootstrap';
import { newPost, newDesktopBrowserPage } from '../support/utils';
import { newPost, newDesktopBrowserPage, pressWithModifier } from '../support/utils';

describe( 'a11y', () => {
beforeAll( async () => {
Expand All @@ -11,9 +11,7 @@ describe( 'a11y', () => {
} );

it( 'tabs header bar', async () => {
await page.keyboard.down( 'Control' );
await page.keyboard.press( '~' );
await page.keyboard.up( 'Control' );
await pressWithModifier( 'Control', '~' );

await page.keyboard.press( 'Tab' );

Expand Down
Loading

0 comments on commit ac89ff3

Please sign in to comment.