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

Editor: Reshape editor state for optimized and accurate dirtiness detection #10844

Merged
merged 3 commits into from
Nov 8, 2018

Conversation

aduth
Copy link
Member

@aduth aduth commented Oct 21, 2018

Closes #10427
Closes #7409

This pull request seeks to refactor editor state to to enable a more sensible, accurate, and performant dirty detection routine. Rather than representing isDirty as a reflection of the entirety of the state.editor state object, it's now been moved to only handle the new state.editor.blocks, which contains byClientId and order (previously state.editor.blocksByClientId and state.editor.blockOrder). Dirty detection for fields other than content are reflected by the presence of any keys in state.editor.edits.

This helps to support:

Previous attempts to implement this had been unsuccessful in that withChangeDetection adds a property to the combined reducer which was incompatible with Redux's combineReducers (logging an error to the console). To work around this, the combineReducers exported from @wordpress/data was swapped with an in-place-compatible, more-performant variation turbo-combine-reducers (authored by yours truly).

Testing Instructions:

Verify that there is no regressions in the behavior of autosave, namely that it occurs after delay if and only if one of content, title, or excerpt is updated.

Conversely, verify that if a field other than the three aforementioned is updated (e.g. categories added or removed), it is reflected by the UI as needing save ("Save Draft" becomes active). An autosave should not occur, but you should be prompted about unsaved changes if you attempt to leave the page before pressing "Save Draft".

Finally, verify that if changing both one of content, title, excerpt and another field, that the autosave does occur, and once completed, the editor is still in a "dirty" state ("Save Changes" present, after brief delay once save finishes). This is because autosaves do not include other fields.

@aduth aduth added [Status] In Progress Tracking issues with work in progress [Type] Performance Related to performance efforts [Package] Data /packages/data [Feature] Saving Related to saving functionality labels Oct 21, 2018
@aduth aduth requested a review from ellatrix October 21, 2018 01:42
export function hasChangedContent( state ) {
return (
state.editor.present.blocks.isDirty ||
'content' in state.editor.present.edits
Copy link
Member

Choose a reason for hiding this comment

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

I don't really get what this second condition is checking for.

Copy link
Member Author

@aduth aduth Oct 22, 2018

Choose a reason for hiding this comment

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

I don't really get what this second condition is checking for.

I'll add a comment to clarify, but it's a combination of two things which require this consideration:

  • While content in Visual mode is represented by the blocks state, in Text mode it is tracked by edits.content.
  • edits is intended to contain only the different property values, so it is expected that the mere presence of any property is an indicator that the value is different than what is known to be saved.

resetTypes: [ 'SETUP_EDITOR_STATE', 'REQUEST_POST_UPDATE_START' ],
ignoreTypes: [ 'RECEIVE_BLOCKS', 'RESET_POST', 'UPDATE_POST' ],
} ),
] )( {
Copy link
Member

Choose a reason for hiding this comment

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

Is everything below here just indented? It's a bit hard to see the code changes, if there are any.

Copy link
Member Author

Choose a reason for hiding this comment

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

Is everything below here just indented? It's a bit hard to see the code changes, if there are any.

Yes, no changes aside from indentation.

GitHub has an option to hide whitespace-only changes, which should help make things a bit clearer.

image

Copy link
Contributor

Choose a reason for hiding this comment

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

Funny that this Higher order reducer moved from "edits" to just "blocks". It almost feels like we need an editBlocks :)

@ellatrix
Copy link
Member

To work around this, the combineReducers exported from @wordpress/data was swapped with an in-place-compatible, more-performant variation turbo-combine-reducers (authored by yours truly).

If we're swapping something from @wordpress/data because it is not performant enough, might it not be better to adjust combineReducers in @wordpress/data?

@aduth
Copy link
Member Author

aduth commented Oct 22, 2018

If we're swapping something from @wordpress/data because it is not performant enough, might it not be better to adjust combineReducers in @wordpress/data?

To be clear, we're not swapping it because of performance, we're swapping it to work around the errors which are logged by the shape validation in the Redux implementation, triggered by the main work here in using combineReducers with blocks state's withChangeDetection. Performance is a "plus".

To your question, is the point that we should bring it in rather than rely on it as a third-party dependency? I don't really feel strongly one way or the other. I implemented it as a separate project largely to disable the availability of transpilation, for performances' sake (it's a weekend side hack project, something I felt fine to "waste" time over-optimizing).

@WordPress WordPress deleted a comment from ellatrix Oct 22, 2018
@WordPress WordPress deleted a comment from ellatrix Oct 22, 2018
@WordPress WordPress deleted a comment from ellatrix Oct 22, 2018
@WordPress WordPress deleted a comment from ellatrix Oct 22, 2018
@WordPress WordPress deleted a comment from ellatrix Oct 22, 2018
@aduth
Copy link
Member Author

aduth commented Oct 23, 2018

I've rebased and pushed a number of revisions, including updated unit tests.

There's failing end-to-end tests which are legitimate, and which highlight an issue I've dealt with previously: We can't naively consider emptiness of state.editor.edits object for considering dirtiness currently, because we have this concept of "initial edits" to e.g. set the 'Auto-Draft' default title to an empty string. This isn't initiated by the user, but is a field we need to send in the save, but should not trigger a dirty prompt for the user in a new post (as it does in the branch currently).

I'll need to think on this some more.

Related: #9403

@aduth
Copy link
Member Author

aduth commented Oct 30, 2018

Extracted substitution of combineReducers implementation separately to #11255

Edit: #11255 has been merged and this pull request rebased to drop the relevant commit.

@aduth
Copy link
Member Author

aduth commented Oct 30, 2018

This has been rebased and is now passing. The changes are still substantial, and I've extracted at least one more commit out (the initial edits refactoring, to #11267). It was originally developed separately, but cherry-picked into this branch to verify its viability for addressing the remaining E2E test failures.

@aduth
Copy link
Member Author

aduth commented Oct 31, 2018

Extracted blocksByClientId, blockOrder -> blocks.byClientId, blocks.order state shape refactoring to #11315.

Edit: #11315 has been merged and this pull request rebased to drop the relevant commit.

@aduth aduth removed the [Status] In Progress Tracking issues with work in progress label Oct 31, 2018
@aduth
Copy link
Member Author

aduth commented Oct 31, 2018

#11315 has been separately merged and this one rebased in response, reducing from +1,556 −1,044 changes to +550 −242.

@aduth aduth added this to the 4.3 milestone Nov 2, 2018
@aduth aduth force-pushed the update/dirty-content branch 2 times, most recently from 9fc77bb to 804a206 Compare November 6, 2018 20:42
@aduth
Copy link
Member Author

aduth commented Nov 6, 2018

#11267 has been separately merged and this one rebased in response, reducing from +660 -243 changes to +261 -111.

I'm not planning on extracting any other separate pull requests. This one should be considered ready for final review as-is.

return true;
}

return inSomeHistory( state, isEditedPostDirty );
Copy link
Contributor

@youknowriad youknowriad Nov 7, 2018

Choose a reason for hiding this comment

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

So if I understand properly. We conder a post dirty until it's completely saved (successful response)? right? any particular reason? should we clarify why in a comment? (I know it's not new in this PR though)

