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: Leverage core data entities for edits, history, change detection #16761

Closed
wants to merge 6 commits into from

Conversation

aduth
Copy link
Member

@aduth aduth commented Jul 25, 2019

This pull request seeks to begin exploring a fairly significant refactor to minimize the responsibilities of the editor package by delegating edits tracking to core-data "entities" behavior. Doing so should allow for improved support of edits tracking to multiple entities in the same editor session (multiple posts and non-post entities such as site options or even third-party APIs). At that point, the save mechanics of the existing post editor serve more as an intent to persist (sync) all outstanding edits of any entity.

Work in Progress: Basic behaviors work as expected, but there are many issues yet to be resolved, particularly around post "dirtying" and undo history. It is entirely expected that many of the proposed changes will be extracted into their own pull requests to simplify the eventual "final" review-ready version of this pull request.

Implementation Notes:

This exploration required fleshing out the core-data module to add additional support for:

  • Edits tracking
  • Undo history

Existing functionality for these features in the editor module is effectively soft-deprecated. The selectors exist today and are not marked as deprecated, but largely only because the larger effort to port existing editor components to use equivalent core-data selectors isn't considered as part of this branch.

In enhancing the core-data module, it was found to lack some support for more advanced entities tracking that had been implemented in the editor module:

  • When receiving an entity, we should try to reuse value references when unchanged, since many components / custom sources define dependencies on this data, and should only be updated when the values are meaningfully different.

To represent the editor's current treatment of blocks state as the temporal, preferred representation of content, the edits implementation supports semi-hidden non-enumerable properties. Using Object.defineProperty, both blocks and content are assigned as edits of a post, but are only computed or considered for dirtiness, post history, or save payloads as appropriate. For more information, see the inline code comments of the editor's resetEditorBlocks action. With this change, the editor module no longer tracks in state full copies of the post and its content, nor the current blocks value.

General summary of pending work:

  • Extract out commits to separate pull requests when applicable
  • Handle autosave
  • There needs to be some deeper consideration of performance impact. I do not expect the performance of the current state of this branch is anywhere close to that of master.
  • Documentation
  • Unit tests
  • Consider how to represent the editor's initialEdits prop in this implementation. This is currently addressed for "Auto Draft" title with server-side filtering. I intend that these changes are valuable irrespective what comes of initialEdits, but we should consider how to continue supporting these initial edits values. One use case is the Demo page, which pre-fills edited content but the content is not intended to be dirtying.
  • There are a number of inline "TODO" code comments which should be addressed and removed.

@aduth aduth added [Type] Technical Prototype Offers a technical exploration into an idea as an example of what's possible [Status] In Progress Tracking issues with work in progress [Package] Core data /packages/core-data [Package] Editor /packages/editor labels Jul 25, 2019
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.

This is just a few quick questions but I find it pretty telling that it's more code removed than added (even if it lacks documentation...).


const result = {};
for ( const key in nextItem ) {
if ( isEqual( nextItem[ key ], item[ key ] ) ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Did we do this previously for "currentPost"? Do you recall why? any concern about perf?

Copy link
Member Author

Choose a reason for hiding this comment

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

Did we do this previously for "currentPost"?

Yes, sort of. I believe the way it works currently is that we (for better or worse) don't actually reset (RESET_POST) the post, but only ever update (UPDATE_POST) the persisted values, where this exact behavior to replace the persisted values only if deeply equal:

case 'UPDATE_POST':
case 'RESET_POST':
const getCanonicalValue = action.type === 'UPDATE_POST' ?
( key ) => action.edits[ key ] :
( key ) => getPostRawValue( action.post[ key ] );
return reduce( state, ( result, value, key ) => {
if ( ! isEqual( value, getCanonicalValue( key ) ) ) {
return result;
}
result = getMutateSafeObject( state, result );
delete result[ key ];
return result;
}, state );

Do you recall why?

Why we did, or didn't? At least for the purposes of this pull request, it was important to ensure that dependencies don't fire as having been changed when the values are effectively the same.

For example, the meta custom source:

meta: yield select( 'core/editor', 'getEditedPostAttribute', 'meta' ),

if ( ! isShallowEqual( dependencies, lastDependencies ) ) {
lastBlockSourceDependencies.set( source, dependencies );
// Allow the loop to continue in order to assign latest
// dependencies values, but mark for reset.
reset = true;
}

any concern about perf?

Not really. This only happens on full reset (at load and after a save). And even then, most entities are pretty flat, so deep equality doesn't make much of a difference.

return state;
} );

