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

Fix multi-entity multi-property undo redo #50911

Merged
merged 9 commits into from
May 29, 2023
Merged

Fix multi-entity multi-property undo redo #50911

merged 9 commits into from
May 29, 2023

Conversation

youknowriad
Copy link
Contributor

@youknowriad youknowriad commented May 24, 2023

closes #12075

It's been sometime I didn't dig into the undo/redo reducer and while taking a deep look into it, I noticed a fundamental issue in how the stack is recorded and applied when undoing/redoing.

How Undo works ?

To understand the issue and the need for this PR to solve it, we first need to understand how the undo/redo works in Gutenberg.

In trunk, for any change done in the editor, a "set of edits" is added to a list of undos. Each edit, contain the following information: The identifier of the modified entity/record + the set of edits to apply when going (either back or forth) to that state. So the list resembles something like this:

  • entity1, { property1: value1, property2: value2 }
  • entityX { propertyX: valueX, propertyY: valueY }
  • ...

We also keep track of a "pointer" to a particular "step" in this list. By default the pointer is always pointing at the last step (current step) and when calling "undo" it goes back by 1 step (and opposite for redo).

Each time this step is modified (undo or redo called), we retrieve the set of "edits" corresponding to that step and apply it.

How do these steps gets recorded

This is a bit complex (as there are edge cases) but for the regular case, the behavior is the following: Let's stay initially the "undo list" contained the following content:

  • entity1, { property1: value1 }

The user in the UI edits property1 with value12, the list becomes:

  • entity1, { property1: value1 }
  • entity1, { property1: value12 }

The user in the UI edits property2 with value22 (let's also assume that the previous value of value2 was value21 but this was not in the undo list, it's just the saved value), the list becomes

  • entity1, { property1: value1 }
  • entity1, { property2: value21 }
  • entity1, { property2: value22 }

Notice that for the second item (penultimate item) of the list, we actually updated the edits to store the previous value of property2. The reason we do this is because when "undoing" to that step, we need to restore the previous value, so we need to "edit" value2 to set the previous value.

And here's the bug, now let's say with used undo twice, we went to the initial step (property 1 is value1), and now we "redo" to move to the pointer to step2. What will happen now is that the edits of step2 will be applied, so property2 set to value21 but if you remember properly, in step2, property1 need to be set to value12 and it didn't happen. (This is the bug recorded in #12075)

So what this tells us is that for each "step" in the undo list, we need to record both:

  • The edits to apply when we go up the list (redo)
  • The edits to apply when we go down the list (undo)

Multi-entity bug

Worth nothing that in trunk, the undo/redo suffer also from the "multi-entity" diff problem. Each diff can contain multiple properties (especially when using transient edits) but in theory "multi-entity" diffs are possible. So each "diff" should be a list of diffs instead.

Solution

One solution to this is to update the "diff" format to use a (from, to) tupples instead, so for the example above, we should have the following undo stack:

  • [ { entity1, property1, from: value1, to: value12 } ]
  • [ { entity2, property2, from: value21, to: value22 } ]

And when undoing, we use the pointer to retrieve the current diff and edit the entities using the "from" values. (Restore the from values), while for redos, we'd use the "to" values.

Now, this solves navigating up and down the list and we'll get the expected result (and works both for multi property and multi entities).

This is the main change in this PR

  • It changes the format of the undo list in the reducer
  • It allows "multiple edits from multiple entities" to be applied at the same time (using UNDO, REDO actions for now)
  • It also contains secondary changes to simplify the reducer: more specifically "flattenedUndo" is replaced with "cache" which represents a list of multi entity edits that have been performed but no "undo" step has been created for them, they are just waiting for the "next" undo step to be created. This is needed to support "transient entity changes" and "stacked undo steps in RichText".
  • Unfortunately, since the format of the list change and the format is currently exposed in getUndoEdit/getRedoEdit, this PR contains a breaking change right now. Ideally these selectors should have been private, I'll find a way to solve this by using new private selectors and trying to return something similar in format for these two selectors and probably deprecate them.

There are some extra simplifications that are possible to our undo/redo APIs but are not included in this PR.

Todo

  • Ensure all existing unit/e2e tests pass.
  • Add a unit test and/or e2e test for the specific issue being fixed.
  • Backward compatibility for getUndoEdit / getRedoEdit

Testing instructions

  • Create a new post
  • Type a character in the post title
  • Remove that character from the post title
  • Type something in the post content
  • Undo until the post title is filled back with the initial character
  • Redo ==> the character should get removed again.

