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: Factor initial edits to own state #11267

Merged
merged 5 commits into from
Nov 6, 2018
Merged

Conversation

aduth
Copy link
Member

@aduth aduth commented Oct 30, 2018

Extracted from / complements: #10844
Partially addresses: #7409

This pull request seeks to refactor the treatment of "initial edits" out of the state.editor.present.edits to allow for the existing edits state to represent only user-initiated edits to be considered for change detection. This will enable some simplification of change detection reset conditions, and works toward #10844's goal of considering "dirtiness" as a condition of a non-empty edits state.

Implementation notes:

While I think this partly moves away from a goal of eliminating the editor setup actions, it concedes in acknowledging that there should be at most a single parse which occurs at the start of an editing session, with consideration of both edits and content. Thus, it is implemented here as part of the effect handler for SETUP_EDITOR. I still have some faith this could be refactored in the future, but have chosen the most direct option to supporting #10844 here.

Testing instructions:

Verify there are no regressions in the behavior of "initial edits", notably:

  • Saving a new draft with an empty title does not show "Auto Draft" upon reload
  • The demo content works as expected

Ensure tests pass:

npm test

Follow-up tasks:

@aduth aduth added Framework Issues related to broader framework topics, especially as it relates to javascript [Package] Editor /packages/editor labels Oct 30, 2018
@aduth aduth force-pushed the add/initial-edits-state branch from ff8f2f9 to 5f457aa Compare October 30, 2018 20:27
@aduth
Copy link
Member Author

aduth commented Oct 30, 2018

I still need to add some test improvements here.

Edit: Added in rebased e59e7d0.

@aduth
Copy link
Member Author

aduth commented Oct 30, 2018

Related: #10827

I'd incorporated getMutateSafeObject from there to the implementation here. I think also the diff and merge functions there will help further eliminate the redundancy between edits and initialEdits reducers.

@aduth aduth force-pushed the add/initial-edits-state branch 2 times, most recently from e59e7d0 to 76b0097 Compare October 31, 2018 19:24
@aduth aduth added this to the 4.3 milestone Nov 2, 2018
@aduth aduth force-pushed the add/initial-edits-state branch from 76b0097 to 8d38ce4 Compare November 2, 2018 21:11
*
* @return {Object} Mutation-safe object.
*/
function getMutateSafeObject( original, working ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we consider https://github.com/mweststrate/immer instead of this?

Copy link
Member Author

@aduth aduth Nov 5, 2018

Choose a reason for hiding this comment

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

Can we consider https://github.com/mweststrate/immer instead of this?

I'm not too opposed to the idea of it as a general pattern, but for one-off use it seems maybe a bit much both in terms of bundle size impact and developer knowledge barrier. Should we discuss separate to this pull request? I do think Immer seems to tackle well a pattern which is very prevalent in our stores.

Copy link
Contributor

Choose a reason for hiding this comment

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

getMutateSafeObject can also be considered "something to learn" but yes definitely a topic for a separate discussion.

Copy link
Member Author

Choose a reason for hiding this comment

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

There are degrees of learning 😆


return action.edits;

case 'SETUP_EDITOR_STATE':
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a bit confusing? Why do we need to consider two "setup" actions here? can't we just merge both in one? It probably means only use SETUP_EDITOR_STATE 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.

This is a bit confusing? Why do we need to consider two "setup" actions here? can't we just merge both in one? It probably means only use SETUP_EDITOR_STATE here.

Right, this confused me as well, and I left it as a note in the original comment to circle back to this:

Follow-up tasks:

[...]

SETUP_EDITOR handling of blocks should be eliminated in favor of a consistent, single RESET_BLOCKS action.

This is a bit of consistency for consistency's sake. You can see similar use in untouched existing reducers of editor.


return state;

case 'UPDATE_POST':
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure I understand why we need to address this here? my thinking is that initialEdits should be "overriden" by regular edits anyway when saving, why do we need to change the "initial edits" when we "edit" a post?

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 issue we need to avoid is having an initial edit resurface when an edits key is unset by UPDATE_POST.

An alternative here (perhaps better even) is to unset keys from initialEdits when an (assumed-user-initiated) EDIT_POST occurs (i.e. the initial edit can be discarded at the point where a user defines their own value).

Copy link
Contributor

Choose a reason for hiding this comment

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

How does a user unset an edit? editPost( { something: undefined } )? Can this be considered as revert to initial value, which means use initialEdit if 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.

It becomes unset when the edit matches the saved value of the post. So if there's an initial edit lingering, it will become (wrongly) assumed as an edit.

Copy link
Contributor

Choose a reason for hiding this comment

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

It becomes unset when the edit matches the saved value of the post

Should this be: "become unset if it matches the initial edit if defined, the saved value otherwise?

Isn't it a bug now?

Copy link
Member Author

@aduth aduth Nov 5, 2018

Choose a reason for hiding this comment

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

It becomes more an issue when considering the demo post, which should be immediately saveable even if technically editor.edits would be empty (since it's all initial edits).

(We should have an E2E test for this)

Copy link
Contributor

Choose a reason for hiding this comment

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

One thing I noticed which is weird though in our current behavior is that I'd have expected the "save" button to be disabled when you open the demo page without any modification. And if the current behavior is the right one, that I'm completely confused :P

Copy link
Contributor

Choose a reason for hiding this comment

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

(I wrote the last comment without reading your response :P )

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 I may have mispoke here:

Edit title back to empty:

post = { title: '' };
initialEdits = { title: 'demo' };
edits = {}; // title omitted because matches post value

Data is never unset from edits when the user makes a change in the interface:

case 'EDIT_POST':
case 'SETUP_EDITOR_STATE':
return reduce( action.edits, ( result, value, key ) => {
// Only assign into result if not already same value
if ( value !== state[ key ] ) {
// Avoid mutating original state by creating shallow
// clone. Should only occur once per reduce.
if ( result === state ) {
result = { ...state };
}
result[ key ] = value;
}
return result;
}, state );

This only happens when they save the post, or the post is otherwise reset:

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 ( value !== getCanonicalValue( key ) ) {
return result;
}
if ( state === result ) {
result = { ...state };
}
delete result[ key ];
return result;
}, state );