const saving = ifMatchingEntityConfig( ( state = {}, action ) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Curious why we need this. Can we use the resolving 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.

Curious why we need this. Can we use the resolving state?

Unless I'm totally forgetting, that doesn't apply since resolvers are for selectors? Saving state occurs as a result of dispatching.

That being said, I could see some usefulness in knowing whether an action has "finished", and I imagine it would be possible to implement.

Copy link
Contributor

Choose a reason for hiding this comment

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

That being said, I could see some usefulness in knowing whether an action has "finished", and I imagine it would be possible to implement.

At the @wordpress/data level? That would be nice.

@@ -277,6 +369,82 @@ export function autosaves( state = {}, action ) {
return state;
}

export const undo = combineReducers( {
Copy link
Contributor

Choose a reason for hiding this comment

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

I was doubtful about the refactoring to rely on actions instead of history for this but just the fact that it makes the implementation "separate" from the original reducer (SoC) seems cleaner.

@epiqueras
Copy link
Contributor

This is awesome! Great work 🚀 I just finished reading all of the changes. I think this is the right approach. Let me know when you think it's polished enough to receive detailed feedback.

Let's try to get the extracted PRs merged as well as a way to trim this one down a bit.

const postId = yield select( 'core/editor', 'getCurrentPostId' );
const edits = yield select( 'core', 'getEntityRecordEdits', 'postType', postType, postId );
const record = { id: postId, ...edits };
yield dispatch( 'core', 'saveEntityRecord', 'postType', postType, record );
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't this just persist the current edits if none are provided?

Copy link
Member Author

Choose a reason for hiding this comment

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

Shouldn't this just persist the current edits if none are provided?

Could you clarify what you mean by this? The edits state should ideally reflect the minimal difference between the server-persisted copy, so what's sent here is only those subset of properties.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, but is saveEntityRecord ever going to be called with anything other than the result of getEntityRecordEdits? I think that can go inside saveEntityRecord, at least as a default value.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, but is saveEntityRecord ever going to be called with anything other than the result of getEntityRecordEdits?

I think it could, yes. I might want to totally bypass local edits, and just save something outright.

That being said, I could see an argument to be made to your latter point that it might make sense as a default, or to merge pending edits into what is passed to saveEntityRecord, though it's unclear whether those edits would be preferred over or defaulted in absence of what's passed explicitly to saveEntityRecord.

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 this goes in conjunction with the result of getEntityRecord. If by default it returns the entity wiith the edits, save should also take the edits into account otherwise it's going to be confusing for third-party usage: trigger save and get a different version from the one visible before the save.

My opinion is still to keep this explicit (not bake edits) because that's how it works now otherwise we might risk some BC concerns in plugins.

Copy link
Contributor

Choose a reason for hiding this comment

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

defaulted in absence of what's passed explicitly to saveEntityRecord.

I think that would avoid BC issues right?

Copy link
Contributor

Choose a reason for hiding this comment

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

It would yes, but don't you think it's weird that getEntiityRecord would return the persisted value by default, calling saveEntityRecord would bring new changes and the user might be confused (where are they coming from)?

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe it's better if we have a separate action called saveEntityRecordEdits?

@kadamwhite
Copy link
Contributor

I'm cross-linking this comment from the extracted PR for moving auto-draft handling to the server, to echo the previous question about whether the REST API controller was a more appropriate place for that change than the insert post method. As the PR's been merged I'm noting it here for visibility.

@aduth
Copy link
Member Author

aduth commented Nov 4, 2019

Closing as superseded by #16814, #16867, #16903, #16932.

@aduth aduth closed this Nov 4, 2019
@aduth aduth deleted the add/data-edits branch November 4, 2019 15:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Package] Core data /packages/core-data [Package] Editor /packages/editor [Status] In Progress Tracking issues with work in progress [Type] Technical Prototype Offers a technical exploration into an idea as an example of what's possible
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants