-
Notifications
You must be signed in to change notification settings - Fork 0
fix: stabilize undo/redo logic through refactoring #125
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
Merged
Merged
Changes from all commits
Commits
Show all changes
31 commits
Select commit
Hold shift + click to select a range
76fd4d2
apply undo/redo
MinCrohn feab379
fix patch
MinCrohn c50f7d3
fix
MinCrohn 581fe28
fix
MinCrohn c8b4175
add mixin utils test
MinCrohn 36b276b
add undo-redo test
MinCrohn 4f245e3
add command Background test
MinCrohn 5896d98
add command Bar test
MinCrohn 98b0afe
fix Text
MinCrohn 7227667
fix arraymerge to mergeStrategy
MinCrohn 92fe574
fix
MinCrohn aecc7e7
fix docs
MinCrohn 60a6061
add Icon undo-redo test
MinCrohn 9978de6
add Group&Grid undo/redo test
MinCrohn dd6f49b
add Relations undo/redo test
MinCrohn 2a5c339
fix relations style
MinCrohn 971ec88
fix diff func
MinCrohn 80eea39
fix update method
MinCrohn 4839b22
fix diff func
MinCrohn 5d37759
fix relations test
MinCrohn 184eafb
fix typo
MinCrohn 3f848eb
fix data.d.ts padding
MinCrohn 7cdf746
fix
MinCrohn f5afef7
fix attrs
MinCrohn 9a0e319
fix
MinCrohn af81e53
fix relations style
MinCrohn dc99edf
chore
MinCrohn ddf6257
chore
MinCrohn 74754d5
fix
MinCrohn d616fdf
chore
MinCrohn d6d8dfd
fix
MinCrohn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| import { Command } from './base'; | ||
|
|
||
| export class UpdateCommand extends Command { | ||
| constructor(element, changes, options) { | ||
| super(options.historyId); | ||
| this.element = element; | ||
| this.changes = changes; | ||
| this.options = options; | ||
| this.previousProps = this._createPreviousState(this.changes); | ||
| } | ||
|
|
||
| execute() { | ||
| this.element.update(this.changes, { | ||
| ...this.options, | ||
| historyId: false, | ||
| }); | ||
| } | ||
|
|
||
| undo() { | ||
| this.element.update(this.previousProps, { | ||
| ...this.options, | ||
| mergeStrategy: 'replace', | ||
| historyId: false, | ||
| }); | ||
| } | ||
|
|
||
| _createPreviousState(changes) { | ||
| const slice = {}; | ||
| if (!changes) { | ||
| return slice; | ||
| } | ||
| const currentProps = this.element.props; | ||
|
|
||
| for (const key of Object.keys(changes)) { | ||
| if ( | ||
| key === 'attrs' && | ||
| changes.attrs !== null && | ||
| typeof changes.attrs === 'object' | ||
| ) { | ||
| const prevAttrs = {}; | ||
| for (const attrKey of Object.keys(changes.attrs)) { | ||
| prevAttrs[attrKey] = this._deepClone(this.element[attrKey]); | ||
| } | ||
| slice.attrs = prevAttrs; | ||
| } else { | ||
| slice[key] = this._deepClone(currentProps[key]); | ||
| } | ||
| } | ||
| return slice; | ||
| } | ||
|
|
||
| _deepClone(value) { | ||
| if (value && typeof value.clone === 'function') { | ||
| return value.clone(); | ||
| } | ||
| try { | ||
| return structuredClone(value); | ||
| } catch (_) { | ||
| return value; | ||
| } | ||
MinCrohn marked this conversation as resolved.
Show resolved
Hide resolved
MinCrohn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
MinCrohn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,172 @@ | ||
| import { describe, expect, it, vi } from 'vitest'; | ||
| import { Command } from './commands/base'; | ||
| import { BundleCommand } from './commands/bundle'; | ||
| import { UndoRedoManager } from './undo-redo-manager'; | ||
|
|
||
| /** | ||
| * A mock command for testing purposes. | ||
| * It tracks how many times execute and undo have been called. | ||
| */ | ||
| class MockCommand extends Command { | ||
| constructor(id = null) { | ||
| super(id); | ||
| this.executeCount = 0; | ||
| this.undoCount = 0; | ||
| } | ||
|
|
||
| execute() { | ||
| this.executeCount++; | ||
| } | ||
|
|
||
| undo() { | ||
| this.undoCount++; | ||
| } | ||
| } | ||
|
|
||
| describe('UndoRedoManager', () => { | ||
| it('should correctly execute a command and add it to the history', () => { | ||
| const manager = new UndoRedoManager(); | ||
| const command = new MockCommand(); | ||
|
|
||
| manager.execute(command); | ||
|
|
||
| expect(command.executeCount).toBe(1); | ||
| expect(manager.commands).toHaveLength(1); | ||
| expect(manager.canUndo()).toBe(true); | ||
| expect(manager.canRedo()).toBe(false); | ||
| }); | ||
|
|
||
| it('should correctly undo and redo a command', () => { | ||
| const manager = new UndoRedoManager(); | ||
| const command = new MockCommand(); | ||
|
|
||
| manager.execute(command); | ||
|
|
||
| // Undo | ||
| manager.undo(); | ||
| expect(command.undoCount).toBe(1); | ||
| expect(manager.canUndo()).toBe(false); | ||
| expect(manager.canRedo()).toBe(true); | ||
|
|
||
| // Redo | ||
| manager.redo(); | ||
| expect(command.executeCount).toBe(2); // Initial execute + redo | ||
| expect(manager.canUndo()).toBe(true); | ||
| expect(manager.canRedo()).toBe(false); | ||
| }); | ||
|
|
||
| it('should clear the redo stack after a new command is executed', () => { | ||
| const manager = new UndoRedoManager(); | ||
| const command1 = new MockCommand(); | ||
| const command2 = new MockCommand(); | ||
|
|
||
| manager.execute(command1); | ||
| manager.undo(); | ||
|
|
||
| expect(manager.canRedo()).toBe(true); | ||
|
|
||
| manager.execute(command2); | ||
|
|
||
| expect(command2.executeCount).toBe(1); | ||
| expect(manager.commands).toHaveLength(1); | ||
| expect(manager.commands[0]).toBe(command2); | ||
| expect(manager.canRedo()).toBe(false); | ||
| }); | ||
|
|
||
| it('should respect the maxCommands limit', () => { | ||
| const manager = new UndoRedoManager(3); // Max 3 commands | ||
| const commands = [ | ||
| new MockCommand(), | ||
| new MockCommand(), | ||
| new MockCommand(), | ||
| new MockCommand(), | ||
| ]; | ||
|
|
||
| manager.execute(commands[0]); | ||
| manager.execute(commands[1]); | ||
| manager.execute(commands[2]); | ||
| manager.execute(commands[3]); | ||
|
|
||
| expect(manager.commands).toHaveLength(3); | ||
| expect(manager.commands[0]).toBe(commands[1]); // The first command should be discarded | ||
| expect(manager.canUndo()).toBe(true); | ||
| }); | ||
|
|
||
| it('should clear the entire command history', () => { | ||
| const manager = new UndoRedoManager(); | ||
| manager.execute(new MockCommand()); | ||
| manager.execute(new MockCommand()); | ||
|
|
||
| expect(manager.commands).toHaveLength(2); | ||
|
|
||
| manager.clear(); | ||
|
|
||
| expect(manager.commands).toHaveLength(0); | ||
| expect(manager.canUndo()).toBe(false); | ||
| expect(manager.canRedo()).toBe(false); | ||
| }); | ||
|
|
||
| it('should bundle commands with the same historyId', () => { | ||
| const manager = new UndoRedoManager(); | ||
| const command1 = new MockCommand('bundle-1'); | ||
| const command2 = new MockCommand('bundle-1'); | ||
| const command3 = new MockCommand('bundle-2'); | ||
|
|
||
| manager.execute(command1, { historyId: 'bundle-1' }); | ||
| manager.execute(command2, { historyId: 'bundle-1' }); | ||
| manager.execute(command3, { historyId: 'bundle-2' }); | ||
|
|
||
| expect(manager.commands).toHaveLength(2); // Two bundles | ||
| expect(manager.commands[0]).toBeInstanceOf(BundleCommand); | ||
| expect(manager.commands[0].commands).toHaveLength(2); | ||
| expect(manager.commands[0].commands[0]).toBe(command1); | ||
| expect(manager.commands[0].commands[1]).toBe(command2); | ||
| expect(manager.commands[1]).toBeInstanceOf(BundleCommand); | ||
| }); | ||
|
|
||
| it('should correctly undo/redo a bundle of commands', () => { | ||
| const manager = new UndoRedoManager(); | ||
| const command1 = new MockCommand('bundle-1'); | ||
| const command2 = new MockCommand('bundle-1'); | ||
|
|
||
| manager.execute(command1, { historyId: 'bundle-1' }); | ||
| manager.execute(command2, { historyId: 'bundle-1' }); | ||
|
|
||
| expect(command1.executeCount).toBe(1); | ||
| expect(command2.executeCount).toBe(1); | ||
|
|
||
| // Undo the whole bundle | ||
| manager.undo(); | ||
| expect(command1.undoCount).toBe(1); | ||
| expect(command2.undoCount).toBe(1); | ||
|
|
||
| // Redo the whole bundle | ||
| manager.redo(); | ||
| expect(command1.executeCount).toBe(2); | ||
| expect(command2.executeCount).toBe(2); | ||
| }); | ||
|
|
||
| it('should correctly notify subscribers on state changes', () => { | ||
| const manager = new UndoRedoManager(); | ||
| const listener = vi.fn(); | ||
| const unsubscribe = manager.subscribe(listener); | ||
|
|
||
| expect(listener).toHaveBeenCalledTimes(1); | ||
|
|
||
| manager.execute(new MockCommand()); | ||
| expect(listener).toHaveBeenCalledTimes(2); | ||
|
|
||
| manager.undo(); | ||
| expect(listener).toHaveBeenCalledTimes(3); | ||
|
|
||
| manager.redo(); | ||
| expect(listener).toHaveBeenCalledTimes(4); | ||
|
|
||
| manager.clear(); | ||
| expect(listener).toHaveBeenCalledTimes(5); | ||
|
|
||
| unsubscribe(); | ||
| manager.execute(new MockCommand()); | ||
| expect(listener).toHaveBeenCalledTimes(5); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.