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

Improve autosaves #4156

Closed
wants to merge 73 commits into from
Closed
Show file tree
Hide file tree
Changes from 60 commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
08cdb15
Merge branch 'master' of github.com:WordPress/gutenberg
Oct 19, 2017
9d6b95c
Merge branch 'master' of github.com:WordPress/gutenberg
Oct 27, 2017
1e9572c
Merge branch 'master' of github.com:WordPress/gutenberg
Nov 16, 2017
b82fa9d
Merge branch 'master' of github.com:WordPress/gutenberg
Nov 26, 2017
cbd0c62
Merge branch 'master' of github.com:WordPress/gutenberg
Dec 8, 2017
9374837
Merge branch 'master' of github.com:WordPress/gutenberg
Dec 18, 2017
218299c
Merge branch 'master' of github.com:WordPress/gutenberg
Dec 20, 2017
bb7b896
Create per user autosaves, first pass.
Dec 20, 2017
3ee6738
no message for autosaves
Dec 21, 2017
2e3ec2e
Tag the autosave action to avoid creating revisions.
Dec 21, 2017
86b70c2
pass options to savePost
Dec 21, 2017
db02aea
cap check before storing autosave
Dec 21, 2017
e82ffc1
Set and return an autosave ID
Dec 21, 2017
9e5ccf8
Don’t show link top save published posts (autosave)
Dec 21, 2017
968241d
WP_REST_Autosaves_Controller first pass
Dec 22, 2017
52110b7
Autosaves: add support for create_item
Dec 22, 2017
9997325
Add an autosavePost action
Dec 24, 2017
f389030
REQUEST_POST_AUTOSAVE / effects for autosave
Dec 24, 2017
4b31a3c
remove unused query var set
Dec 24, 2017
36e3183
Autosave controller cleanup
Dec 24, 2017
f4f0d47
Add a wp.api.getPostTypeAutosaveModel helper
Dec 24, 2017
e484958
Register autosave controllers for posts types that support it.
Dec 24, 2017
4d81b62
undo some save changes
Dec 24, 2017
15e0573
whitespace/undo change
Dec 24, 2017
6bf285c
simplify autosave
Dec 24, 2017
39fb050
autosave controller cleanup
Dec 24, 2017
a5b26db
actions: revert savePost changes
Dec 24, 2017
17c26ca
move create_autosave to autosave controller
Dec 24, 2017
fd6464c
remove some logging
Dec 24, 2017
51ac91e
Docs cleanup
Dec 24, 2017
ad4e8b3
remove unused addQueryArgs
Dec 24, 2017
9d01248
remove gutenberg_handle_rest_pre_insert
Dec 24, 2017
9dad2ff
rename class file to match class name
Dec 24, 2017
471b507
remove unused gutenberg_add_rest_endpoints
Dec 24, 2017
8928813
mobile: don’t display save link for published posts
Dec 26, 2017
76e7973
remove unused autosavePost options
Dec 26, 2017
bf03be9
combine save and autosave actions, removing REQUEST_POST_AUTOSAVE
Dec 26, 2017
5ba2bf1
isEditedPostNew checked above, remove duplicate check
Dec 26, 2017
ccd6ac6
Add isAutosavingPost selector
Dec 26, 2017
2e887a2
Add isAutosaving reducer
Dec 26, 2017
22cfcfd
remove REQUEST_POST_AUTOSAVE, dispatch isAutosaving
Dec 26, 2017
96a348b
Add isAutosaving action
Dec 26, 2017
bf1ab9d
Disable update/publish button during autosaves
Dec 26, 2017
5bf6c7f
Add an autosaving indicator
Dec 27, 2017
a0b5858
improve autosave monitor
Dec 27, 2017
4d84074
first pass: selectors for isPostAutosavable
Dec 28, 2017
14147c6
autosave reducer; improve isAutosaving
Dec 28, 2017
557d653
store the autosaved data
Dec 28, 2017
a988bb8
autosave monitor - add isAutosavable check
Dec 28, 2017
8d75bb7
Merge branch 'master' of github.com:WordPress/gutenberg
Dec 28, 2017
389237f
Merge branch 'master' into feature/autosaves-for-published-posts
Dec 28, 2017
dd2d9b4
remove unused block controller code after merge
Dec 28, 2017
3ee7326
localize autosave data as _wpAutosave
Dec 28, 2017
8e8e9c0
REQUEST_AUTOSAVE_EXISTS effect - show the autosave exists notice
Dec 28, 2017
91a07c0
add an autosaveAlert action
Dec 28, 2017
b2b89ec
When initializing the editor, show the autosave alert
Dec 28, 2017
ecbf499
Only add autosave data if it is newer and changed.
Dec 28, 2017
19dabd9
If this autosave isn't newer and different from the current post, rem…
Dec 28, 2017
7965579
remove unused localised autosave data
Dec 28, 2017
62230fd
update inline docs
Dec 28, 2017
2dd8774
rename autosaveAlert-> showAutosaveAlert
Dec 29, 2017
765ae5d
rename action isAutosaving->toggleAutosave
Dec 29, 2017
06be44d
fix autosaving check in post-publish-button
Dec 29, 2017
4bdfad9
remove duplicate line, out of date todo
Dec 29, 2017
c1ef42c
If the parent post is in a draft state, autosaving updates it.
Dec 29, 2017
c05796c
complete rename isAutosaving -> toggleAutosave
Dec 29, 2017
95a245b
when autosaving a draft, update the draft and don’t create a revision
Dec 29, 2017
b7ae85a
Merge branch 'master' of github.com:WordPress/gutenberg
Dec 29, 2017
8d40fd6
Merge branch 'master' into feature/autosaves-for-published-posts
Dec 29, 2017
3e559ca
fixes for phpcs
Dec 29, 2017
7ace629
fixes for phpcs
Dec 29, 2017
12d7225
ensure tests pass
Dec 30, 2017
4d03072
fixes for eslint
Dec 30, 2017
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
10 changes: 6 additions & 4 deletions editor/components/autosave-monitor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ import { autosave } from '../../store/actions';
import {
isEditedPostDirty,
isEditedPostSaveable,
isPostAutosavable,
} from '../../store/selectors';

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

if ( isDirty && isSaveable && isAutosavable ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess previously, we're stopping the timer if we don't need to autosave anymore, should we do this as well. Maybe something like:

const autosaveStatus = isDirty && isSaveable && isAutosavable
const previousAutosaveStatus = prevProps.isDirty && prevProps.isSaveable && prevProps.isAutosavable;
if ( autosaveStatus !== previousAutosaveStatus ) {
   this.toggleTimer( autosaveStatus );
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, that makes sense. i guess this could happen if you made a change then undid it before an autosave would have fired? i'm going to review the current core autosave.js again to see what the expected/current behavior is.

this.toggleTimer( true );
}
}

Expand Down Expand Up @@ -51,6 +52,7 @@ export default connect(
return {
isDirty: isEditedPostDirty( state ),
isSaveable: isEditedPostSaveable( state ),
isAutosavable: isPostAutosavable( state ),
};
},
{ autosave }
Expand Down
4 changes: 3 additions & 1 deletion editor/components/post-publish-button/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
getEditedPostVisibility,
isEditedPostSaveable,
isEditedPostPublishable,
isAutosavingPost,
getCurrentPostType,
} from '../../store/selectors';

Expand Down Expand Up @@ -52,7 +53,7 @@ export function PostPublishButton( {
}

const className = classnames( 'editor-post-publish-button', {
'is-saving': isSaving,
'is-saving': isSaving && ! isAutosavingPost,
} );

const onClick = () => {
Expand Down Expand Up @@ -81,6 +82,7 @@ const applyConnect = connect(
visibility: getEditedPostVisibility( state ),
isSaveable: isEditedPostSaveable( state ),
isPublishable: isEditedPostPublishable( state ),
isAutosaving: isAutosavingPost( state ),
postType: getCurrentPostType( state ),
} ),
{
Expand Down
12 changes: 7 additions & 5 deletions editor/components/post-saved-state/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,21 @@ import {
isEditedPostSaveable,
getCurrentPost,
getEditedPostAttribute,
isAutosavingPost,
} from '../../store/selectors';

export function PostSavedState( { isNew, isPublished, isDirty, isSaving, isSaveable, status, onStatusChange, onSave } ) {
export function PostSavedState ( { isNew, isPublished, isDirty, isSaving, isSaveable, status, onStatusChange, onSave, isAutosaving } ) {
const className = 'editor-post-saved-state';

if ( isSaving ) {
return (
<span className={ className }>
{ __( 'Saving' ) }
{ isAutosaving ? __( 'Autosaving' ) : __( 'Saving' ) }
</span>
);
}

if ( ! isSaveable || isPublished ) {
if ( ! isSaveable ) {
return null;
}

Expand All @@ -59,8 +60,8 @@ export function PostSavedState( { isNew, isPublished, isDirty, isSaving, isSavea

return (
<Button className={ classnames( className, 'button-link' ) } onClick={ onClick }>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we do if ( isPublished ) { return null; } instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, unless we still want to use this component to show the autosave progress while in action? right now it says 'Autosaving' then 'Saved'. if we do remove this entirely, would disabling/re-enabling the update button be enough to indicate autosaving?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling designers :P @jasmussen @karmatosed

As I said, maybe we'd want to separate "autosaving" from "saving drafts" but I'll let our awesome designers find a solution for this. And I don't consider this a blocking for this PR.

<span className="editor-post-saved-state__mobile">{ __( 'Save' ) }</span>
<span className="editor-post-saved-state__desktop">{ __( 'Save Draft' ) }</span>
<span className="editor-post-saved-state__mobile">{ isPublished ? '' : __( 'Save' ) }</span>
<span className="editor-post-saved-state__desktop">{ isPublished ? '' : __( 'Save Draft' ) }</span>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you clarify this change? You are hiding the link only on the desktop. Should we avoid rendering the button entirely when the post is published?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, right - i'll fix that. i don't think a save button is appropriate when the post is published as you currently can't save drafts of published posts. i'll add that for mobile.

</Button>
);
}
Expand All @@ -74,6 +75,7 @@ export default connect(
isSaving: isSavingPost( state ),
isSaveable: isEditedPostSaveable( state ),
status: getEditedPostAttribute( state, 'status' ),
isAutosaving: isAutosavingPost( state ),
} ),
{
onStatusChange: ( status ) => editPost( { status } ),
Expand Down
8 changes: 7 additions & 1 deletion editor/components/provider/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
/**
* Internal Dependencies
*/
import { setupEditor, undo } from '../../store/actions';
import { setupEditor, undo, autosaveAlert } from '../../store/actions';
import store from '../../store';

/**
Expand Down Expand Up @@ -52,6 +52,12 @@ class EditorProvider extends Component {
// Assume that we don't need to initialize in the case of an error recovery.
if ( ! props.recovery ) {
this.store.dispatch( setupEditor( props.post, this.settings ) );

// @todo only show the autosave alert when there exists an autosave newer than the post
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I understand this todo is this a leftover? since we already show the alert.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, leftover

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

// and if that autosave is different than the post (title, excerpt & content)
if ( props.autosave ) {
this.store.dispatch( autosaveAlert( props.autosave ) );
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions editor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,12 @@ export function recreateEditorInstance( target, settings ) {
* @param {?Object} settings Editor settings object
* @return {Object} Editor interface
*/
export function createEditorInstance( id, post, settings ) {
export function createEditorInstance( id, post, settings, autosave ) {
const target = document.getElementById( id );
const reboot = recreateEditorInstance.bind( null, target, settings );

render(
<EditorProvider settings={ settings } post={ post }>
<EditorProvider settings={ settings } post={ post } autosave={ autosave }>
<ErrorBoundary onError={ reboot }>
<Layout />
</ErrorBoundary>
Expand Down
17 changes: 16 additions & 1 deletion editor/store/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -220,9 +220,10 @@ export function editPost( edits ) {
};
}

export function savePost() {
export function savePost( options ) {
return {
type: 'REQUEST_POST_UPDATE',
options,
};
}

Expand Down Expand Up @@ -252,6 +253,20 @@ export function autosave() {
};
}

export function isAutosaving( isAutosaving ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the actions are better named as "verbs", maybe toggleAutosave?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, good suggestion

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed

return {
type: 'DOING_AUTOSAVE',
isAutosaving,
}
}

export function autosaveAlert( autosave ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe better as showAutosaveAlert or setAutosave?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right, will change

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

return {
type: 'REQUEST_AUTOSAVE_EXISTS',
autosave,
}
}

/**
* Returns an action object used in signalling that undo history should
* restore last popped state.
Expand Down
123 changes: 90 additions & 33 deletions editor/store/effects.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@ import {
replaceBlocks,
createSuccessNotice,
createErrorNotice,
createWarningNotice,
removeNotice,
savePost,
isAutosaving,
editPost,
requestMetaBoxUpdates,
updateReusableBlock,
Expand All @@ -44,6 +46,8 @@ import {
getDirtyMetaBoxes,
getEditedPostContent,
getPostEdits,
getEditedPostTitle,
getEditedPostExcerpt,
isCurrentPostPublished,
isEditedPostDirty,
isEditedPostNew,
Expand All @@ -57,6 +61,7 @@ import {
* Module Constants
*/
const SAVE_POST_NOTICE_ID = 'SAVE_POST_NOTICE_ID';
const AUTOSAVE_POST_NOTICE_ID = 'AUTOSAVE_POST_NOTICE_ID';
const TRASH_POST_NOTICE_ID = 'TRASH_POST_NOTICE_ID';
const SAVE_REUSABLE_BLOCK_NOTICE_ID = 'SAVE_REUSABLE_BLOCK_NOTICE_ID';

Expand All @@ -71,26 +76,65 @@ export default {
content: getEditedPostContent( state ),
id: post.id,
};
const isAutosave = action.options && action.options.autosave;
let Model, newModel;

dispatch( {
type: 'UPDATE_POST',
edits: toSend,
optimist: { type: BEGIN, id: POST_UPDATE_TRANSACTION_ID },
} );
dispatch( removeNotice( SAVE_POST_NOTICE_ID ) );
const Model = wp.api.getPostTypeModel( getCurrentPostType( state ) );
new Model( toSend ).save().done( ( newPost ) => {
dispatch( {
type: 'RESET_POST',
post: newPost,
} );
if ( isAutosave ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know I told you to merge the two effects, but I'm starting to regret 🙃
I was originally thinking the autosave is just a save and should perform the same actions (update the post object etc...) but I was wrong as the autosave is something completely different now.
Seeing all these ifs makes me think I was even more wrong to ask you to merge them.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fine to separate them again; as you can see in the code, autosaving is a pretty different action than saving the post - for both drafts and published posts.

To summarize the differences:

  • at most one autosave is stored for each post for each user
  • regular saves or updates (save draft or update publish post) create a revision each time (if something has changed)

Also:

  • autosaves trigger a warning when the post editor is opened if newer and changed
  • autosaves are marked as such is the revisions screen
  • autosaves work with localstorage when a network connection is unavailable (not implemented yet)

toSend.parent = post.id;
delete toSend.id ;
Model = wp.api.getPostTypeAutosaveModel( getCurrentPostType( state ) );
newModel = new Model( toSend );
} else {
dispatch( {
type: 'REQUEST_POST_UPDATE_SUCCESS',
previousPost: post,
post: newPost,
optimist: { type: COMMIT, id: POST_UPDATE_TRANSACTION_ID },
type: 'UPDATE_POST',
edits: toSend,
optimist: { type: BEGIN, id: POST_UPDATE_TRANSACTION_ID },
} );
dispatch( removeNotice( SAVE_POST_NOTICE_ID ) );
dispatch( removeNotice( AUTOSAVE_POST_NOTICE_ID ) );
Model = wp.api.getPostTypeModel( getCurrentPostType( state ) );
newModel = new Model( toSend );
}

newModel.save().done( ( newPost ) => {
if ( isAutosave ) {
const autosave = {
id: newPost.id,
title: getEditedPostTitle( state ),
excerpt: getEditedPostExcerpt( state ),
content: getEditedPostContent( state ),
};
dispatch( {
type: 'RESET_AUTOSAVE',
post: autosave,
} );
dispatch( isAutosaving( false ) );

dispatch( {
type: 'REQUEST_POST_UPDATE_SUCCESS',
previousPost: post,
post: post,
isAutosave: true,
} );
} else {
// dispatch post autosaved false
// delete the autosave
dispatch( {
type: 'RESET_POST',
post: newPost,
} );
dispatch( {
type: 'REQUEST_POST_UPDATE_SUCCESS',
previousPost: post,
post: newPost,
optimist: { type: COMMIT, id: POST_UPDATE_TRANSACTION_ID },
} );
}
} ).fail( ( err ) => {
if ( isAutosave ) {
dispatch( isAutosaving( false ) );
}

dispatch( {
type: 'REQUEST_POST_UPDATE_FAILURE',
error: get( err, 'responseJSON', {
Expand All @@ -99,12 +143,29 @@ export default {
} ),
post,
edits,
optimist: { type: REVERT, id: POST_UPDATE_TRANSACTION_ID },
optimist: isAutosave ? false : { type: REVERT, id: POST_UPDATE_TRANSACTION_ID },
} );
} );
},
REQUEST_AUTOSAVE_EXISTS( action, store ) {
const { autosave } = action;
const { dispatch, getState } = store;
if ( autosave ) {
dispatch( createWarningNotice(
<p>
<span>{ __( 'There is an autosave of this post that is more recent than the version below.' ) }</span>
{ ' ' }
{ <a href={ autosave.edit_link }>{ __( 'View the autosave' ) }</a> }
</p>,
{
id: AUTOSAVE_POST_NOTICE_ID,
}
) );

}
},
REQUEST_POST_UPDATE_SUCCESS( action, store ) {
const { previousPost, post } = action;
const { previousPost, post, isAutosave } = action;
const { dispatch, getState } = store;

const publishStatus = [ 'publish', 'private', 'future' ];
Expand All @@ -129,8 +190,10 @@ export default {
future: __( 'Post scheduled!' ),
}[ post.status ];
} else {
// Generic fallback notice
noticeMessage = __( 'Post updated!' );
if ( ! isAutosave ) {
// Generic fallback notice
noticeMessage = __( 'Post updated!' );
}
}

if ( noticeMessage ) {
Expand Down Expand Up @@ -263,25 +326,19 @@ export default {
return;
}

if ( ! isEditedPostNew( state ) && ! isEditedPostDirty( state ) ) {
// Change status from auto-draft to draft, saving the post.
if ( isEditedPostNew( state ) ) {
dispatch( editPost( { status: 'draft' } ) );
dispatch( savePost() );
return;
}

if ( isCurrentPostPublished( state ) ) {
// TODO: Publish autosave.
// - Autosaves are created as revisions for published posts, but
// the necessary REST API behavior does not yet exist
// - May need to check for whether the status of the edited post
// has changed from the saved copy (i.e. published -> pending)
if ( ! isEditedPostDirty( state ) ) {
return;
}

// Change status from auto-draft to draft
if ( isEditedPostNew( state ) ) {
dispatch( editPost( { status: 'draft' } ) );
}

dispatch( savePost() );
dispatch( isAutosaving( true ) );
dispatch( savePost( { 'autosave': true } ) );
},
SETUP_EDITOR( action ) {
const { post, settings } = action;
Expand Down
20 changes: 20 additions & 0 deletions editor/store/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,15 @@ export const editor = flow( [
// resetting at each post save.
partialRight( withChangeDetection, { resetTypes: [ 'SETUP_EDITOR', 'RESET_POST' ] } ),
] )( {
autosave ( state = false, action ) {
const { post } = action;
switch ( action.type ) {
case 'RESET_AUTOSAVE':
return post;
}

return state;
},
edits( state = {}, action ) {
switch ( action.type ) {
case 'EDIT_POST':
Expand Down Expand Up @@ -473,6 +482,16 @@ export function blocksMode( state = {}, action ) {
return state;
}

export function isAutosaving( state = false, action ) {
switch ( action.type ) {
case 'DOING_AUTOSAVE':
const { isAutosaving } = action;
return isAutosaving;
}

return state;
}

/**
* Reducer returning the block insertion point
*
Expand Down Expand Up @@ -780,6 +799,7 @@ export const reusableBlocks = combineReducers( {

export default optimist( combineReducers( {
editor,
isAutosaving,
currentPost,
isTyping,
blockSelection,
Expand Down
Loading