Since these are the same triggers for resetting initialEdits, there shouldn't be an issue here.

Copy link
Contributor

Choose a reason for hiding this comment

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

👍

@aduth
Copy link
Member Author

aduth commented Nov 5, 2018

I'm working to fix up the failing tests. Must have gone wrong in the rebase.

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.

I still don't like that the initialEdits should consider any user edits. I think we should just keep them as initial edits until the first save.

But that said, that's not a big concern, I'm approving the PR. Let's move forward.

@aduth
Copy link
Member Author

aduth commented Nov 6, 2018

I still don't like that the initialEdits should consider any user edits. I think we should just keep them as initial edits until the first save.

I'm not too opposed to dropping this. I don't think it has much of an impact in either case, given that user edits will always take precedence and never be unset until the save.

I'm going to do a final pass over the changes here to see if it makes sense / if we need more test coverage / etc.

@aduth
Copy link
Member Author

aduth commented Nov 6, 2018

I still don't like that the initialEdits should consider any user edits. I think we should just keep them as initial edits until the first save.

In looking over the proposed changes, initialEdits doesn't respond to edits. There should be effectively no changes to the state value between the initial setup and the first save. There might be some confusion in the action.edits value of the UPDATE_POST action, but this in-fact the save action.

@aduth aduth force-pushed the add/initial-edits-state branch from dc88ebd to 3cb31b6 Compare November 6, 2018 20:14
@aduth
Copy link
Member Author

aduth commented Nov 6, 2018

I've rebased and added a number of new tests, some only incidentally related:

Our end-to-end utility treatment of querystrings is a bit contrary to expectations, but I opted to not mess with it too much here.

@aduth
Copy link
Member Author

aduth commented Nov 6, 2018

Aside: I wanted to refactor this test to use the query arguments for prepopulating a title (post-new.php?post_title=foo):

it( 'should not focus the title if the title exists', async () => {
// Enter a title for this post.
await page.type( '.editor-post-title__input', 'Here is the title' );
// Save the post as a draft.
await page.click( '.editor-post-save-draft' );
await page.waitForSelector( '.editor-post-saved-state.is-saved' );
// Reload the browser so a post is loaded with a title.
await page.reload();
const activeElementClasses = await page.evaluate( () => {
return Object.values( document.activeElement.classList );
} );
const activeElementTagName = await page.evaluate( () => {
return document.activeElement.tagName.toLowerCase();
} );
expect( activeElementClasses ).not.toContain( 'editor-post-title__input' );
// The document `body` should be the `activeElement`, because nothing is
// focused by default when a post already has a title.
expect( activeElementTagName ).toEqual( 'body' );
} );

But in fact it fails, since the test makes the wrong assumption that only non-new posts can have a title. It still focuses the title for a new post, even if the title is non-empty.

cc @tofumatt re: #9608

@aduth aduth merged commit 8cbfb3c into master Nov 6, 2018
@aduth aduth deleted the add/initial-edits-state branch November 6, 2018 20:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Framework Issues related to broader framework topics, especially as it relates to javascript [Package] Editor /packages/editor
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants