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

Introduce a dedicated autosaves endpoint for handling autosave behavior #6257

Merged
merged 124 commits into from
May 31, 2018
Merged
Show file tree
Hide file tree
Changes from 97 commits
Commits
Show all changes
124 commits
Select commit Hold shift + click to select a range
9b41d7c
autosaves with rest controller, first pass
Apr 17, 2018
004c830
use newer title call
Apr 18, 2018
9b0a3a4
autosave: compare raw attributes for change check
Apr 18, 2018
b4ad672
Clean up autosave effects
Apr 18, 2018
aba8548
pull isAutosaving from props in PostSavedState
Apr 18, 2018
c6d5cce
Merge branch 'master' of github.com:WordPress/gutenberg
Apr 18, 2018
345a508
Merge branch 'master' of github.com:WordPress/gutenberg
Apr 18, 2018
bb99f4c
from .14
Apr 18, 2018
5e85a53
effects, improve toggle autosave timing, enable autosaving for publis…
Apr 18, 2018
491ce55
Disable ‘save draft’ after draft is autosaved
Apr 18, 2018
17a67f6
revert post saved state changes, keep save button active regardless o…
Apr 18, 2018
81ca0de
remove unrelated showAutosaveAlert
Apr 18, 2018
4da3ef1
fixes for eslint
Apr 18, 2018
b189a3c
leverage withChangeDetection for autosaves
Apr 18, 2018
7769231
Merge branch 'master' into fix/autosaves
Apr 18, 2018
d4400a9
fixes for php slint
Apr 18, 2018
ab44c63
Merge branch 'master' of github.com:WordPress/gutenberg
Apr 22, 2018
5b0e36d
Merge branch 'master' of github.com:WordPress/gutenberg
Apr 23, 2018
949557e
Merge branch 'master' into fix/autosaves
Apr 23, 2018
e4d20ff
rename WP_REST_Autosaves_Controller -> GB_REST_Autosaves_Controller …
Apr 23, 2018
0833e15
Add a docblock for savePost action
Apr 23, 2018
6b07e56
check state.autosave exists before using
Apr 24, 2018
becb66b
Fix autosave tests
Apr 24, 2018
ecc1527
Revert "leverage withChangeDetection for autosaves"
Apr 24, 2018
16ce6f1
rename rest controler file to match class passing phplint
Apr 24, 2018
124895b
Merge branch 'master' of github.com:WordPress/gutenberg
Apr 26, 2018
fd84ca5
Merge branch 'master' into fix/autosaves
Apr 26, 2018
dad5683
set up isAutosaving
Apr 27, 2018
6c74256
rename controller WP_REST_Autosaves_Controller and only load if class…
Apr 27, 2018
5018748
Cleanup doc blocks
Apr 27, 2018
93881fc
rename revision_controller -> revisions_controller
Apr 27, 2018
5771f51
Merge branch 'master' of github.com:WordPress/gutenberg
Apr 27, 2018
7585b00
Merge branch 'master' into fix/autosaves
Apr 27, 2018
ed7d553
clean up isAutosaving reducer, removing toggleAutosave action
Apr 27, 2018
82a1b16
fixes for php-lint
Apr 27, 2018
e02c636
fix test: button stays in saved state when post not dirty
Apr 27, 2018
a3c61f7
restore state check
Apr 27, 2018
a204ece
Properly instantiate the autosaves controller
danielbachhuber Apr 27, 2018
6c4b7a2
Merge branch 'fix/autosaves' of github.com:WordPress/gutenberg into f…
Apr 30, 2018
e5e9e25
ensure isAutosave returns a boolean
Apr 30, 2018
7cd9547
Add PHP Unit tests covering WP_REST_Autosaves_Controller functionality
Apr 30, 2018
ba0a25f
Avoid duplicate route resgistration
Apr 30, 2018
bc79d1f
Rename/fix up unit tests for rest controller
Apr 30, 2018
7a9c123
fixes for phpcs
Apr 30, 2018
76e6296
Merge branch 'master' into fix/autosaves
May 3, 2018
5ce9de1
Merge branch 'master' of github.com:WordPress/gutenberg
May 3, 2018
750355f
Merge branch 'master' into fix/autosaves
May 3, 2018
f28f2de
Merge branch 'master' of github.com:WordPress/gutenberg
May 4, 2018
b4b95e0
Merge branch 'master' of github.com:WordPress/gutenberg
May 7, 2018
b68c7a5
Merge branch 'master' of github.com:WordPress/gutenberg
May 8, 2018
159d6e0
Merge branch 'master' into fix/autosaves
May 8, 2018
9142591
document autosave as a parameter property for options
May 8, 2018
6e7c57c
remove redundant check for isEditedPostNew
May 8, 2018
5ee1cae
rename currentlyAutosaving -> isAutosaving
May 8, 2018
7247780
move autosave reducer outside editor
May 8, 2018
193d32e
set the default autosave state to null
May 8, 2018
35f41ee
remove unneded object check that confused logic
May 8, 2018
9af3e3f
fix for eslint
May 8, 2018
e83bdc2
ensure href set before using in gutenberg_add_target_schema_to_links
May 8, 2018
dfc22ed
Merge branch 'master' of github.com:WordPress/gutenberg
May 11, 2018
00d586a
Merge branch 'master' into fix/autosaves
May 11, 2018
07cccd7
include the autosaves controller
May 11, 2018
dbd3ab6
docblock object -> boolean
May 11, 2018
4204ad2
document isAutosaving reducer
May 11, 2018
dfe6bc3
document autosave reducer
May 11, 2018
6cd8729
skip destructuring post
May 11, 2018
7e8b6a2
eliminate unneeded conditional
May 11, 2018
bb26c8c
rename autosavable -> autosaveable (with an e)
May 11, 2018
a4eab38
Simplify class check
May 11, 2018
0ca5be0
fixes for phpcs
May 11, 2018
d253b12
savePost on CMD-S
May 11, 2018
b06eb18
correctly return autosave from selector
May 11, 2018
668907b
correct state autosave usage
May 11, 2018
0d1f9b5
fix docblock
May 11, 2018
92b8d79
add test: should stop autosave timer when the autosave is up to date
May 11, 2018
376379d
correct test naming: should stop autosave timer when the autosave is …
May 11, 2018
74935ff
Add an e2e test for post autosaving
May 11, 2018
85a67cd
whitespace
May 11, 2018
c81ab2a
Merge branch 'master' into fix/autosaves
May 14, 2018
d795e1d
respect AUTOSAVE_INTERVAL for autosave timing
May 14, 2018
2531102
reset saving state on `RESET_AUTOSAVE`
May 14, 2018
264965d
add mu plugin to lower the autosave interval for testing
May 14, 2018
f1df3ff
docs spacing
May 14, 2018
b7042f7
Add JSDoc for hasAutosave
May 14, 2018
1fe8e20
eslint: missing trailing comma
May 14, 2018
c31eb05
Add tests for the hasAutosave selector
May 14, 2018
f88fe54
remove unused isAutosave in REQUEST_POST_UPDATE_SUCCESS
May 14, 2018
5b5107b
adjust initial state for autosave test
May 14, 2018
2012003
only pass autosaveInterval
May 14, 2018
89131f1
Testing: Trigger programmatic autosave
aduth May 14, 2018
e2e05e5
Merge branch 'fix/autosaves' of github.com:WordPress/gutenberg into f…
May 15, 2018
3e5e744
Merge branch 'master' of github.com:WordPress/gutenberg
May 23, 2018
2042645
Merge branch 'master' into fix/autosaves
May 23, 2018
bd29ca0
revert 2012003
May 23, 2018
751a47c
remove unused `className`
May 23, 2018
7b1945a
use isAutosaving
May 23, 2018
7fd7c79
Drop autosave interval back to 10 seconds, because 60 is really slow
danielbachhuber May 23, 2018
7034cdc
clarify use of isEditedPostSaveable in effects/AUTOSAVE
May 23, 2018
20f2b43
Merge branch 'fix/autosaves' of github.com:WordPress/gutenberg into f…
May 23, 2018
f669754
Prevent autosaves if an autosave is in progress.
May 23, 2018
7b4bb19
Add isEditedPostSaveable logic to isPostAutosaveable
May 24, 2018
18e1018
autosave monitor tests - use isAutosaveable
May 24, 2018
1d40167
fix tests for autosave monitor
May 24, 2018
f8724bd
Autosave: Cancel scheduled autosave when autosaving starts
aduth May 25, 2018
7a3e87b
Autosave: Style: Use property shorthand for autosaveInterval assignment
aduth May 25, 2018
02f5a15
Revert "revert 2012003"
aduth May 25, 2018
de5c887
State: Rename `isPostAutosaveable` to `isEditedPostAutosaveable`
aduth May 25, 2018
8347b52
State: Implement `isEditedPostAutosaveable` as composing `isAutosavin…
aduth May 25, 2018
66505be
State: Implement autosave action as action creator
aduth May 25, 2018
77ce650
State: Improve isAutosavingPost selector JSDoc
aduth May 25, 2018
e0c33c8
State: Revert doAutosave action creator name to autosave
aduth May 25, 2018
4e82c42
Testing: Avoid potential race conditions on Cmd+S save fail
aduth May 25, 2018
5d25123
State: Prevent save request if post is not saveable
aduth May 25, 2018
747f90c
State: Implement autosaving as superset of saving condition
aduth May 25, 2018
dc2e758
State: Consider save-in-progress as unsaveable
aduth May 25, 2018
8d5dcf0
Testing: Evaluate autosave completion by class presence
aduth May 25, 2018
83ad219
Keyboard Shortcuts: Disallow non-dirty save requests
aduth May 25, 2018
4821f49
State: Track autosave object as subset of received properties
aduth May 25, 2018
2dfc666
State: Remove redundant condition from autosaveable
aduth May 25, 2018
102b944
State: Simplify isEditedPostAutosaveable as Array#some of fields
aduth May 25, 2018
563a294
State: Deprecate getEditedPostExcerpt selector
aduth May 25, 2018
0bec4ea
Merge branch 'master' into fix/autosaves
danielbachhuber May 31, 2018
f3e564a
Remove tests originally removed in bdcbf714345045b5c96ef2d61ee986a1cf…
danielbachhuber May 31, 2018
11f9930
Fix linting
danielbachhuber May 31, 2018
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
28 changes: 20 additions & 8 deletions editor/components/autosave-monitor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ import { withSelect, withDispatch } from '@wordpress/data';

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, isAutosaveable } = this.props;
if (
prevProps.isDirty !== isDirty ||
prevProps.isSaveable !== isSaveable ||
prevProps.isAutosaveable !== isAutosaveable
) {
this.toggleTimer( isDirty && isSaveable && isAutosaveable );
Copy link
Member

Choose a reason for hiding this comment

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

I seem to recall raising previously, but am unable to find the conversation and resulting rebuttal (if one exists): When would isAutosaveable be true and isSaveable be false ? The issue I'm getting to here is that it seems like isSaveable should be built in to the selector logic for isAutosaveable and thus made redundant as a separate prop here, simplifying to isDirty && isAutosaveable.

Copy link
Member Author

Choose a reason for hiding this comment

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

In this case isSaveable maps to isEditedPostSaveable which tests if the post contains a title, an excerpt, or non-empty content. if you edit the content of an existing post and remove everything, the post is no longer savable, this prevents calling the autosave in that case. its possibly redundant to have this here and in effects.js, should i remove it there?

Copy link
Member

Choose a reason for hiding this comment

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

Should isEditedPostSaveable be tested as part of the isAutosaveable selector? In other words, is it ever the case that an autosave is valid where isEditedPostSaveable would otherwise return false ? This would allow us to simplify both the effect and this component, where we could simply test isAutosaveable and be assured that the post is also "saveable" (with valid content).

Copy link
Member Author

Choose a reason for hiding this comment

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

sure, thats a good idea. I can move that test into isAutosavable to clear up the logic here.

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 in 7b4bb19, adding the isEditedPostSaveable logic to isPostAutosaveable and removing the extra calls to isEditedPostSaveable here

}
}

Expand All @@ -19,11 +22,11 @@ export class AutosaveMonitor extends Component {

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

const { autosaveInterval } = this.props;
if ( isPendingSave ) {
this.pendingSave = setTimeout(
() => this.props.autosave(),
10000
autosaveInterval * 1000
);
}
}
Expand All @@ -35,13 +38,22 @@ export class AutosaveMonitor extends Component {

export default compose( [
withSelect( ( select ) => {
const { isEditedPostDirty, isEditedPostSaveable } = select( 'core/editor' );
const {
isEditedPostDirty,
isEditedPostSaveable,
isPostAutosaveable,
getEditorSettings,
} = select( 'core/editor' );
const { autosaveInterval } = getEditorSettings();
return {
isDirty: isEditedPostDirty(),
isSaveable: isEditedPostSaveable(),
isAutosaveable: isPostAutosaveable(),
autosaveInterval: autosaveInterval,
};
} ),

withDispatch( ( dispatch ) => ( {
autosave: dispatch( 'core/editor' ).autosave,
autosave: dispatch( 'core/editor' ).doAutosave,
} ) ),
] )( AutosaveMonitor );
8 changes: 7 additions & 1 deletion editor/components/autosave-monitor/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,17 @@ describe( 'AutosaveMonitor', () => {

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

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

it( 'should stop autosave timer when the autosave is up to date', () => {
wrapper.setProps( { isDirty: true, isSaveable: true, isAutosaveable: false } );

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

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

Expand Down
4 changes: 2 additions & 2 deletions editor/components/editor-global-keyboard-shortcuts/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export default compose( [
redo,
undo,
removeBlocks,
autosave,
savePost,
} = dispatch( 'core/editor' );

return {
Expand All @@ -125,7 +125,7 @@ export default compose( [
onRedo: redo,
onUndo: undo,
onRemove: removeBlocks,
onSave: autosave,
onSave: savePost,
};
} ),
] )( EditorGlobalKeyboardShortcuts );
5 changes: 4 additions & 1 deletion editor/components/post-publish-button/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ export function PostPublishButton( {
visibility,
isPublishable,
isSaveable,
isAutosaving,
Copy link
Member

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?

Copy link
Member Author

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

hasPublishAction,
onSubmit = noop,
forceIsSaving,
} ) {
const isButtonEnabled = ! isSaving && isPublishable && isSaveable;
const isButtonEnabled = ! isSaving && ! isAutosaving && isPublishable && isSaveable;

let publishStatus;
if ( ! hasPublishAction ) {
Expand Down Expand Up @@ -70,6 +71,7 @@ export default compose( [
isEditedPostPublishable,
getCurrentPost,
getCurrentPostType,
isAutosavingPost,
} = select( 'core/editor' );
return {
isSaving: forceIsSaving || isSavingPost(),
Expand All @@ -79,6 +81,7 @@ export default compose( [
isPublishable: forceIsDirty || isEditedPostPublishable(),
hasPublishAction: get( getCurrentPost(), [ '_links', 'wp:action-publish' ], false ),
postType: getCurrentPostType(),
isAutosaving: isAutosavingPost(),
};
} ),
withDispatch( ( dispatch ) => {
Expand Down
6 changes: 4 additions & 2 deletions editor/components/post-saved-state/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,13 @@ export class PostSavedState extends Component {
}

render() {
const { isNew, isPublished, isDirty, isSaving, isSaveable, onSave } = this.props;
const { isNew, isPublished, isDirty, isSaving, isSaveable, onSave, isAutosaving } = this.props;
const { forceSavedMessage } = this.state;
if ( isSaving ) {
return (
<span className="editor-post-saved-state is-saving">
<Dashicon icon="cloud" />
{ __( 'Saving' ) }
{ isAutosaving ? __( 'Autosaving' ) : __( 'Saving' ) }
</span>
);
}
Expand Down Expand Up @@ -88,6 +88,7 @@ export default compose( [
isSavingPost,
isEditedPostSaveable,
getCurrentPost,
isAutosavingPost,
} = select( 'core/editor' );
return {
post: getCurrentPost(),
Expand All @@ -96,6 +97,7 @@ export default compose( [
isDirty: forceIsDirty || isEditedPostDirty(),
isSaving: forceIsSaving || isSavingPost(),
isSaveable: isEditedPostSaveable(),
isAutosaving: isAutosavingPost(),
};
} ),
withDispatch( ( dispatch ) => ( {
Expand Down
13 changes: 11 additions & 2 deletions editor/store/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -351,9 +351,18 @@ export function editPost( edits ) {
};
}

export function savePost() {
/**
* Returns an action object to save the post.
*
* @param {Object} options Options for the save.
* @param {boolean} options.autosave Perform an autosave if true.
*
* @return {Object} Action object.
*/
export function savePost( options ) {
Copy link
Member

Choose a reason for hiding this comment

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

Do these options merit a doc block?

Copy link
Member Author

Choose a reason for hiding this comment

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

added

return {
type: 'REQUEST_POST_UPDATE',
options,
};
}

Expand Down Expand Up @@ -391,7 +400,7 @@ export function mergeBlocks( blockAUid, blockBUid ) {
*
* @return {Object} Action object.
*/
export function autosave() {
export function doAutosave() {
return {
type: 'AUTOSAVE',
};
Expand Down
96 changes: 52 additions & 44 deletions editor/store/effects.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,7 @@ import {
getCurrentPostType,
getEditedPostContent,
getPostEdits,
isCurrentPostPublished,
isEditedPostDirty,
isEditedPostNew,
Copy link
Member

Choose a reason for hiding this comment

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

Why has this been removed? This is causing an error because the AUTOSAVE effect attempts to call it:

Uncaught ReferenceError: isEditedPostNew is not defined

Copy link
Member Author

Choose a reason for hiding this comment

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

that usage is now removed

isEditedPostSaveable,
getBlock,
getBlockCount,
Expand Down Expand Up @@ -102,38 +100,56 @@ 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 ) {
toSend.parent = post.id;
wp.apiRequest( { path: `/wp/v2/${ basePath }/${ post.id }/autosaves`, method: 'POST', data: toSend } ).then(
( autosave ) => {
dispatch( {
type: 'RESET_AUTOSAVE',
post: autosave,
} );
},
() => {
dispatch( {
type: 'RESET_AUTOSAVE',
Copy link
Member

Choose a reason for hiding this comment

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

Is the behavior here to revert back to the previous state when an autosave fails? This is already accomplished for the proper save via redux-optimist, and it seems like we could leverage the same here. We might not want to do it on a common UPDATE_POST (or do we?), but maybe a separate dummy action which includes the optimist meta? See below in the else case for optimist property handling.

Copy link
Member Author

Choose a reason for hiding this comment

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

The reason we dispatch RESET_AUTOSAVE here is to set the 'isAutosaving' state to false (which in turn is reflected in the post publish button and post-saved-state display... see

case 'RESET_AUTOSAVE':

Is redux-optimist appropriate for this use case?

Copy link
Member

Choose a reason for hiding this comment

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

As I recall, Redux optimist behaves in a way that an action dispatched can either be committed (confirmed) or reverted, in which case the original dispatched action is pretended to have never existed, though all other subsequent will proceed as expected. It could be relevant here, though seems like it could be just as well to reset explicitly.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think its fine to set this explicitly here. Optimist is really useful when you want to show the ui state completed (optimistically) then revert that if it somehow fails. In this case it seems fine to show the autosave is working until it completes or fails.

post: post,
} );
} );
}
);
} 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;
Expand All @@ -145,8 +161,9 @@ export default {

let noticeMessage;
let shouldShowLink = true;

if ( ! isPublished && ! willPublish ) {
// If saving a non published post, don't show any notice
// 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
Expand Down Expand Up @@ -309,20 +326,11 @@ export default {
return;
Copy link
Member

Choose a reason for hiding this comment

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

The line above this: We check isEditedPostSaveable. Shouldn't this be isEditedPostAutosaveable ? Do we allow autosave to occur while autosave is already in progress? What happens then to the value of state's isAutosaving when the first autosave returns (i.e. triggers RESET_AUTOSAVE) but the second hasn't?

Copy link
Member Author

Choose a reason for hiding this comment

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

isEditedPostSaveable verifies that a post has a title, an excerpt, or non-empty content. I added inline documentation explaining the use. Fixed in 7034cdc.

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.

Copy link
Member

Choose a reason for hiding this comment

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

With the changes in 7b4bb19, should the AUTOSAVE effect be checking isEditedPostAutosaveable instead of isEditedPostSaveable (which now includes isEditedPostSaveable anyways)?

Copy link
Member

Choose a reason for hiding this comment

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

Maybe worth testing the behavior of Cmd+S, which fires itself as autosave, where we rely on the effect to stop itself from an unnecessary save.

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 ) ) {
Copy link
Member

Choose a reason for hiding this comment

The 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 ) ) {

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, removed in 6e7c57c

return;
}

dispatch( savePost() );
dispatch( savePost( { autosave: true } ) );
},
SETUP_EDITOR( action, { getState } ) {
const { post } = action;
Expand Down
40 changes: 40 additions & 0 deletions editor/store/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,26 @@ export function isTyping( state = false, action ) {
return state;
}

/**
* Reducer returning autosave state. True if an autosave is currently occurring.
*
* @param {boolean} state Current state.
* @param {Object} action Dispatched action.
*
* @return {boolean} Updated state.
*/
export function isAutosaving( state = false, action ) {
Copy link
Member

Choose a reason for hiding this comment

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

Can we add JSDoc describing what state is being tracked here.

Copy link
Member Author

Choose a reason for hiding this comment

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

added in 4204ad2

Copy link
Member

Choose a reason for hiding this comment

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

it( 'should return ??? when the post is published while autosave is in progress', () => {
	const original = isAutosaving( undefined, savePost( { autosave: true } ) );
	const state = isAutosaving( original, savePost() );

	expect( state ).toBe( /* ??? */ );
} );

Note: UI limitations imposed aren't enough. If we want it to be a case prevented, at the very least it should be considered as part of an isEditedPostPublishable selector or equivalent state layer consideration (e.g. considered as part of the REQUEST_POST_UPDATE effect).

It maybe speaks to a broader concern of: Is having separate state handling for "is autosaving" vs. "is saving" causing us more headaches to handle than if they were one in the same? Does it make sense for them to be considered as equivalent?

Copy link
Member Author

Choose a reason for hiding this comment

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

possibly. autosaves and saves are very distinct and an autosave could be happening and then a save fired, these shouldn't interfere with each other. ideally, you should be able to click the update button even while an autosave is firing?

if you are concerned about race conditions between the two, we could drop isAutosaving and prevent saves during autosaves and autosaves during saves.

switch ( action.type ) {
case 'REQUEST_POST_UPDATE':
const isAutosave = action.options && action.options.autosave;
return !! isAutosave;
case 'RESET_AUTOSAVE':
return false;
}

return state;
}

/**
* Reducer returning the block selection's state.
*
Expand Down Expand Up @@ -873,6 +893,7 @@ export function saving( state = {}, action ) {
error: null,
};

case 'RESET_AUTOSAVE':
case 'REQUEST_POST_UPDATE_SUCCESS':
return {
requesting: false,
Expand Down Expand Up @@ -1056,8 +1077,26 @@ export const blockListSettings = ( state = {}, action ) => {
return state;
};

/**
* Reducer returning the most recent autosave.
*
* @param {Object} state The autosave object.
* @param {Object} action Dispatched action.
*
* @return {Object} Updated state.
*/
export const autosave = ( state = null, action ) => {
Copy link
Member

Choose a reason for hiding this comment

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

Can we add JSDoc describing what state is being tracked here.

Copy link
Member Author

Choose a reason for hiding this comment

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

added in dfe6bc3

switch ( action.type ) {
case 'RESET_AUTOSAVE':
return action.post;
}

return state;
};

export default optimist( combineReducers( {
editor,
isAutosaving,
currentPost,
isTyping,
blockSelection,
Expand All @@ -1070,5 +1109,6 @@ export default optimist( combineReducers( {
notices,
sharedBlocks,
template,
autosave,
settings,
} ) );
Loading