Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Perform dirty detection as diff against saved post #1996

Merged
merged 2 commits into from
Aug 8, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -243,15 +238,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