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

Block Editor: Add local change detection. #19741

Closed
wants to merge 4 commits into from

Conversation

epiqueras
Copy link
Contributor

Description

This PR enhances change detection mechanisms so that an edit's dirtiness can scope to a specific block and its children, solving issue number 2 found by @jorgefilipecosta in #19521 (review). The approach taken is to have attribute updates optionally provide a root client ID and have the persistent change selector optionally take a root client ID. This way, changing a block's inner blocks that don't save to the top-level won't dirty the top-level.

This PR also solves issue number 1 found by @jorgefilipecosta in #19521 (review) by reusing block editor store caches for the blocks that haven't changed by referential equality after a reset. This way, undoing or redoing won't dirty nested blocks that save to other entities unless they have changed.

How to test this?

Follow the steps for the two issues from #19521 (review).

Types of Changes

New Feature: Change detection/dirtiness can now scope to specific blocks. E.g., template parts.

Checklist:

  • My code is tested.
  • My code follows the WordPress code style.
  • My code follows the accessibility standards.
  • My code has proper inline documentation.
  • I've included developer documentation if appropriate.
  • I've updated all React Native files affected by any refactorings/renamings in this PR. .

// The default save outputs null.
// We can have more nuanced heuristics in the future.
isLocalChange =
select( 'core/blocks' ).getBlockType( getBlockName( rootClientId ) )
Copy link
Member

Choose a reason for hiding this comment

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

If a block implements its own save that returns null, this logic will fail, right?
I also wonder about back compatibility: Imagining a current block that uses InnerBlocks without any save and listens to save actions (e.g: executes when saved state changes) to save its InnerBlocks using a rest API. Would a block like that break with this change because if its InnerBlocks are changed the post would not become dirty? I think the odds of a block like that existing are low, but at least we would need to create a dev note.

As an alternative solution, what if we use a supports flag to indicate this type of blocks?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If a block implements its own save that returns null, this logic will fail, right?
I also wonder about back compatibility: Imagining a current block that uses InnerBlocks without any save and listens to save actions (e.g: executes when saved state changes) to save its InnerBlocks using a rest API. Would a block like that break with this change because if its InnerBlocks are changed the post would not become dirty?

Yes, for both, I don't see value in supporting either, though.

As an alternative solution, what if we use a supports flag to indicate this type of blocks?

It would work, but it would be adding more cruft.

I am not sure. Thoughts @youknowriad?

Copy link
Contributor

Choose a reason for hiding this comment

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

If i understand properly where we're coming from here. i believe all these issues are happening because we mix the blocks of the sub-entity into the blocks of the top entity.

I think we discussed this previously, IMO, we should find a way to stop doing this as it doesn't make sense to mix content from entity A into an edit of entity B.

One potential idea I shared previously is to have a unique block list in the "editor" store and each entity has its own "blocks" edits in the "core" store.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think the issue is a bit more general than that.

We save serialized content yet detect changes on block objects. Any edit to a block will be dirtying even if its serialization doesn't change.

One potential idea I shared previously is to have a unique block list in the "editor" store and each entity has its own "blocks" edits in the "core" store.

So like reusable blocks, but resolved into a single blocklist? What do you see as the benefits of that approach over this one?

Copy link
Contributor

Choose a reason for hiding this comment

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

So like reusable blocks, but resolved into a single blocklist? What do you see as the benefits of that approach over this one?