Copy link
Member Author

Choose a reason for hiding this comment

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

So if I understand properly. We conder a post dirty until it's completely saved (successful response)? right? any particular reason? should we clarify why in a comment? (I know it's not new in this PR though)

It's because change detection (now only for blocks) is reset at the start of the save, but we still want to avoid allowing a user to reload while that request is still pending.

It's covered in this test:

it( 'Should prompt if changes and save is in-flight', async () => {
await page.type( '.editor-post-title__input', 'Hello World' );
// Hold the posts request so we don't deal with race conditions of the
// save completing early. Other requests should be allowed to continue,
// for example the page reload test.
await interceptSave();
// Keyboard shortcut Ctrl+S save.
await pressWithModifier( META_KEY, 'S' );
await releaseSaveIntercept();
await assertIsDirty( true );
} );

I'll see if I can form a useful comment 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.

I'll see if I can form a useful comment here.

Added in rebased fd7a8ec.

// flag in place of content field comparison against the known autosave.
// This is not strictly accurate, and relies on a tolerance toward autosave
// request failures for unnecessary saves.
if ( hasChangedContent( state ) ) {
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 this is the fix for the performance issue.

It might not be easy but I wonder if we can add an e2e test checking that we don't run save functions as we type :)

Copy link
Member Author

Choose a reason for hiding this comment

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

It might not be easy but I wonder if we can add an e2e test checking that we don't run save functions as we type :)

Hmm, might not be too hard! I'll give it a shot.

Copy link
Contributor

@youknowriad youknowriad left a comment

Choose a reason for hiding this comment

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

LGTM 👍

@aduth aduth merged commit 005c790 into master Nov 8, 2018
@aduth aduth deleted the update/dirty-content branch November 8, 2018 18:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Feature] Saving Related to saving functionality [Package] Data /packages/data [Type] Performance Related to performance efforts
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants