-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
State: Trigger autosave as standard save for draft by current user #7130
Conversation
Are there any predictable non-desirable side effects of using normal save for an autosave? I feel the number of revisions has increased e.g: before for each autosave of a new not previously saved post we had no revisions now I think we have one revision on each autosave. |
@jorgefilipecosta Part of the problem is that this is already how the server is treating it under these conditions, so we're simply not respecting (or taking advantage of) that. |
Sorry @aduth it seems the number of revisions created is the same in both cases, I did something wrong in the first test cases. When we execute this actions in master the autosave contains the last version and in the classic editor, there are no revisions available. If we execute this tests on this branch and we go to the classic editor we have the post as expected but we have one revision each autosave. |
Nice sleuthing @jorgefilipecosta . Digging further, this is related to the constant that the endpoint defines when performing as an autosave, even if it ultimately just ends up calling the same gutenberg/lib/class-wp-rest-autosaves-controller.php Lines 168 to 170 in 7141ce6
This is considered in preventing the creation of a revision in With this in mind, I'd be inclined that it might be better to follow the other option mentioned in my original comment about not trying to vary this in the client before the save, but rather calling the autosave endpoint and inferring after the fact whether it was saved with parent. There are a few complications to this which might require some broader refactoring to the save process. |
I attempted to change the logic such that we infer after-the-fact whether the autosave was treated as a regular save, but encountered an issue: We can't reset the post dirty state after the save completes, because the user may have made edits while the request was in-flight that need to be respected as dirtying the post. Instead, what I opted for in c36ad21 was to keep the logic which determines from the client whether the server will treat the request as a regular save, considering it as a regular save for purposes of resetting change detection (before request is issued) and in resetting some post fields, but importantly still using the I still think there's room for improvement here:
The effect handler is quite complex, and I'm not super comfortable with all of the conditions necessary to reconcile saving and autosaving therein. Some of this may be unavoidable, which is why I'm going to try to work on some more end-to-end tests here. There's a new one already, though I also want to ensure that previewing works correctly, which is also meant to be addressed by these changes. |
I took a different approach in 4df1bb6, leveraging This required tinkering with the reset and ignore types of One issue that came up in discussing this with @youknowriad are the fields we expect to update in an autosave request which falls under the condition of "same user draft". These are applied as full saves, but do we send just the title, content, and excerpt? What about other fields like status or terms? We can't treat the post as not having unsaved changes unless we can be certain that these are all updated on the server. Currently we do send them all, but is it expected that the server would save all these values, or just those relevant for a revision? I got to thinking that since withChangeDetection( {
isChanging( state, nextState ) {
return state !== nextState || ! isEmpty( nextState.edits );
},
} ); With above, we could assure that if only a subset of properties are updated, leaving remaining unsaved changes, they would be accounted for in change detection (the dirty post prompt). |
// is simply reversing the assumption that the updates | ||
// were applied to the post proper, such that the post | ||
// treated as having unsaved changes. | ||
type: isRevision ? REVERT : COMMIT, |
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.
If the server returns a different excerpt (or title, content, etc... ) on the autosave endpoint ( because of a filter, for example) this revert is going to revert the changes made by the server, and they are not going to be reflected by UI right? This is something that I'm not sure if it is even possible but I may be missing something, and it may be helpful for me to understand the changes.
Edited: I think I understand the idea now, the revert is only going to revert the update post action with the redux optimistic id the other actions e.g: resetAutosave don't have the redux optimistic id and will stay untouched. So if the endpoint returns something different we are going to reflect it :)
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.
If the server returns a different excerpt (or title, content, etc... ) on the autosave endpoint ( because of a filter, for example) this revert is going to revert the changes made by the server, and they are not going to be reflected by UI right? [...] I think I understand the idea now, the revert is only going to revert the update post action with the redux optimistic id [...] So if the endpoint returns something different we are going to reflect it
Yes and no, but it's an important point to raise, one which is partially considered in #7124 but which this pull request doesn't address entirely: A resetPost
called after the save completes will update the canonical post values and remove edits from state.editor.edits
. A revision, if created, is a separate entity from the post and shouldn't update state.currentPost
. But: Draft autosaves by same author are full saves, not separate revisions. These should update title
, content
, excerpt
for state.currentPost
(not sure about other fields). This doesn't exist yet, but probably should. The UI shouldn't be impacted though, since the edits
reducer does a diff on UPDATE_POST
and RESET_POST
: the edits
values will remain. The one exception is currentPost.status
which needs to be updated by autosave received since it's considered for the isEditedPostNew
selector. Ideally this would be generalized to receive all post attributes which we know to have been updated by an autosave to the root post.
editor/store/effects.js
Outdated
|
||
// Prevent save if not saveable. | ||
const isSaveable = isAutosave ? isEditedPostAutosaveable : isEditedPostSaveable; | ||
if ( ! isSaveable( state ) ) { | ||
return; | ||
} | ||
|
||
const post = getCurrentPost( state ); | ||
const edits = getPostEdits( state ); | ||
let toSend = { | ||
...edits, |
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.
We know that the server does not save all the fields during autosave should we pick here the fields we send to the server, in order not have unused bytes sent to the server? Or they are being sent all the time because the server may threat autosave as normal save and in that case even during an autosave all the edits may get saved?
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.
We know that the server does not save all the fields during autosave
Do we know this? I sincerely don't know and would be curious to know the answer. In theory it seems it should only need to save these fields, but in the implementation it's just calling wp_update_post
and doesn't have its own implementation of prepare_item_for_database
, falling back instead to WP_REST_Revisions_Controller::prepare_item_for_database
. It's hard to follow if these fields might be dropped by part of a schema definition in one of the autosaves, revisions, or post controller classes.
$autosave_id = wp_update_post( wp_slash( (array) $prepared_post ), true ); |
That said, to the previous conversation, it might make things simpler if we could assume that at most, only title, excerpt, and content would be updated in order to know how to update state.currentPost
.
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.
Do we know this? I sincerely don't know and would be curious to know the answer.
I tested this, and for a new post, if I change author and hit preview (i.e. autosave), the author is updated.
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.
We know that the server does not save all the fields during autosave
Nearly all fields (at least date, date_gmt, post_title, post_content, post_excerpt, post_modified, post_modified_gmt), are saved to the autosave as a new wp_posts row linked to the original post by a post_parent column. some fields get a special value: post_name (usually the slug) is stored as [POST_ID]-autosave-v1, post_status is inherit, post_type is revision.
I'm not certain about to_ping, pinged, post_password, ping_status, comment_count, menu_order, comment_status - I think these are saved. guid is a unique id pointing to the autosave.
related - attached meta fields are currently saved to the original post a longstanding core bug, see https://core.trac.wordpress.org/ticket/20299 - we recently fixed this for featured image selection, however a more complete implementation would need to support revisioning of meta, since we need to attach the saved meta to the autosave for previewing. see https://github.com/adamsilverstein/wp-post-meta-revisions and https://core.trac.wordpress.org/ticket/20564
Related (extended by): #7189 |
// is simply reversing the assumption that the updates | ||
// were applied to the post proper, such that the post | ||
// treated as having unsaved changes. | ||
type: isRevision ? REVERT : COMMIT, |
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.
Instead of a revert would the following idea work:?
Save the post the autosave logic (but send flag) and maybe pick the fields to send, and then when we receive the return value from the server we compare the fields against the edits. In the case, a field was present in the edits and not returned by the server we use an action e.g: RESTORE_EDITS to restore those edits. This would make the post dirty again because some fields edits are not saved.
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.
Instead of a revert would the following idea work:?
This reminds me of another goal: For autosaving a new draft post, ideally the user should not be prompted about unsaved changes after the autosave completes. This only works if we know that all fields are transmitted and considered in the save request.
Rather than a standalone action, something like the isChanging
implementation in my comment above might serve the same purpose to what you describe.
It looks like there's the issue I was concerned about:
|
Thanks for testing @youknowriad . That's disappointing, and I can reproduce, but maybe highlights to an opportunity to enhance as described in #7130 (comment) with a more aware change detection which considers the non-empty |
I think I'll plan to merge #7189 into this branch just so I'm not managing related changes in two separate branches / pull requests. |
@youknowriad can you clarify, did you publish the post, or did it remain a draft? Please note this matches the existing behavior in the classic editor for a published post (for draft posts the autosave action is the same as the save draft action and does save the tags). Perhaps for drafts we should avoid using the autosave endpoint, just do a regular draft save (only difference vs clicking the save draft button: update the draft without creating a revision?) |
This has turned into something of a rabbit hole, of all things impacted largely by shared blocks. Below is a brain dump of the problem: We can know whether an autosave will have lingering unsaved changes (e.g. tags) by picking only those fields we expect the endpoint to handle as part of the payload, and in the The Thus, determining whether a post has unsaved changes is a two-step process: Check whether there are any properties in There's a few ways we could go about doing this. Ultimately I landed on refactoring export function isEditedPostDirty( state ) {
return (
state.editor.present.blocks.isDirty ||
! isEmpty( getPostEdits( state ) ) ||
inSomeHistory( state, isEditedPostDirty )
);
} Now enters shared blocks: In order to have consistent treatment of a block regardless of whether it is shared or not, as part of #5228 I moved shared blocks data into We work around this in master by setting an This also surfaces itself in some interesting ways, because we do assume that blocks in Because the gutenberg/editor/store/selectors.js Lines 565 to 579 in 3504ae7
How then do we solve this? We could consider shared blocks data separate from the const block = (
state.editor.present.blocks.byUID[ uid ] ||
state.sharedBlocks.blocks.byUID[ uid ]
); Let alone the confusion caused by mixed messaging in expecting shared blocks to sometimes be referenced by a selector. Where do we go from here?
cc @noisysocks (having experience with shared blocks) |
I do think shared blocks shouldn't live in the editor state at all. I tried doing this in #7080 but to handle nested blocks in shared blocks we need to refactor how to edit shared blocks (how to show the block in edit mode) I'm proposing some ideas in #7119 maybe the way to go is
No matter the solution adopted, It will require a lot of work IMO |
Yeah, "historically" some post boxes on the Edit Post screen save "instantly" (with AJAX), other require a form submission/page reload. Autosave only saves title, content, excerpt. However there are two preview buttons: one to preview a draft post, another for published posts:
Thinking this logic makes sense. We shouldn't update a published post or its settings from autosave or when previewing it. Also thinking that in Gutenberg autosaves can be "full" saves for drafts, but partial saves for published posts, following the above logic. That will remove the long-standing problem with some post boxes being "instant" and others needing the user to click "save". |
This is the assumption I've been operating under, but the testing instructions in #7130 (comment) also apply for draft posts as well (i.e. not saving tags, even though they are included in the payload). I suppose it makes some sense that the endpoint have a consistent schema for the parameters it accepts regardless of published vs. draft, but it requires some additional handling in the client since we can't assume all fields are saved, even if, as a draft, those fields which are saved are applied to the original post (contrasted with a published revision). |
My gut feeling is we should get shared block state out of I understand #5228 was an alternative to #5010 so maybe we can revive that approach? Another reason for getting shared blocks out of |
@noisysocks I have a work-in-progress branch which renders shared blocks as an embedded editor, similar to suggested idea and subsequent discussion in #7119. It works fairly well, though is a significant refactor. Most all of the behaviors I have working cleanly, aside from some few remaining items like shared block deletion and converting to/from shared/static. Given that this bug has persisted much longer than I would have liked, my current plan is to see if I can find a more direct solution here, likely to manually trigger a dirtying change to state after a Separately then we can work toward the refactoring of shared blocks and editor dirtiness as a condition of Line 25 in c6cbfb0
|
Change detection reset for known-to-be-draft, pick from non-effective autosave for resetting post
Passed previously because when autosaving, class is assigned with _both_ `is-saving` and `is-autosaving`. The superset of classes is what what should be expected.
* REST API: Move preview_link to autosaves controller * Post Preview: Redirect to preview by autosave property availability * Preview Button: Use preview link as href if available * Preview Button: Move window bind to after write Was not reliably triggered when redirect occurs by setPreviewWindowLink, possibly because document.open triggered by document.write erased window handlers. * Preview Button: Always trigger autosave when dirty * Preview Button: Consider preview ready by change in value * State: Set default value for savePost options Allows use in handlers to assume options present * Store: Clarify preview link with query parameter * REST API: Set preview link by parent only if autosave * Store: Invalid autosave preview link on save intent Preview link is no longer valid until autosave completes, by which UI can infer availability of preview as being ready to view. * Testing: Add preview E2E tests * Testing: Fix copypasta * Preview: Use correct location property for URL comparison
Must dirty artificially once save completes if there were edits not included in the payload.
111fb73
to
6fa254e
Compare
I've rebased and pushed changes which account for all known flows:
There's some unfortunate complexity in managing this with the autosaves endpoint (related to supported fields and auto-draft status), which I've documented in the relevant Trac ticket. The current implementation for ensuring that unsaved edits (e.g. tags, sticky) are presented in a warning prompt after a draft/auto-draft autosave is intended to be a temporary resolution while a better (bigger) refactor can be achieved (#7409). |
Re: #7130 (comment) , The idea for an gutenberg/editor/utils/with-change-detection/index.js Lines 31 to 34 in 657c368
Since with #7130 (comment) the assumption would be that state changes over time, but the |
I had also attempted to introduce a isShallowEqual( [ state.editor.blocks.byUID, state.editor.blocks.byUID ], snapshot ); This is viable, but only once we refactor blocks state to omit shared blocks, since with |
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.
Thanks for the awesome work on this PR. I tested all the use-cases I can think of and it works as expected for me.
Thanks for the e2e tests
Now reflected in committed updatePost edit of status to draft in save effect for auto-draft (by current user).
There are still some lingering edge case issues with autosave, particularly around non-content edits when an autosave exists for a post, which I've documented in #7416. These exist on master as well. I had started down the path of resolving them here before deciding that this pull request is already large enough as it is, and works well enough for the majority case. |
Closes #7124
This pull request seeks to change an autosave request to occur as a standard save when the saved post is a draft or auto-draft, and the request is issued by the author of the post (i.e. current user is author).
This resolves two issues:
Implementation notes:
Note: This is a stub pull-request while I think through the implementation.
We currently do not track the current user ID in Gutenberg. As a short-term fix, this introduces access on the
window.userSettings.uid
value. Obviously this value may or may not be present, depending on where Gutenberg is implemented, but is assumed to exist in wp-admin.Alternatively, we could trigger the autosave request, and allow the server to perform the necessary logic, testing that
newPost.id !== post.id
to see whether it was effectively saved as autosave. There are two caveats to this:UPDATE_POST
which only occurs for full savesresetPost
with the autosave response value.