@github-actions
Copy link

github-actions bot commented May 24, 2023

Size Change: -17.7 kB (-1%)

Total Size: 1.39 MB

Filename Size Change
build/a11y/index.min.js 955 B -27 B (-3%)
build/annotations/index.min.js 2.69 kB -72 B (-3%)
build/api-fetch/index.min.js 2.28 kB -49 B (-2%)
build/autop/index.min.js 2.1 kB -38 B (-2%)
build/blob/index.min.js 451 B -21 B (-4%)
build/block-directory/index.min.js 7.05 kB -128 B (-2%)
build/block-editor/index.min.js 194 kB -5.09 kB (-3%)
build/block-editor/style-rtl.css 14.9 kB -206 B (-1%)
build/block-editor/style.css 14.9 kB -206 B (-1%)
build/block-library/blocks/navigation/view-modal.min.js 2.78 kB -27 B (-1%)
build/block-library/blocks/navigation/view.min.js 438 B -5 B (-1%)
build/block-library/index.min.js 200 kB -4.26 kB (-2%)
build/block-library/interactivity/runtime.min.js 2.69 kB +4 B (0%)
build/block-serialization-spec-parser/index.min.js 2.87 kB +39 B (+1%)
build/blocks/index.min.js 50.3 kB -663 B (-1%)
build/commands/index.min.js 14.9 kB -59 B (0%)
build/components/index.min.js 230 kB -1.59 kB (-1%)
build/compose/index.min.js 12.1 kB -351 B (-3%)
build/core-commands/index.min.js 1.74 kB -66 B (-4%)
build/core-data/index.min.js 15.7 kB -879 B (-5%)
build/customize-widgets/index.min.js 12 kB -168 B (-1%)
build/data-controls/index.min.js 640 B -68 B (-10%) 👏
build/data/index.min.js 8.24 kB -441 B (-5%)
build/date/index.min.js 40.4 kB -32 B (0%)
build/deprecated/index.min.js 451 B -56 B (-11%) 👏
build/dom/index.min.js 4.63 kB -99 B (-2%)
build/edit-post/index.min.js 34.6 kB -634 B (-2%)
build/edit-site/index.min.js 64.6 kB +623 B (+1%)
build/edit-site/style-rtl.css 10.9 kB +377 B (+4%)
build/edit-site/style.css 10.9 kB +372 B (+4%)
build/edit-widgets/index.min.js 16.8 kB -444 B (-3%)
build/editor/index.min.js 44.6 kB -1.13 kB (-2%)
build/element/index.min.js 4.8 kB -89 B (-2%)
build/format-library/index.min.js 7.57 kB -202 B (-3%)
build/hooks/index.min.js 1.55 kB -90 B (-6%)
build/i18n/index.min.js 3.58 kB -149 B (-4%)
build/keyboard-shortcuts/index.min.js 1.71 kB -77 B (-4%)
build/keycodes/index.min.js 1.84 kB -68 B (-4%)
build/list-reusable-blocks/index.min.js 2.13 kB -12 B (-1%)
build/media-utils/index.min.js 2.9 kB -69 B (-2%)
build/notices/index.min.js 875 B -88 B (-9%)
build/plugins/index.min.js 1.84 kB -10 B (-1%)
build/preferences-persistence/index.min.js 1.84 kB -381 B (-17%) 👏
build/preferences/index.min.js 1.24 kB -90 B (-7%)
build/primitives/index.min.js 941 B -3 B (0%)
build/redux-routine/index.min.js 2.7 kB -39 B (-1%)
build/reusable-blocks/index.min.js 2.21 kB -44 B (-2%)
build/rich-text/index.min.js 10.7 kB -347 B (-3%)
build/router/index.min.js 1.77 kB -6 B (0%)
build/server-side-render/index.min.js 2.02 kB -50 B (-2%)
build/shortcode/index.min.js 1.39 kB -28 B (-2%)
build/style-engine/index.min.js 1.42 kB -105 B (-7%)
build/token-list/index.min.js 582 B -62 B (-10%) 👏
build/url/index.min.js 3.57 kB -80 B (-2%)
build/vendors/react-dom.min.js 41.8 kB -13 B (0%)
build/viewport/index.min.js 1.04 kB -42 B (-4%)
build/widgets/index.min.js 7.16 kB -123 B (-2%)
build/wordcount/index.min.js 1.02 kB -36 B (-3%)
ℹ️ View Unchanged
Filename Size
build/block-directory/style-rtl.css 1.02 kB
build/block-directory/style.css 1.02 kB
build/block-editor/content-rtl.css 4.23 kB
build/block-editor/content.css 4.23 kB
build/block-editor/default-editor-styles-rtl.css 381 B
build/block-editor/default-editor-styles.css 381 B
build/block-library/blocks/archives/editor-rtl.css 61 B
build/block-library/blocks/archives/editor.css 60 B
build/block-library/blocks/archives/style-rtl.css 90 B
build/block-library/blocks/archives/style.css 90 B
build/block-library/blocks/audio/editor-rtl.css 150 B
build/block-library/blocks/audio/editor.css 150 B
build/block-library/blocks/audio/style-rtl.css 122 B
build/block-library/blocks/audio/style.css 122 B
build/block-library/blocks/audio/theme-rtl.css 126 B
build/block-library/blocks/audio/theme.css 126 B
build/block-library/blocks/avatar/editor-rtl.css 116 B
build/block-library/blocks/avatar/editor.css 116 B
build/block-library/blocks/avatar/style-rtl.css 91 B
build/block-library/blocks/avatar/style.css 91 B
build/block-library/blocks/block/editor-rtl.css 305 B
build/block-library/blocks/block/editor.css 305 B
build/block-library/blocks/button/editor-rtl.css 584 B
build/block-library/blocks/button/editor.css 582 B
build/block-library/blocks/button/style-rtl.css 624 B
build/block-library/blocks/button/style.css 623 B
build/block-library/blocks/buttons/editor-rtl.css 337 B
build/block-library/blocks/buttons/editor.css 337 B
build/block-library/blocks/buttons/style-rtl.css 332 B
build/block-library/blocks/buttons/style.css 332 B
build/block-library/blocks/calendar/style-rtl.css 239 B
build/block-library/blocks/calendar/style.css 239 B
build/block-library/blocks/categories/editor-rtl.css 113 B
build/block-library/blocks/categories/editor.css 112 B
build/block-library/blocks/categories/style-rtl.css 124 B
build/block-library/blocks/categories/style.css 124 B
build/block-library/blocks/code/editor-rtl.css 53 B
build/block-library/blocks/code/editor.css 53 B
build/block-library/blocks/code/style-rtl.css 121 B
build/block-library/blocks/code/style.css 121 B
build/block-library/blocks/code/theme-rtl.css 124 B
build/block-library/blocks/code/theme.css 124 B
build/block-library/blocks/columns/editor-rtl.css 108 B
build/block-library/blocks/columns/editor.css 108 B
build/block-library/blocks/columns/style-rtl.css 409 B
build/block-library/blocks/columns/style.css 409 B
build/block-library/blocks/comment-author-avatar/editor-rtl.css 125 B
build/block-library/blocks/comment-author-avatar/editor.css 125 B
build/block-library/blocks/comment-content/style-rtl.css 92 B
build/block-library/blocks/comment-content/style.css 92 B
build/block-library/blocks/comment-template/style-rtl.css 199 B
build/block-library/blocks/comment-template/style.css 198 B
build/block-library/blocks/comments-pagination-numbers/editor-rtl.css 123 B
build/block-library/blocks/comments-pagination-numbers/editor.css 121 B
build/block-library/blocks/comments-pagination/editor-rtl.css 222 B
build/block-library/blocks/comments-pagination/editor.css 209 B
build/block-library/blocks/comments-pagination/style-rtl.css 235 B
build/block-library/blocks/comments-pagination/style.css 231 B
build/block-library/blocks/comments-title/editor-rtl.css 75 B
build/block-library/blocks/comments-title/editor.css 75 B
build/block-library/blocks/comments/editor-rtl.css 840 B
build/block-library/blocks/comments/editor.css 839 B
build/block-library/blocks/comments/style-rtl.css 637 B
build/block-library/blocks/comments/style.css 636 B
build/block-library/blocks/cover/editor-rtl.css 647 B
build/block-library/blocks/cover/editor.css 650 B
build/block-library/blocks/cover/style-rtl.css 1.61 kB
build/block-library/blocks/cover/style.css 1.6 kB
build/block-library/blocks/details/editor-rtl.css 65 B
build/block-library/blocks/details/editor.css 65 B
build/block-library/blocks/details/style-rtl.css 159 B
build/block-library/blocks/details/style.css 159 B
build/block-library/blocks/embed/editor-rtl.css 293 B
build/block-library/blocks/embed/editor.css 293 B
build/block-library/blocks/embed/style-rtl.css 410 B
build/block-library/blocks/embed/style.css 410 B
build/block-library/blocks/embed/theme-rtl.css 126 B
build/block-library/blocks/embed/theme.css 126 B
build/block-library/blocks/file/editor-rtl.css 316 B
build/block-library/blocks/file/editor.css 316 B
build/block-library/blocks/file/interactivity.min.js 395 B
build/block-library/blocks/file/style-rtl.css 269 B
build/block-library/blocks/file/style.css 270 B
build/block-library/blocks/file/view.min.js 375 B
build/block-library/blocks/freeform/editor-rtl.css 2.58 kB
build/block-library/blocks/freeform/editor.css 2.58 kB
build/block-library/blocks/gallery/editor-rtl.css 947 B
build/block-library/blocks/gallery/editor.css 952 B
build/block-library/blocks/gallery/style-rtl.css 1.53 kB
build/block-library/blocks/gallery/style.css 1.53 kB
build/block-library/blocks/gallery/theme-rtl.css 108 B
build/block-library/blocks/gallery/theme.css 108 B
build/block-library/blocks/group/editor-rtl.css 654 B
build/block-library/blocks/group/editor.css 654 B
build/block-library/blocks/group/style-rtl.css 57 B
build/block-library/blocks/group/style.css 57 B
build/block-library/blocks/group/theme-rtl.css 78 B
build/block-library/blocks/group/theme.css 78 B
build/block-library/blocks/heading/style-rtl.css 76 B
build/block-library/blocks/heading/style.css 76 B
build/block-library/blocks/html/editor-rtl.css 336 B
build/block-library/blocks/html/editor.css 337 B
build/block-library/blocks/image/editor-rtl.css 834 B
build/block-library/blocks/image/editor.css 833 B
build/block-library/blocks/image/interactivity.min.js 783 B
build/block-library/blocks/image/style-rtl.css 1.07 kB
build/block-library/blocks/image/style.css 1.07 kB
build/block-library/blocks/image/theme-rtl.css 126 B
build/block-library/blocks/image/theme.css 126 B
build/block-library/blocks/latest-comments/style-rtl.css 357 B
build/block-library/blocks/latest-comments/style.css 357 B
build/block-library/blocks/latest-posts/editor-rtl.css 213 B
build/block-library/blocks/latest-posts/editor.css 212 B
build/block-library/blocks/latest-posts/style-rtl.css 478 B
build/block-library/blocks/latest-posts/style.css 478 B
build/block-library/blocks/list/style-rtl.css 88 B
build/block-library/blocks/list/style.css 88 B
build/block-library/blocks/media-text/editor-rtl.css 266 B
build/block-library/blocks/media-text/editor.css 263 B
build/block-library/blocks/media-text/style-rtl.css 507 B
build/block-library/blocks/media-text/style.css 505 B
build/block-library/blocks/more/editor-rtl.css 431 B
build/block-library/blocks/more/editor.css 431 B
build/block-library/blocks/navigation-link/editor-rtl.css 712 B
build/block-library/blocks/navigation-link/editor.css 711 B
build/block-library/blocks/navigation-link/style-rtl.css 115 B
build/block-library/blocks/navigation-link/style.css 115 B
build/block-library/blocks/navigation-submenu/editor-rtl.css 296 B
build/block-library/blocks/navigation-submenu/editor.css 295 B
build/block-library/blocks/navigation/editor-rtl.css 2.33 kB
build/block-library/blocks/navigation/editor.css 2.33 kB
build/block-library/blocks/navigation/interactivity.min.js 896 B
build/block-library/blocks/navigation/style-rtl.css 2.21 kB
build/block-library/blocks/navigation/style.css 2.2 kB
build/block-library/blocks/nextpage/editor-rtl.css 395 B
build/block-library/blocks/nextpage/editor.css 395 B
build/block-library/blocks/page-list/editor-rtl.css 401 B
build/block-library/blocks/page-list/editor.css 401 B
build/block-library/blocks/page-list/style-rtl.css 175 B
build/block-library/blocks/page-list/style.css 175 B
build/block-library/blocks/paragraph/editor-rtl.css 174 B
build/block-library/blocks/paragraph/editor.css 174 B
build/block-library/blocks/paragraph/style-rtl.css 279 B
build/block-library/blocks/paragraph/style.css 281 B
build/block-library/blocks/post-author/style-rtl.css 175 B
build/block-library/blocks/post-author/style.css 176 B
build/block-library/blocks/post-comments-form/editor-rtl.css 96 B
build/block-library/blocks/post-comments-form/editor.css 96 B
build/block-library/blocks/post-comments-form/style-rtl.css 508 B
build/block-library/blocks/post-comments-form/style.css 508 B
build/block-library/blocks/post-date/style-rtl.css 61 B
build/block-library/blocks/post-date/style.css 61 B
build/block-library/blocks/post-excerpt/editor-rtl.css 71 B
build/block-library/blocks/post-excerpt/editor.css 71 B
build/block-library/blocks/post-excerpt/style-rtl.css 141 B
build/block-library/blocks/post-excerpt/style.css 141 B
build/block-library/blocks/post-featured-image/editor-rtl.css 588 B
build/block-library/blocks/post-featured-image/editor.css 586 B
build/block-library/blocks/post-featured-image/style-rtl.css 319 B
build/block-library/blocks/post-featured-image/style.css 319 B
build/block-library/blocks/post-navigation-link/style-rtl.css 153 B
build/block-library/blocks/post-navigation-link/style.css 153 B
build/block-library/blocks/post-template/editor-rtl.css 99 B
build/block-library/blocks/post-template/editor.css 98 B
build/block-library/blocks/post-template/style-rtl.css 281 B
build/block-library/blocks/post-template/style.css 281 B
build/block-library/blocks/post-terms/style-rtl.css 96 B
build/block-library/blocks/post-terms/style.css 96 B
build/block-library/blocks/post-time-to-read/style-rtl.css 69 B
build/block-library/blocks/post-time-to-read/style.css 69 B
build/block-library/blocks/post-title/style-rtl.css 100 B
build/block-library/blocks/post-title/style.css 100 B
build/block-library/blocks/preformatted/style-rtl.css 103 B
build/block-library/blocks/preformatted/style.css 103 B
build/block-library/blocks/pullquote/editor-rtl.css 135 B
build/block-library/blocks/pullquote/editor.css 135 B
build/block-library/blocks/pullquote/style-rtl.css 335 B
build/block-library/blocks/pullquote/style.css 335 B
build/block-library/blocks/pullquote/theme-rtl.css 167 B
build/block-library/blocks/pullquote/theme.css 167 B
build/block-library/blocks/query-pagination-numbers/editor-rtl.css 122 B
build/block-library/blocks/query-pagination-numbers/editor.css 121 B
build/block-library/blocks/query-pagination/editor-rtl.css 221 B
build/block-library/blocks/query-pagination/editor.css 211 B
build/block-library/blocks/query-pagination/style-rtl.css 288 B
build/block-library/blocks/query-pagination/style.css 284 B
build/block-library/blocks/query-title/style-rtl.css 63 B
build/block-library/blocks/query-title/style.css 63 B
build/block-library/blocks/query/editor-rtl.css 450 B
build/block-library/blocks/query/editor.css 449 B
build/block-library/blocks/quote/style-rtl.css 222 B
build/block-library/blocks/quote/style.css 222 B
build/block-library/blocks/quote/theme-rtl.css 223 B
build/block-library/blocks/quote/theme.css 226 B
build/block-library/blocks/read-more/style-rtl.css 132 B
build/block-library/blocks/read-more/style.css 132 B
build/block-library/blocks/rss/editor-rtl.css 149 B
build/block-library/blocks/rss/editor.css 149 B
build/block-library/blocks/rss/style-rtl.css 289 B
build/block-library/blocks/rss/style.css 288 B
build/block-library/blocks/search/editor-rtl.css 178 B
build/block-library/blocks/search/editor.css 178 B
build/block-library/blocks/search/style-rtl.css 434 B
build/block-library/blocks/search/style.css 432 B
build/block-library/blocks/search/theme-rtl.css 114 B
build/block-library/blocks/search/theme.css 114 B
build/block-library/blocks/separator/editor-rtl.css 146 B
build/block-library/blocks/separator/editor.css 146 B
build/block-library/blocks/separator/style-rtl.css 234 B
build/block-library/blocks/separator/style.css 234 B
build/block-library/blocks/separator/theme-rtl.css 194 B
build/block-library/blocks/separator/theme.css 194 B
build/block-library/blocks/shortcode/editor-rtl.css 323 B
build/block-library/blocks/shortcode/editor.css 323 B
build/block-library/blocks/site-logo/editor-rtl.css 754 B
build/block-library/blocks/site-logo/editor.css 754 B
build/block-library/blocks/site-logo/style-rtl.css 203 B
build/block-library/blocks/site-logo/style.css 203 B
build/block-library/blocks/site-tagline/editor-rtl.css 86 B
build/block-library/blocks/site-tagline/editor.css 86 B
build/block-library/blocks/site-title/editor-rtl.css 116 B
build/block-library/blocks/site-title/editor.css 116 B
build/block-library/blocks/site-title/style-rtl.css 57 B
build/block-library/blocks/site-title/style.css 57 B
build/block-library/blocks/social-link/editor-rtl.css 184 B
build/block-library/blocks/social-link/editor.css 184 B
build/block-library/blocks/social-links/editor-rtl.css 674 B
build/block-library/blocks/social-links/editor.css 673 B
build/block-library/blocks/social-links/style-rtl.css 1.4 kB
build/block-library/blocks/social-links/style.css 1.39 kB
build/block-library/blocks/spacer/editor-rtl.css 348 B
build/block-library/blocks/spacer/editor.css 348 B
build/block-library/blocks/spacer/style-rtl.css 48 B
build/block-library/blocks/spacer/style.css 48 B
build/block-library/blocks/table/editor-rtl.css 433 B
build/block-library/blocks/table/editor.css 433 B
build/block-library/blocks/table/style-rtl.css 645 B
build/block-library/blocks/table/style.css 644 B
build/block-library/blocks/table/theme-rtl.css 146 B
build/block-library/blocks/table/theme.css 146 B
build/block-library/blocks/tag-cloud/style-rtl.css 251 B
build/block-library/blocks/tag-cloud/style.css 253 B
build/block-library/blocks/template-part/editor-rtl.css 403 B
build/block-library/blocks/template-part/editor.css 403 B
build/block-library/blocks/template-part/theme-rtl.css 101 B
build/block-library/blocks/template-part/theme.css 101 B
build/block-library/blocks/text-columns/editor-rtl.css 95 B
build/block-library/blocks/text-columns/editor.css 95 B
build/block-library/blocks/text-columns/style-rtl.css 166 B
build/block-library/blocks/text-columns/style.css 166 B
build/block-library/blocks/verse/style-rtl.css 99 B
build/block-library/blocks/verse/style.css 99 B
build/block-library/blocks/video/editor-rtl.css 552 B
build/block-library/blocks/video/editor.css 555 B
build/block-library/blocks/video/style-rtl.css 174 B
build/block-library/blocks/video/style.css 174 B
build/block-library/blocks/video/theme-rtl.css 126 B
build/block-library/blocks/video/theme.css 126 B
build/block-library/classic-rtl.css 179 B
build/block-library/classic.css 179 B
build/block-library/common-rtl.css 1.1 kB
build/block-library/common.css 1.1 kB
build/block-library/editor-elements-rtl.css 75 B
build/block-library/editor-elements.css 75 B
build/block-library/editor-rtl.css 12.1 kB
build/block-library/editor.css 12.1 kB
build/block-library/elements-rtl.css 54 B
build/block-library/elements.css 54 B
build/block-library/interactivity/vendors.min.js 8.2 kB
build/block-library/reset-rtl.css 478 B
build/block-library/reset.css 478 B
build/block-library/style-rtl.css 13.1 kB
build/block-library/style.css 13.1 kB
build/block-library/theme-rtl.css 686 B
build/block-library/theme.css 691 B
build/block-serialization-default-parser/index.min.js 1.12 kB
build/commands/style-rtl.css 827 B
build/commands/style.css 827 B
build/components/style-rtl.css 11.7 kB
build/components/style.css 11.7 kB
build/customize-widgets/style-rtl.css 1.38 kB
build/customize-widgets/style.css 1.38 kB
build/dom-ready/index.min.js 324 B
build/edit-post/classic-rtl.css 544 B
build/edit-post/classic.css 545 B
build/edit-post/style-rtl.css 7.76 kB
build/edit-post/style.css 7.75 kB
build/edit-widgets/style-rtl.css 4.53 kB
build/edit-widgets/style.css 4.53 kB
build/editor/style-rtl.css 3.54 kB
build/editor/style.css 3.53 kB
build/escape-html/index.min.js 537 B
build/format-library/style-rtl.css 554 B
build/format-library/style.css 553 B
build/html-entities/index.min.js 448 B
build/is-shallow-equal/index.min.js 527 B
build/list-reusable-blocks/style-rtl.css 836 B
build/list-reusable-blocks/style.css 836 B
build/priority-queue/index.min.js 1.52 kB
build/private-apis/index.min.js 939 B
build/react-i18n/index.min.js 696 B
build/react-refresh-entry/index.min.js 8.44 kB
build/react-refresh-runtime/index.min.js 7.31 kB
build/reusable-blocks/style-rtl.css 243 B
build/reusable-blocks/style.css 243 B
build/vendors/inert-polyfill.min.js 2.48 kB
build/vendors/react.min.js 4.02 kB
build/warning/index.min.js 268 B
build/widgets/style-rtl.css 1.15 kB
build/widgets/style.css 1.16 kB

compressed-size-action

@github-actions
Copy link

github-actions bot commented May 24, 2023

Flaky tests detected in 0b1bfff.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/5110632790
📝 Reported issues:

@youknowriad youknowriad self-assigned this May 25, 2023
@youknowriad youknowriad added [Type] Bug An existing feature does not function as intended [Feature] Writing Flow Block selection, navigation, splitting, merging, deletion... labels May 25, 2023
@youknowriad youknowriad marked this pull request as ready for review May 25, 2023 04:27
@youknowriad youknowriad requested a review from nerrad as a code owner May 25, 2023 04:27
expect( undoState ).toEqual( expectedUndoState );

// Check that undo levels are created with the latest action,
// even if undone.
// Check that create after undo does nothing.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the only place where I disagree with the previous expectations/behavior.

@@ -406,14 +407,14 @@ export const editEntityRecord =
export const undo =
() =>
( { select, dispatch } ) => {
const undoEdit = select.getUndoEdit();
// Todo: we shouldn't have to pass "root" here.
const undoEdit = select( ( state ) => getUndoEdits( state.root ) );
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@adamziel @jsnajdr I've learned today that:

  • you can call private selectors like that in "thunks"
  • you can't (or at least I didn't see any way to do it) call unlock( select ).somePrivateSelector() in "thunks".
  • The first approach is buggy because I had to pass state.root instead of just state (state.root should be just an internal thing, the store author shouldn't even know it exists).

Fixing that bug is going to be a breaking change. Maybe we can still "proxy" .root and deprecate it somehow though. I'm not fixing this in this unrelated PR but just wanted to let you know about this.

Copy link
Member

Choose a reason for hiding this comment

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

Thunks themselves are a private implementation detail of the store. The user of the store doesn't see how a particular action is implemented.

Therefore, the thunk should be able to see the private selectors and actions without having to unlock anything. The select object passed into a thunk should be already "unlocked", it should expose all the private selectors and let you call select.getUndoEdits() directly.

The same applies to private actions and the dispatch object.

Note that the ability to pass functions to select and dispatch, i.e., calling them as select( state => state.foo ) or dispatch( { type: 'BAR' } ), is also unique to thunks -- see how the thunkArgs properties are constructed with Object.assign here. This allows the thunk to access the state directly and dispatch actions to the reducer directly.

If select doesn't expose private selectors, we need to fix the thunkArgs construction: instead of getSelectors() we need to get the "unlocked" selectors.

The first approach is buggy because I had to pass state.root instead of just state

It seems like this was intentional because we're passing store.__unstableOriginalGetState() to select() here. If we didn't want that, we could very easily pass store.getState(). Or was this an oversight that I and @adamziel didn't catch 2 years ago?

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 select doesn't expose private selectors, we need to fix the thunkArgs construction: instead of getSelectors() we need to get the "unlocked" selectors.

Yes, I tried without unlocking and it doesn't work, these things are not available.

It seems like this was intentional because we're passing store.__unstableOriginalGetState() to select() here. If we didn't want that, we could very easily pass store.getState(). Or was this an oversight that I and @adamziel didn't catch 2 years ago?

I'd argue that it's oversight because all selectors receive the "state" and not the "original state" with metadata (which is an internal thing added by the data module).

@jsnajdr
Copy link
Member

jsnajdr commented May 25, 2023

Reading your description of the undo stack data structure, I'm wondering if we shouldn't be storing diffs? Like:

entity1: { property2: { from: value21, to: value22 } }

Then each edit creates one undo stack record, and doesn't need to amend the penultimate one. Going through the stack backward or forward would be applying the diffs in the desired direction. It's like having a classic diff:

@@ property2
- value21
+ value22

and then applying it either with patch or patch -R. To be able to apply the patch in both directions, we need both the - and + values. In one-directional diff, only one value would be needed, the other is just for integrity checking.

The currently implemented format -- and this PR is only bugfixing it, not fundamentally changing it -- basically stores snapshots at each step. Only the snapshots are not complete, but compressed: they store only the modified properties. After modifying property2 or entity2, the older snapshots become incomplete and must be completed.

@youknowriad
Copy link
Contributor Author

@jsnajdr It is possible indeed. But I think it's probably a change that is too big IMO. Worth noting that editing the penultimate edit was being done before too but in a buggy way.

@youknowriad
Copy link
Contributor Author

@jsnajdr I'll probably explore it separately to see what we get.

Copy link
Contributor

@ntsekouras ntsekouras left a comment

Choose a reason for hiding this comment

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

Thanks Riad! Took a first pass here and tests well. I'll have to take a better look though because of the high complexity.

package-lock.json Show resolved Hide resolved
packages/core-data/src/reducer.js Show resolved Hide resolved
packages/core-data/src/reducer.js Show resolved Hide resolved
*
* @return The edit.
*/
export function getUndoEdits( state: State ): Optional< any > {
Copy link
Member

Choose a reason for hiding this comment

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

We know the return value is an array so Optional< unknown[] > should be better.

pageUtils,
} ) => {
await page.getByRole( 'textbox', { name: 'Add title' } ).type( 'a' ); // First step.
await page.keyboard.press( 'Backspace' ); // Second step.
Copy link
Member

Choose a reason for hiding this comment

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

Could we also add a test for this for blocks? The title case is a bit different? Maybe we need some logic in RichText that treats Backspace changes differently from input, which normally only marks changes as persistent after 1s.

export function useMarkPersistent( { html, value } ) {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can you give me an example of test you have in mind. The issue fixed by this PR is hardly reproducible with blocks in trunk.

@@ -436,107 +456,110 @@ let lastEditAction;
* @return {UndoState} Updated state.
*/
export function undo( state = UNDO_INITIAL_STATE, action ) {
const omitPendingRedos = ( currentState ) => {
Copy link
Member

@ellatrix ellatrix May 25, 2023

Choose a reason for hiding this comment

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

Nit: why not use named functions for easier debugging?

Copy link
Contributor

Choose a reason for hiding this comment

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

Function declarations in const f = (...) => syntax are equivalent to named functions. I don't remember when this started, but it's been some years. See for yourself, even in Safari:

(() => {
    const foo = () => 42
    const bar = foo

    function logFunctionName(fn) {
        console.log(fn.name)
    }

    logFunctionName(bar);
})() // logs 'foo'

(But I agree that we should name our callbacks when possible.)

Copy link
Member

Choose a reason for hiding this comment

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

Ah cool! So all these anon functions are anon because they're not assigned to a var?

* @property {Object} [flattenedUndo] Flattened form of undo stack.
* @property {number} list The undo stack.
* @property {number} offset Where in the undo stack we are.
* @property {Object} cache Cache of unpersisted transient edits.
Copy link
Member

Choose a reason for hiding this comment

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

What exactly is this cache?

Copy link
Member

Choose a reason for hiding this comment

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

Sounds like that's just unpersisted edits, not really cache?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's a cache of "edits" that were not added to the "undo" stack yet.

}
case 'UNDO':
case 'REDO': {
const nextState = appendCachedEditsToLastUndo( state );
Copy link
Member

Choose a reason for hiding this comment

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

Why do we persist unpersisted changes on redo? Normally it should no longer be possible to redo once there are more changes made?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In theory it's not going to do anything at all because when you call "undo", the "unpersisted changes" are persisted.
So whenever you have a "redo", there's probably no cached changes. The code is here for the "undo" case. (to share the same code) but I can add a check if needed.

@youknowriad
Copy link
Contributor Author

@jsnajdr Here's the alternative exploration (on top of this branch) #51002
As you can see in the code, I don't think there's a very big difference between both but I prefer this new alternative as it's conceptually slightly better and easier to understand. It does store a bit more "bits" in the state but I think it's fine.

I'll be waiting for the tests to pass on that PR to merge into the current one.

@youknowriad
Copy link
Contributor Author

I've merged the other PR into this one and updated the PR description accordingly. We're now using the alternative "diff" format as suggested by @jsnajdr

@youknowriad
Copy link
Contributor Author

This is ready to ship. Any approvals/reviews?

packages/core-data/src/reducer.js Outdated Show resolved Hide resolved
packages/core-data/src/reducer.js Outdated Show resolved Hide resolved
Copy link
Member

@jsnajdr jsnajdr left a comment

Choose a reason for hiding this comment

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

Looks good 👍

packages/core-data/src/reducer.js Outdated Show resolved Hide resolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Feature] Writing Flow Block selection, navigation, splitting, merging, deletion... [Type] Bug An existing feature does not function as intended
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Undo / Redo unreliable with the post title
5 participants