-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Introduce a dedicated autosaves endpoint for handling autosave behavior #6257
Changes from 25 commits
9b41d7c
004c830
9b0a3a4
b4ad672
aba8548
c6d5cce
345a508
bb99f4c
5e85a53
491ce55
17a67f6
81ca0de
4da3ef1
b189a3c
7769231
d4400a9
ab44c63
5b0e36d
949557e
e4d20ff
0833e15
6b07e56
becb66b
ecc1527
16ce6f1
124895b
fd84ca5
dad5683
6c74256
5018748
93881fc
5771f51
7585b00
ed7d553
82a1b16
e02c636
a3c61f7
a204ece
6c4b7a2
e5e9e25
7cd9547
ba0a25f
bc79d1f
7a9c123
76e6296
5ce9de1
750355f
f28f2de
b4b95e0
b68c7a5
159d6e0
9142591
6e7c57c
5ee1cae
7247780
193d32e
35f41ee
9af3e3f
e83bdc2
dfc22ed
00d586a
07cccd7
dbd3ab6
4204ad2
dfe6bc3
6cd8729
7e8b6a2
bb26c8c
a4eab38
0ca5be0
d253b12
b06eb18
668907b
0d1f9b5
92b8d79
376379d
74935ff
85a67cd
c81ab2a
d795e1d
2531102
264965d
f1df3ff
b7042f7
1fe8e20
c31eb05
f88fe54
5b5107b
2012003
89131f1
e2e05e5
3e5e744
2042645
bd29ca0
751a47c
7b1945a
7fd7c79
7034cdc
20f2b43
f669754
7b4bb19
18e1018
1d40167
f8724bd
7a3e87b
02f5a15
de5c887
8347b52
66505be
77ce650
e0c33c8
4e82c42
5d25123
747f90c
dc2e758
8d5dcf0
83ad219
4821f49
2dfc666
102b944
563a294
0bec4ea
f3e564a
11f9930
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -353,9 +353,17 @@ export function editPost( edits ) { | |
}; | ||
} | ||
|
||
export function savePost() { | ||
/** | ||
* Returns an action object to save the post. | ||
* | ||
* @param {Object} options Set autosave: true for an autosave. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: JSDoc has a solution for documenting properties of an argument that we could leverage: http://usejsdoc.org/tags-param.html#parameters-with-properties There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice, changed in 9142591 |
||
* | ||
* @return {Object} Action object. | ||
*/ | ||
export function savePost( options ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do these There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added |
||
return { | ||
type: 'REQUEST_POST_UPDATE', | ||
options, | ||
}; | ||
} | ||
|
||
|
@@ -393,12 +401,19 @@ export function mergeBlocks( blockAUid, blockBUid ) { | |
* | ||
* @return {Object} Action object. | ||
*/ | ||
export function autosave() { | ||
export function doAutosave() { | ||
return { | ||
type: 'AUTOSAVE', | ||
}; | ||
} | ||
|
||
export function toggleAutosave( isAutosaving ) { | ||
return { | ||
type: 'DOING_AUTOSAVE', | ||
isAutosaving, | ||
}; | ||
} | ||
|
||
/** | ||
* Returns an action object used in signalling that undo history should | ||
* restore last popped state. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -43,15 +43,14 @@ import { | |
removeBlock, | ||
resetBlocks, | ||
setTemplateValidity, | ||
toggleAutosave, | ||
} from './actions'; | ||
import { | ||
getCurrentPost, | ||
getCurrentPostType, | ||
getEditedPostContent, | ||
getPostEdits, | ||
isCurrentPostPublished, | ||
isEditedPostDirty, | ||
isEditedPostNew, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why has this been removed? This is causing an error because the
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. that usage is now removed |
||
isEditedPostSaveable, | ||
getBlock, | ||
getBlockCount, | ||
|
@@ -102,41 +101,66 @@ export default { | |
content: getEditedPostContent( state ), | ||
id: post.id, | ||
}; | ||
|
||
dispatch( { | ||
type: 'UPDATE_POST', | ||
edits: toSend, | ||
optimist: { type: BEGIN, id: POST_UPDATE_TRANSACTION_ID }, | ||
} ); | ||
dispatch( removeNotice( SAVE_POST_NOTICE_ID ) ); | ||
const basePath = wp.api.getPostTypeRoute( getCurrentPostType( state ) ); | ||
wp.apiRequest( { path: `/wp/v2/${ basePath }/${ post.id }`, method: 'PUT', data: toSend } ).then( | ||
( newPost ) => { | ||
dispatch( resetPost( newPost ) ); | ||
dispatch( { | ||
type: 'REQUEST_POST_UPDATE_SUCCESS', | ||
previousPost: post, | ||
post: newPost, | ||
edits: toSend, | ||
optimist: { type: COMMIT, id: POST_UPDATE_TRANSACTION_ID }, | ||
} ); | ||
}, | ||
( err ) => { | ||
dispatch( { | ||
type: 'REQUEST_POST_UPDATE_FAILURE', | ||
error: get( err, 'responseJSON', { | ||
code: 'unknown_error', | ||
message: __( 'An unknown error occurred.' ), | ||
} ), | ||
post, | ||
edits, | ||
optimist: { type: REVERT, id: POST_UPDATE_TRANSACTION_ID }, | ||
const isAutosave = action.options && action.options.autosave; | ||
|
||
if ( isAutosave ) { | ||
dispatch( toggleAutosave( true ) ); | ||
toSend.parent = post.id; | ||
wp.apiRequest( { path: `/wp/v2/${ basePath }/${ post.id }/autosaves`, method: 'POST', data: toSend } ).then( | ||
( autosave ) => { | ||
dispatch( toggleAutosave( false ) ); | ||
|
||
dispatch( { | ||
type: 'RESET_AUTOSAVE', | ||
post: autosave, | ||
} ); | ||
|
||
dispatch( { | ||
type: 'REQUEST_POST_UPDATE_SUCCESS', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we dispatch
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Right, I think its mostly useful where you want to show the UI in the updated state "optimistically" (eg adding a todo, you show the todo added), then if things fail for some reason you can show an error and roll back the change. Not sure how useful that is for the autosave? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
good question, let me dig in a bit further to see why this is here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I think of it more in the pure data sense: We assume some success state where a new autosave will become the referenced value, and if a failure is to occur, we revert back to the prior state value. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @aduth this is used in the I see how the naming is confusing, maybe I can try resetting There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
previousPost: post, | ||
post: post, | ||
isAutosave: true, | ||
} ); | ||
}, | ||
() => { | ||
dispatch( toggleAutosave( false ) ); | ||
} ); | ||
} | ||
); | ||
} else { | ||
dispatch( { | ||
type: 'UPDATE_POST', | ||
edits: toSend, | ||
optimist: { type: BEGIN, id: POST_UPDATE_TRANSACTION_ID }, | ||
} ); | ||
dispatch( removeNotice( SAVE_POST_NOTICE_ID ) ); | ||
wp.apiRequest( { path: `/wp/v2/${ basePath }/${ post.id }`, method: 'PUT', data: toSend } ).then( | ||
( newPost ) => { | ||
dispatch( resetPost( newPost ) ); | ||
dispatch( { | ||
type: 'REQUEST_POST_UPDATE_SUCCESS', | ||
previousPost: post, | ||
post: newPost, | ||
edits: toSend, | ||
optimist: { type: COMMIT, id: POST_UPDATE_TRANSACTION_ID }, | ||
} ); | ||
}, | ||
( err ) => { | ||
dispatch( { | ||
type: 'REQUEST_POST_UPDATE_FAILURE', | ||
error: get( err, 'responseJSON', { | ||
code: 'unknown_error', | ||
message: __( 'An unknown error occurred.' ), | ||
} ), | ||
post, | ||
edits, | ||
optimist: { type: REVERT, id: POST_UPDATE_TRANSACTION_ID }, | ||
} ); | ||
} | ||
); | ||
} | ||
}, | ||
REQUEST_POST_UPDATE_SUCCESS( action, store ) { | ||
const { previousPost, post } = action; | ||
const { previousPost, post, isAutosave } = action; | ||
const { dispatch } = store; | ||
|
||
const publishStatus = [ 'publish', 'private', 'future' ]; | ||
|
@@ -145,8 +169,9 @@ export default { | |
|
||
let noticeMessage; | ||
let shouldShowLink = true; | ||
if ( ! isPublished && ! willPublish ) { | ||
// If saving a non published post, don't show any notice | ||
|
||
if ( isAutosave || ( ! isPublished && ! willPublish ) ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When will There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. right, good catch. removed in f88fe54 |
||
// If autosaving, or saving a non published post, don't show any notice | ||
noticeMessage = null; | ||
} else if ( isPublished && ! willPublish ) { | ||
// If undoing publish status, show specific notice | ||
|
@@ -309,20 +334,11 @@ export default { | |
return; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The line above this: We check There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I added a check to prevent triggering a new autosave while an autosave is occurring in f669754. I tested this using a throttled connection and a low autosave interval and in my testing it prevented duplicate autosave requests. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With the changes in 7b4bb19, should the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe worth testing the behavior of Cmd+S, which fires itself as Edit: Particularly, what happens when I press Cmd+S in quick succession (second press before the first autosave completes). |
||
} | ||
|
||
if ( ! isEditedPostNew( state ) && ! isEditedPostDirty( state ) ) { | ||
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 ) ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With this, could we just eliminate the previous condition? if ( ! isEditedPostNew( state ) && ! isEditedPostDirty( state ) ) { There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, removed in 6e7c57c |
||
return; | ||
} | ||
|
||
dispatch( savePost() ); | ||
dispatch( savePost( { autosave: true } ) ); | ||
}, | ||
SETUP_EDITOR( action ) { | ||
const { post, settings } = action; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -228,6 +228,15 @@ export const editor = flow( [ | |
ignoreTypes: [ 'RECEIVE_BLOCKS' ], | ||
} ), | ||
] )( { | ||
autosave( state = false, action ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Per #6257 (comment), I hesitate about including There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right! I moved this back out of the editor reducer section in 7247780 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we represent the default state as a boolean, rather than a more semantically meaningful empty value like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. null is probably a better choice, chenged in 193d32e |
||
const { post } = action; | ||
switch ( action.type ) { | ||
case 'RESET_AUTOSAVE': | ||
return post; | ||
} | ||
|
||
return state; | ||
}, | ||
edits( state = {}, action ) { | ||
switch ( action.type ) { | ||
case 'EDIT_POST': | ||
|
@@ -584,6 +593,16 @@ export function isTyping( state = false, action ) { | |
return state; | ||
} | ||
|
||
export function currentlyAutosaving( state = false, action ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Noting some naming inconsistencies: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. renamed to |
||
switch ( action.type ) { | ||
case 'DOING_AUTOSAVE': | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need a separate action for this? Couldn't we assume There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. removed/refactored in ed7d553 |
||
const { isAutosaving } = action; | ||
return isAutosaving; | ||
} | ||
|
||
return state; | ||
} | ||
|
||
/** | ||
* Reducer returning the block selection's state. | ||
* | ||
|
@@ -1008,6 +1027,7 @@ export const sharedBlocks = combineReducers( { | |
|
||
export default optimist( combineReducers( { | ||
editor, | ||
currentlyAutosaving, | ||
currentPost, | ||
isTyping, | ||
blockSelection, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -299,6 +299,44 @@ export function isEditedPostEmpty( state ) { | |
); | ||
} | ||
|
||
/** | ||
* Returns true if the post can be autosaved, or false otherwise. | ||
* | ||
* @param {Object} state Global application state | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. JSDoc standards treat There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. right, correcting There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed in f1df3ff |
||
* @return {boolean} Whether the post can be autosaved | ||
*/ | ||
export function isPostAutosavable( state ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it "saveable" or "savable"? We should pick one and keep consistency. Currently this is inconsistent with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Its either: https://en.oxforddictionaries.com/definition/saveable Since you already have the 'saveable' use, I'll update my PR with this variation. its a bit easier to read. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed in bb26c8c |
||
// If the post is autosaving, it is not autosavable. | ||
if ( state.currentlyAutosaving ) { | ||
return false; | ||
} | ||
|
||
// If we don't already have an autosave, the post is autosavable. | ||
if ( ! hasAutosave( state ) ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is a post autosavable if it has no content? Or are we relying on the consumer (e.g. if ( ! isPostSavable( state ) ) {
return false;
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes I think so, if thats a change (the post isn't new). For example, if you create a post with only a title and publish it, then edit the title and clear it. When autosave fires it will save the empty state as an autosave. I guess we could avoid this if thats desired, I can check the classic editor to see what the behavior is there for empty posts.
I couldn't find the A post can't be saved if nothing has changed since the last save. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I think slightly related is that you also get an autosave if you hit "Add New" for a new post, put your cursor in the default paragraph block, and then don't do anything (don't type, don't touch your keyboard, don't move your mouse, etc). That is not the case for the classic editor. |
||
return true; | ||
} | ||
|
||
const title = getEditedPostAttribute( state, 'title' ); | ||
const excerpt = getEditedPostExcerpt( state ); | ||
const content = getEditedPostContent( state ); | ||
const autosave = state.editor.present.autosave; | ||
|
||
// If the title, excerpt or content has changed, the post is autosavable. | ||
if ( | ||
( autosave.title && title !== autosave.title.raw ) || | ||
( autosave.excerpt && excerpt !== autosave.excerpt.raw ) || | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not clear to me when There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think autosave will always have the excerpt and title properties - they can't be empty, they can only be an empty string which will evaluate correctly. The reason for this is that the api always returns all fields, so excerpt is always returned even if "empty". autosave represents the last stored autosave so we can be confident an excerpt will always be set. I'm going to remove the checks for now since they are confusing and I don't think they are needed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed in 35f41ee |
||
( autosave.content && content !== autosave.content.raw ) | ||
) { | ||
return true; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: This condition could be eliminated and replaced with a single return (
( title !== autosave.title.raw ) ||
( excerpt !== autosave.excerpt.raw ) ||
( content !== autosave.content.raw )
); There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice! changed in 7e8b6a2 |
||
} | ||
|
||
return false; | ||
} | ||
|
||
export function hasAutosave( state ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we add a JSDoc. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unit tests would be good too. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
return !! state.editor.present.autosave; | ||
} | ||
|
||
/** | ||
* Return true if the post being edited is being scheduled. Preferring the | ||
* unsaved status values. | ||
|
@@ -1107,6 +1145,16 @@ export function didPostSaveRequestFail( state ) { | |
return !! state.saving.error; | ||
} | ||
|
||
/** | ||
* Is the post autosaving? | ||
* | ||
* @param {Object} state Global application state | ||
* @return {boolean} Whether the post is autosaving | ||
*/ | ||
export function isAutosavingPost( state ) { | ||
return !! state.currentlyAutosaving; | ||
} | ||
|
||
/** | ||
* Returns a suggested post format for the current post, inferred only if there | ||
* is a single block within the post and it is of a type known to match a | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where is the
isAutosaving
prop being passed?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
setting was lost in some recent changes, fixed in dad5683