Skip to content

Commit

Permalink
Perform dirty detection as diff against saved post
Browse files Browse the repository at this point in the history
  • Loading branch information
aduth committed Aug 8, 2017
1 parent 4d13586 commit da5cbe7
Show file tree
Hide file tree
Showing 11 changed files with 690 additions and 304 deletions.
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
64 changes: 22 additions & 42 deletions editor/modes/text-editor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,60 +8,45 @@ import Textarea from 'react-autosize-textarea';
* WordPress dependencies
*/
import { Component } from '@wordpress/element';
import { serialize, parse } from '@wordpress/blocks';
import { parse } from '@wordpress/blocks';

/**
* Internal dependencies
*/
import './style.scss';
import PostTitle from '../../post-title';
import { getBlocks } from '../../selectors';
import { getEditedPostContent } from '../../selectors';
import { editPost, resetBlocks } from '../../actions';

class TextEditor extends Component {
constructor( { blocks } ) {
constructor( props ) {
super( ...arguments );
const value = serialize( blocks );

this.onChange = this.onChange.bind( this );
this.onPersist = this.onPersist.bind( this );

this.state = {
blocks,
persistedValue: value,
value,
initialValue: props.value,
};
this.onChange = this.onChange.bind( this );
this.onBlur = this.onBlur.bind( this );
}

onChange( event ) {
this.setState( {
value: event.target.value,
} );
this.props.markDirty();
this.props.onChange( event.target.value );
}

onBlur() {
if ( this.state.value === this.state.persistedValue ) {
return;
}
const blocks = parse( this.state.value );
this.setState( {
blocks,
} );
this.props.onChange( blocks );
this.props.markDirty();
}
onPersist( event ) {
const { value } = event.target;
if ( value !== this.state.initialValue ) {
this.props.onPersist( value );

componentWillReceiveProps( newProps ) {
if ( newProps.blocks !== this.state.blocks ) {
const value = serialize( newProps.blocks );
this.setState( {
blocks: newProps.blocks,
persistedValue: value,
value,
initialValue: value,
} );
}
}

render() {
const { value } = this.state;
const { value } = this.props;

return (
<div className="editor-text-editor">
Expand All @@ -88,7 +73,7 @@ class TextEditor extends Component {
autoComplete="off"
value={ value }
onChange={ this.onChange }
onBlur={ this.onBlur }
onBlur={ this.onPersist }
className="editor-text-editor__textarea"
/>
</div>
Expand All @@ -99,19 +84,14 @@ class TextEditor extends Component {

export default connect(
( state ) => ( {
blocks: getBlocks( state ),
value: getEditedPostContent( state ),
} ),
{
onChange( blocks ) {
return {
type: 'RESET_BLOCKS',
blocks,
};
onChange( content ) {
return editPost( { content } );
},
markDirty() {
return {
type: 'MARK_DIRTY',
};
onPersist( content ) {
return resetBlocks( parse( content ) );
},
}
)( TextEditor );
Loading

0 comments on commit da5cbe7

Please sign in to comment.