The benefits is that an entity is dirty only if its block list is dirty (A template block list won't include the blocks from the template area)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The same is true in this PR.

So for your suggestion, we would need a new property in block objects that potentially contain a selector that gets a block list for their inner blocks. The editor store would then dynamically resolve a complete block list. However, when the block editor makes an edit, it will mutate the full block list, how would the editor know to not dirty the top-level entity. It seems like we are trading some complexity for another.

Copy link
Contributor

Choose a reason for hiding this comment

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

Agreed no silver bullet here. I think "onChange" of the "controlled" inner blocks might be the place where we dispatch the change to two block lists. Conceptually speaking, I feel like having in an entity of core-data, things that are saved in another entity will just bite us with this kind of hidden bugs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think "onChange" of the "controlled" inner blocks might be the place where we dispatch the change to two block lists.

We'll still have the same issues when we nest one of these blocks inside another.

Conceptually speaking, I feel like having in an entity of core-data, things that are saved in another entity will just bite us with this kind of hidden bugs.

I don't see a conceptual issue with it. They are object references, it's not like the data is duplicated. That's why this PR was so simple to do.

@epiqueras
Copy link
Contributor Author

Can we revisit this @jorgefilipecosta?

@Addison-Stavlo
Copy link
Contributor

I am just testing the flows listed for Bug 1 and Bug 2 from #19521 (review).
Hopefully I understand the background context here, but if not please forgive any misinterpretation at this point.

I am assuming 'dirty' in this context corresponds to being in a state of unsaved changes, whether it should be in that state or not. However, I am a bit unsure how to tell which sections are currently 'dirty' - what I am currently going on is that 'something' is dirty when we are able to 'Save Draft' in the top right.

I am editing a template with template paragraphs "Template P1" and "Template p2".
Below these is a Template Part with 2 paragraphs "TP-P1" and "TP-P2".

Bug 1 flow -

bug1
When editing content outside the template-part, the post becomes savable as expected.
When using "undo" the content is recovered as expected, but the post is still dirty.

Bug 2 flow -

bug2
When editing text inside the template-part, the post does not become savable.
When pressing "undo", the content is not recovered.

Comparing to Master branch -

Flow 2 on master does become savable and brings back the content on 'undo', but stays dirty.
Flow 1 seems to act the same as on master, so I may be looking at the wrong thing here. In which case I apologize for the misunderstanding.

@epiqueras
Copy link
Contributor Author

Bug 1 was about template parts becoming dirty from unrelated "undos" in the parent template. This is fixed, as can be seen in your gif.

Bug 2 was about changes in template parts, making the parent template dirty. This is also fixed, as can be seen in your gif.

@epiqueras
Copy link
Contributor Author

Note that the publish button shows the dirty template parts.

@Addison-Stavlo
Copy link
Contributor

Thanks for the clarification, this makes much more sense now!

It does look like both bugs are fixed.

However, the "undo" button that becomes available after making changes to a template-part no longer undoes the changes to the template-part. What are your thoughts on this? Should this be considered a regression to fix before merging this PR? Handled in a follow-up PR? Or any other reasoning about this?

@epiqueras
Copy link
Contributor Author

Does it behave differently to master in that sense? Have you tried making enough dirtying edits, like typing with pauses and block insertion/removal?

Copy link
Contributor

@Addison-Stavlo Addison-Stavlo left a comment

Choose a reason for hiding this comment

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

Does it behave differently to master in that sense? Have you tried making enough dirtying edits, like typing with pauses and block insertion/removal?

Yes, it seems a little different than master. On Master, it seems all edits to the template-part are undo-able. Including changing text content of a paragraph or adding/removing blocks. On this branch, it seems text content is only undo-able if it is done after a block insertion or removal.

Testing the following flow:

  • Edit first template-part paragraph
  • Insert a block (image block in this case)
  • Edit second template-part paragraph.
  • Try to undo everything

Master
master-undo

This Branch
branch-undo

Notice how on master all content is reverted, after all "undo"s are made we return to have "TP-P1" and "TP-P2" as the content in the template part paragraphs.

However, on this branch only content changed after the image block is inserted is reverted. Ending in "TP" and "TP-P2" after all undos are done. (Also, it seems adding the block to the template-part dirties the template still - but thats not a change from master)

Comment on lines -503 to +538
...omit( state.cache, visibleClientIds ),
...mapValues( flattenBlocks( action.blocks ), () => ( {} ) ),
...state.cache,
Copy link
Contributor

@Addison-Stavlo Addison-Stavlo Feb 24, 2020

Choose a reason for hiding this comment

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

Just curious why these were being omit-ed from everything in this reducer in the first place? Now that we are no longer omitting them in the cache, could this cause any potential issues?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it was just to keep it simpler.

Do you see an issue with this @aduth, @youknowriad?

Copy link
Member

Choose a reason for hiding this comment

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

This has changed quite a bit since last I recall being familiar with it, but my concerns would be one of (a) are we risking to leave orphaned child blocks or (b) are we not cleaning up blocks which are no longer part of state.

Reading the documentation for the function, it seems like ideally we would want to avoid a merge altogether, and that the only reason we have it is to account for reusable blocks. I'm not familiar if we're to the point with recent changes like #14367 to be able to remove this and let RESET_BLOCKS entirely take over the state (no merge). But if not, then I think we probably still want the omit ? Since otherwise, we may leave some state for blocks which are no longer relevant for the next state, right? When is the cache ever cleaned up?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not familiar if we're to the point with recent changes like #14367 to be able to remove this and let RESET_BLOCKS entirely take over the state (no merge).

That would be ideal, who would be a good person to ask?

@epiqueras
Copy link
Contributor Author

@Addison-Stavlo I believe that behavior is expected because, from the perspective of the selector that queries for the undo stack, those changes are not "persistent". They are only "persistent" in the context of a specific set of inner blocks.

How we deal with this depends on how we want to deal with multiple undo contexts. Do template parts get their own undo stacks? Does everything share a global undo stack?

I think a global undo stack makes sense, but I wonder if design has given this more thought.

cc @jasmussen @mtias @mapk

@github-actions
Copy link

github-actions bot commented Feb 24, 2020

Size Change: +317 B (0%)

Total Size: 864 kB

Filename Size Change
build/a11y/index.js 1 kB -1 B
build/block-editor/index.js 104 kB +84 B (0%)
build/block-editor/style-rtl.css 10.3 kB +528 B (5%) 🔍
build/block-editor/style.css 10.3 kB +531 B (5%) 🔍
build/block-library/editor-rtl.css 7.67 kB -8 B (0%)
build/block-library/editor.css 7.67 kB -3 B (0%)
build/block-library/index.js 116 kB +1.22 kB (1%)
build/block-serialization-default-parser/index.js 1.65 kB +1 B
build/blocks/index.js 57.7 kB +37 B (0%)
build/components/index.js 190 kB +16 B (0%)
build/components/style-rtl.css 15.5 kB -529 B (3%)
build/components/style.css 15.5 kB -535 B (3%)
build/compose/index.js 5.76 kB +1 B
build/data-controls/index.js 1.03 kB -1 B
build/data/index.js 8.22 kB -4 B (0%)
build/date/index.js 5.37 kB +4 B (0%)
build/deprecated/index.js 772 B +1 B
build/dom-ready/index.js 568 B -1 B
build/dom/index.js 3.06 kB -1 B
build/edit-post/index.js 90.9 kB +234 B (0%)
build/edit-post/style-rtl.css 8.59 kB -116 B (1%)
build/edit-post/style.css 8.58 kB -107 B (1%)
build/edit-site/index.js 4.61 kB +34 B (0%)
build/edit-widgets/index.js 4.43 kB +69 B (1%)
build/edit-widgets/style-rtl.css 2.59 kB -208 B (8%)
build/edit-widgets/style.css 2.58 kB -207 B (8%)
build/editor/editor-styles-rtl.css 325 B -2 B (0%)
build/editor/editor-styles.css 327 B -1 B
build/editor/index.js 44.6 kB -518 B (1%)
build/editor/style-rtl.css 4.01 kB -115 B (2%)
build/editor/style.css 4 kB -111 B (2%)
build/element/index.js 4.45 kB -2 B (0%)
build/format-library/index.js 7.6 kB +3 B (0%)
build/format-library/style-rtl.css 502 B +2 B (0%)
build/format-library/style.css 502 B +1 B
build/html-entities/index.js 622 B +1 B
build/i18n/index.js 3.45 kB -1 B
build/is-shallow-equal/index.js 710 B -1 B
build/keyboard-shortcuts/index.js 2.3 kB +4 B (0%)
build/list-reusable-blocks/style-rtl.css 226 B +11 B (4%)
build/list-reusable-blocks/style.css 226 B +10 B (4%)
build/media-utils/index.js 4.85 kB -1 B
build/nux/index.js 3.02 kB +1 B
build/plugins/index.js 2.54 kB +1 B
build/priority-queue/index.js 879 B +1 B
build/redux-routine/index.js 2.84 kB +2 B (0%)
build/rich-text/index.js 14.3 kB -3 B (0%)
build/server-side-render/index.js 2.54 kB -2 B (0%)
build/token-list/index.js 1.27 kB -1 B
build/url/index.js 4 kB -1 B
ℹ️ View Unchanged
Filename Size Change
build/annotations/index.js 3.43 kB 0 B
build/api-fetch/index.js 3.39 kB 0 B
build/autop/index.js 2.58 kB 0 B
build/blob/index.js 620 B 0 B
build/block-directory/index.js 6.02 kB 0 B
build/block-directory/style-rtl.css 760 B 0 B
build/block-directory/style.css 760 B 0 B
build/block-library/style-rtl.css 7.49 kB 0 B
build/block-library/style.css 7.49 kB 0 B
build/block-library/theme-rtl.css 669 B 0 B
build/block-library/theme.css 671 B 0 B
build/block-serialization-spec-parser/index.js 3.1 kB 0 B
build/core-data/index.js 10.5 kB 0 B
build/edit-site/style-rtl.css 2.77 kB 0 B
build/edit-site/style.css 2.76 kB 0 B
build/escape-html/index.js 733 B 0 B
build/hooks/index.js 1.92 kB 0 B
build/keycodes/index.js 1.68 kB 0 B
build/list-reusable-blocks/index.js 2.99 kB 0 B
build/notices/index.js 1.57 kB 0 B
build/nux/style-rtl.css 616 B 0 B
build/nux/style.css 613 B 0 B
build/primitives/index.js 1.49 kB 0 B
build/shortcode/index.js 1.7 kB 0 B
build/viewport/index.js 1.61 kB 0 B
build/warning/index.js 1.14 kB 0 B
build/wordcount/index.js 1.18 kB 0 B

compressed-size-action

Copy link
Contributor

@Addison-Stavlo Addison-Stavlo left a comment

Choose a reason for hiding this comment

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

Regarding - Bug 2: Changing things inside the template part also makes the template dirty.

It seems this is still the case: While changing content within blocks in the template part no longer dirties the template, adding/removing blocks in the template part still does.

Regarding - template part text content only undo-able for changes made after block addition/removal

How we deal with this depends on how we want to deal with multiple undo contexts. Do template parts get their own undo stacks? Does everything share a global undo stack?

Either way, I would expect one of the following at this point:

  • All Template Part content to be undo-able regardless of adding/removing a block.
  • No Template Part content to be undo-able.

Considering the following:

  • This fixes the two bugs it aims to (minus the block insertion/removal issue).
  • The only remaining bugs after this appear limited to the Site Editor experiment.
  • We may need to answer some design questions and possibly work on some larger-scope changes to fix those remaining.

The remaining issues can be addressed in follow ups if that is the route we want to go and I am comfortable approving these changes from my end.

@epiqueras
Copy link
Contributor Author

Since this involves public API changes, I think we need to answer all questions before moving forward with it.

@epiqueras
Copy link
Contributor Author

While changing content within blocks in the template part no longer dirties the template, adding/removing blocks in the template part still does.

This PR only handles attributes; blocks get replaced through a different code path.

@epiqueras
Copy link
Contributor Author

I just pushed code that handles more types of block editing like replacements, and that resolves the nearest controlled inner blocks parent in a loop so that this works for many layers of nested blocks.

@jasmussen
Copy link
Contributor

I think a global undo stack makes sense, but I wonder if design has given this more thought.

That's an excellent question to discuss, and with it comes a great deal of complexity.

In principle and ideally, you should be able to undo everything you do. There's some nuance there — we don't undo typing a single letter, perhaps not even a single word, but n characters, sure. Inserting an image? Sure. Changing it? Sure. Changing a property? Sure.

As best I can tell, this PR is about each template part having its own undo history, where changes made in a template part are undone separately from changes made to the document itself. Is that correct-ish?

I suspect we can probably live with this in the near future, but it does feel like undo should ideally be a global action that applies to every single change you made, regardless of where or how, even if it means marking a document/site dirty again if it means undoing a change inside a template part. So this seems to be something to strive for.

@epiqueras
Copy link
Contributor Author

The undo stack is global right now, this PR fixes these bugs.

@noahtallen
Copy link
Member

more work towards this in #21368

@noahtallen
Copy link
Member

closing in favor of #21368, feel free to reopen if you feel differently ;)

@noahtallen noahtallen closed this Apr 24, 2020
@aristath aristath deleted the add/local-change-detection branch November 10, 2020 14:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants