Skip to content

Commit

Permalink
Merge pull request #1996 from WordPress/update/diff-attributes-dirty
Browse files Browse the repository at this point in the history
Perform dirty detection as diff against saved post
  • Loading branch information
aduth authored Aug 8, 2017
2 parents d2788f5 + da5cbe7 commit acf4297
Show file tree
Hide file tree
Showing 12 changed files with 690 additions and 305 deletions.
1 change: 0 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@
"no-undef-init": "error",
"no-unreachable": "error",
"no-unsafe-negation": "error",
"no-use-before-define": [ "error", "nofunc" ],
"no-useless-computed-key": "error",
"no-useless-constructor": "error",
"no-useless-return": "error",
Expand Down
27 changes: 15 additions & 12 deletions editor/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@
import uuid from 'uuid/v4';
import { partial } from 'lodash';

/**
* Returns an action object used in signalling that blocks state should be
* reset to the specified array of blocks, taking precedence over any other
* content reflected as an edit in state.
*
* @param {Array} blocks Array of blocks
* @return {Object} Action object
*/
export function resetBlocks( blocks ) {
return {
type: 'RESET_BLOCKS',
blocks,
};
}

/**
* Returns an action object used in signalling that the block with the
* specified UID has been updated.
Expand Down Expand Up @@ -120,18 +135,6 @@ export function autosave() {
};
}

/**
* Returns an action object used in signalling that the post should be queued
* for autosave after a delay.
*
* @return {Object} Action object
*/
export function queueAutosave() {
return {
type: 'QUEUE_AUTOSAVE',
};
}

/**
* Returns an action object used in signalling that undo history should
* restore last popped state.
Expand Down
57 changes: 57 additions & 0 deletions editor/autosave-monitor/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* External dependencies
*/
import { connect } from 'react-redux';

/**
* WordPress dependencies
*/
import { Component } from '@wordpress/element';

/**
* Internal dependencies
*/
import { autosave } from '../actions';
import {
isEditedPostDirty,
isEditedPostSaveable,
} from '../selectors';

export class AutosaveMonitor extends Component {
componentDidUpdate( prevProps ) {
const { isDirty, isSaveable } = this.props;
if ( prevProps.isDirty !== isDirty ||
prevProps.isSaveable !== isSaveable ) {
this.toggleTimer( isDirty && isSaveable );
}
}

componentWillUnmount() {
this.toggleTimer( false );
}

toggleTimer( isPendingSave ) {
clearTimeout( this.pendingSave );

if ( isPendingSave ) {
this.pendingSave = setTimeout(
() => this.props.autosave(),
10000
);
}
}

render() {
return null;
}
}

export default connect(
( state ) => {
return {
isDirty: isEditedPostDirty( state ),
isSaveable: isEditedPostSaveable( state ),
};
},
{ autosave }
)( AutosaveMonitor );
67 changes: 67 additions & 0 deletions editor/autosave-monitor/test/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* External dependencies
*/
import { shallow } from 'enzyme';

/**
* Internal dependencies
*/
import { AutosaveMonitor } from '../';

describe( 'AutosaveMonitor', () => {
const toggleTimer = jest.fn();
let wrapper;
beforeEach( () => {
toggleTimer.mockClear();
wrapper = shallow(
<AutosaveMonitor />,
{ lifecycleExperimental: true }
);

wrapper.instance().toggleTimer = toggleTimer;
} );

describe( '#componentDidUpdate()', () => {
it( 'should start autosave timer when having become dirty and saveable', () => {
wrapper.setProps( { isDirty: true, isSaveable: true } );

expect( toggleTimer ).toHaveBeenCalledWith( true );
} );

it( 'should stop autosave timer when having become dirty but not saveable', () => {
wrapper.setProps( { isDirty: true, isSaveable: false } );

expect( toggleTimer ).toHaveBeenCalledWith( false );
} );

it( 'should stop autosave timer when having become not dirty', () => {
wrapper.setProps( { isDirty: true } );
toggleTimer.mockClear();
wrapper.setProps( { isDirty: false } );

expect( toggleTimer ).toHaveBeenCalledWith( false );
} );

it( 'should stop autosave timer when having become not saveable', () => {
wrapper.setProps( { isDirty: true } );
toggleTimer.mockClear();
wrapper.setProps( { isSaveable: false } );

expect( toggleTimer ).toHaveBeenCalledWith( false );
} );
} );

describe( '#componentWillUnmount()', () => {
it( 'should stop autosave timer', () => {
wrapper.unmount();

expect( toggleTimer ).toHaveBeenCalledWith( false );
} );
} );

describe( '#render()', () => {
it( 'should render nothing', () => {
expect( wrapper.type() ).toBe( null );
} );
} );
} );
34 changes: 12 additions & 22 deletions editor/effects.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,31 @@
* External dependencies
*/
import { BEGIN, COMMIT, REVERT } from 'redux-optimist';
import { get, uniqueId, debounce } from 'lodash';
import { get, uniqueId } from 'lodash';

/**
* WordPress dependencies
*/
import { serialize, getBlockType, switchToBlockType } from '@wordpress/blocks';
import { parse, getBlockType, switchToBlockType } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import { getGutenbergURL, getWPAdminURL } from './utils/url';
import {
resetBlocks,
focusBlock,
replaceBlocks,
createSuccessNotice,
createErrorNotice,
autosave,
queueAutosave,
savePost,
editPost,
} from './actions';
import {
getCurrentPost,
getCurrentPostType,
getBlocks,
getEditedPostContent,
getPostEdits,
isCurrentPostPublished,
isEditedPostDirty,
Expand All @@ -43,19 +42,15 @@ export default {
const edits = getPostEdits( state );
const toSend = {
...edits,
content: serialize( getBlocks( state ) ),
content: getEditedPostContent( state ),
id: post.id,
};
const transactionId = uniqueId();

dispatch( {
type: 'CLEAR_POST_EDITS',
optimist: { type: BEGIN, id: transactionId },
} );
dispatch( {
type: 'UPDATE_POST',
edits: toSend,
optimist: { id: transactionId },
optimist: { type: BEGIN, id: transactionId },
} );
const Model = wp.api.getPostTypeModel( getCurrentPostType( state ) );
new Model( toSend ).save().done( ( newPost ) => {
Expand Down Expand Up @@ -247,15 +242,10 @@ export default {

dispatch( savePost() );
},
QUEUE_AUTOSAVE: debounce( ( action, store ) => {
store.dispatch( autosave() );
}, 10000 ),
UPDATE_BLOCK_ATTRIBUTES: () => queueAutosave(),
INSERT_BLOCKS: () => queueAutosave(),
MOVE_BLOCKS_DOWN: () => queueAutosave(),
MOVE_BLOCKS_UP: () => queueAutosave(),
REPLACE_BLOCKS: () => queueAutosave(),
REMOVE_BLOCKS: () => queueAutosave(),
EDIT_POST: () => queueAutosave(),
MARK_DIRTY: () => queueAutosave(),
RESET_POST( action ) {
const { post } = action;
if ( post.content ) {
return resetBlocks( parse( post.content.raw ) );
}
},
};
10 changes: 1 addition & 9 deletions editor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import 'moment-timezone/moment-timezone-utils';
/**
* WordPress dependencies
*/
import { EditableProvider, parse } from '@wordpress/blocks';
import { EditableProvider } from '@wordpress/blocks';
import { render } from '@wordpress/element';
import { settings } from '@wordpress/date';

Expand Down Expand Up @@ -65,14 +65,6 @@ function preparePostState( store, post ) {
post,
} );

// Parse content as blocks
if ( post.content.raw ) {
store.dispatch( {
type: 'RESET_BLOCKS',
blocks: parse( post.content.raw ),
} );
}

// Include auto draft title in edits while not flagging post as dirty
if ( post.status === 'auto-draft' ) {
store.dispatch( {
Expand Down
2 changes: 2 additions & 0 deletions editor/layout/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import TextEditor from '../modes/text-editor';
import VisualEditor from '../modes/visual-editor';
import UnsavedChangesWarning from '../unsaved-changes-warning';
import DocumentTitle from '../document-title';
import AutosaveMonitor from '../autosave-monitor';
import { removeNotice } from '../actions';
import {
getEditorMode,
Expand All @@ -36,6 +37,7 @@ function Layout( { mode, isSidebarOpened, notices, ...props } ) {
<DocumentTitle />
<NoticeList onRemove={ props.removeNotice } notices={ notices } />
<UnsavedChangesWarning />
<AutosaveMonitor />
<Header />
<div className="editor-layout__content">
{ mode === 'text' && <TextEditor /> }
Expand Down
Loading

0 comments on commit acf4297

Please sign in to comment.