Our editor now has undo/redo! This means that, if you make a silly mistake, like deleting a component you didn’t intend to delete, you can now undo the operation and keep going.
Some curious features of our implementation
The undo/redo stack is serializable, which means it’ll resist page refreshes. The state is stored in
sessionStorage
.The undo/redo stack is repo wide, which means as you move through different places in the dashboard, you’ll be able to undo/redo operations you did anywhere.
So, what took us so long?
This was quite challenging to implement. Our data model is highly dynamic, as it allows different block types, relationships, constraints, etc… and different ways you can edit these: the slash command, pressing “Enter” to create a new block, editing constraints via the properties panel, the context menu, etc.
Concretely, there were three main challenges we had to figure out to make this a successful project:
Block dependencies: Blocks such as the Reference Block can target other blocks, such as Instance Blocks. Now, when an Instance Block gets deleted, it also gets removed from Reference Blocks that are targeting it—we call this a “side effect”. When the user undoes the original operation, all side effects should be undone as well. There are more side effects like this one, and we needed to handle these correctly.
Tabs, and focus states: Because you can take action against a Block from different places—for example, you could edit the title via the properties panel or via the editor—when you undo an operation, we need to take you back to where the operation was performed before undoing it, to prevent confusion.
Make it play nicely with yjs’ Undo Manager: Our Rich Text Block uses yjs to enable collaboration. Yjs has an internal Undo Manager which wasn’t easy to replace. In fact, we decided not to replace it, but rather, include it in our global undo manager in a clever way.
With these three things sorted out, it was downhill from there.