-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
"Undo trap": Avoid getting stuck in an editing state #8119
Comments
I was looking into this a little. An image can be uploaded by 1) pressing Upload on an image block; 2) drag-and-dropping an image onto an image block; and 3) drag-and-dropping an image straight into the editor. Using |
@talldan discovered another bug to do with this in #8066 (comment). |
I also came across this undo trap when I pasted a text that included a URL. The URL became its own block, and when trying to undo, the browser kept reaching out to the URL and the paste could not be undone. |
Thanks for the input, @claudiobrandt. That sounds like an Embed issue (#7323), as listed in this issue's description, correct? |
Yes, @mcsf, the same behavior, except that I noticed it while copying/pasting content that included lines with nothing but an URL. These lines were converted to a block after the paste operation, and undoing the conversion was not possible. This behavior doesn't happen all the time and further testing with copy/paste operation should be performed. As you can see in the video I prepared right now, while testing it again, if I copy and paste a list of URLs from a Notepad.exe document, the URLs are not converted and they become a single text block. If I paste the same list from a word processor, where each URL is already presented as a link (I used LibreOffice for testing), the results vary. The first time a paste, the URLs are converted to embeds, and I can undo it. When pasting the same content a second time, I can't undo it anymore. 1'19 |
Thanks for the details and video, @claudiobrandt. The difference in results between Notepad and LibreOffice is expected: although sometimes perhaps surprising, Gutenberg determines whether the pasted content is "plain text" or not, and depending on that it may skip certain types of processing (because it assumes the user doesn't want added formatting). As for the rest, your report will be taken into account. Internally, pasting and manual conversion of blocks essentially work in the same way. :) |
@mcsf I'm moving out of 4.2 but if your work is ready, we can reconsider. |
I've encountered another issue related to keeping file blobs in attributes while working on #32157. Conditions
Currently affected blocks
When a block has no example, the "Styles" preview will use attributes. Then the block preview will trigger another upload based on the file blob attribute. A simple solution will be to provide examples for both blocks. Another one will be to store temporary URLs in a state like #30114. |
I went ahead and created PR for audio block #32333. File block doesn't have the "Insert from URL" option, so not sure how the example preview should work here. |
Thanks for spotting that one. |
@mcsf Any preference on how I should fix this for the File block? Add an example or use the state for temporary URLs? |
Referencing #36807 for tracking purposes. |
Thanks, keeping that trail is always useful. :) |
I had some time to dig into the problem of the gallery block and the "undo trap" issue in general. This “undo trap” issue can happen when these conditions meet:
As a general rule of thumb, an action should only be able to push to the undo stack if the action is performed by user interactions. However, there are existing patterns in the gutenberg repository (I believe in other third-party blocks too) that don’t follow this practice. The most common mistake is the misuse of the The details of when to and not to use the gutenberg/packages/block-library/src/gallery/edit.js Lines 304 to 314 in 7f7c24d
Upon first creating the Gallery block, the The solutionsThere are some options for this issue:
A common pattern in our codebase, is to use a lesser-known API __unstableMarkNextChangeAsNotPersistent();
setAttributes( { ... } );
I propose a refined API to wrap the underlying actions inside the callback so that it's easier to reason about. __unstableMarkPersistent( () => {
setAttributes( { ... } );
} ); The implementation could roughly look like: function __unstableMarkPersistent( callback ) {
__unstableMarkNextChangeAsNotPersistent();
callback();
} So that we can reuse the old API but with a nicer-looking signature. Note that the implementation still only supports one action at a time, but I think it's possible to refactor it so that it can mark all synchronous actions in the callback as persistent (while disallowing asynchronous callback with a linter plugin or runtime warning). A bonus benefit is that the
The more correct way to fix this, IMHO, is to get rid of the misused const linkTo = attributes.linkTo || window?.wp?.media?.view?.settings?.defaultProps?.link || LINK_DESTINATION_NONE; This requires a larger refactor though and could potentially introduce breaking changes if not treated carefully. But I think it's the right way and can prepare us for a more resilient codebase. How can we prevent this?Since this is a hard problem and none of the solutions so far is trivial to fix by third-party authors, I've been thinking of a general solution to this problem so that we can prevent it from happening from the beginning. There are a few options I think we can try.
As mentioned above, updating attributes should be performed by user interactions. There's little to no reason that we want to set them on change of some props or states. We would want to also provide a link to the alternatives so that they can follow.
Since pushing to the undo stack should always come from user interactions, maybe we can listen to user events to automatically mark the updating methods as persistent or not. I've tried listening to events that can be triggered by users, similar to how trusted events work. We can listen to all the possible events globally and set a variable like // Pseudo code below.
let isFromUserGesture = false;
window.addEventListener( 'click', () => {
isFromUserGesture = true;
}, true ); // Capturing phase runs first.
// Or an arbitrary timeout.
setImmediate( () => {
isFromUserGesture = false
} ); But that quickly faces a problem when deleting a block. For some reason, deleting a block is asynchronous. Probably because the action is a thunk and the implementation of thunk depends on asynchronous calling? @adamziel might know better 😅. Deleting blocks isn't the only action though, I believe there are tons of existing actions that don't follow this rule, so we'll have to go through them one by one. This issue is fairly complicated and I don't think I fully understand it even after days of studying. That's why this is more like a brain dump than a deep dive 😅. There's no easy solution either, I'd be happy to know if there are better solutions. For now, I believe the best solution is to follow the best practices of |
I appreciate the detailed account, @kevin940726. As I see it, we could:
As asked by @annezazu, I don't know what else we can do with this issue. The time may be coming when we finally close it. Thoughts? :) |
I think so :) |
Undo trap: The situation in which a user is "stuck" when continuously pressing Undo. The typical cause: as Undo is pressed and the content is reverted, some event somewhere causes a new undo level to be created. The result is that the user is unable to undo past a certain point, thereby losing access to previous editing states.
This issue aggregates all bugs stemming from that common problem:
Problem
At the root of these bugs is the fact that blocks need to manage a transition using specific ephemeral state:
[gallery ids="x,y,z"]
, a Gallery block needs to temporarily keep the IDsx, y, z
of the images while it waits for API data necessary to the block (the URL andalt
attribute for each image); once it receives the API data, it adds it to its attributes [resolved state];Since the temporary states are encoded in the attributes themselves of the blocks, they automatically constitute an undo level of the editor history. Thus, the action of undoing will bring the blocks back to that temporary state, which itself triggers a transition "back" into the resolved state.
Solutions
There are, as I see it, two approaches:
The first option seems to be worth exploring first. A quick stab at the second one — by hacking
withHistory
— doesn't inspire a lot of confidence.The text was updated successfully, but these errors were encountered: