CODAP-1118: implement plugin undo/redo#2388
Conversation
codap-v3
|
||||||||||||||||||||||||||||
| Project |
codap-v3
|
| Branch Review |
main
|
| Run status |
|
| Run duration | 02m 09s |
| Commit |
|
| Committer | github-actions[bot] |
| View all properties for this run ↗︎ | |
| Test results | |
|---|---|
|
|
0
|
|
|
0
|
|
|
0
|
|
|
0
|
|
|
4
|
| View all changes introduced in this branch ↗︎ | |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #2388 +/- ##
===========================================
- Coverage 86.35% 70.28% -16.08%
===========================================
Files 768 768
Lines 42980 43045 +65
Branches 10280 10709 +429
===========================================
- Hits 37117 30254 -6863
- Misses 5851 12766 +6915
- Partials 12 25 +13
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
scytacki
left a comment
There was a problem hiding this comment.
The part missing from this analysis is the potential issue with id or other state that probably won't be maintained properly when the plugin undoes or replays its actions.
For example if the plugin creates the graph and then the user modifies the graph. If the graph change is undone and then the plugin's creation of the graph is undone. If the plugin's change is then redone, it will very likely create a tile with a new id instead of using the previous id. Now when the graph change is applied it will fail.
Perhaps this kind of failure is OK, but it should be noted here. Since many of the actions plugins take are creating graphs and tables this seems like it will make the undo support not very useful.
a6c1e3c to
113a41c
Compare
Re-analyze V2 implementation in light of comparison with CODAP-1127 spec. Key findings: - V2's handleUndoChangeNotice is mode-agnostic (no standalone vs external branching) - V3 DI handlers already bypass undo (applyModelChange without undo strings routes through withoutUndo), matching V2's retainUndo behavior - V3 can use a single code path for both modes, same as V2 - Shadow counter approach (from CODAP-1127 spec) is not needed - Flag all-cases-handler undo strings as a bug Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
User-triggered CODAP actions (graph changes, table edits) create undo entries on the same stack as plugin entries. In standalone mode, this causes undoButtonPress to potentially undo the wrong action. Revise analysis to recommend shadow counters for standalone mode (keeping plugin undo separate from CODAP stack) while retaining CODAP undo entries for external mode (where interleaving is intentional). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The interleaving of plugin and CODAP undo entries on a shared stack is actually correct behavior — the most recent action is undone first. Shift recommendation from shadow counters to single code path (shared stack) for both standalone and external modes, with shadow counters as a fallback only if plugin testing reveals issues. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add test confirming that an MST action producing zero patches but calling withCustomUndoRedo creates a valid undoable history entry with working undo/redo callbacks. This resolves the open question about whether the history system needs extension for plugin undo/redo — it does not. Update analysis document accordingly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The delete operation in all-cases-handler was the only DI handler that passed undoStringKey/redoStringKey to applyModelChange, causing it to create undo entries for plugin-triggered mutations. All other DI handlers omit these strings so mutations go through withoutUndo(). This aligns the delete handler with the established pattern. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implement the three undoChangeNotice operations: - undoableActionPerformed: creates a custom undo entry via withCustomUndoRedo that sends undoAction/redoAction messages back to the originating plugin - undoButtonPress: calls document.undoLastAction() - redoButtonPress: calls document.redoLastAction() Uses a single shared-stack code path for both standalone and external modes. All responses include canUndo/canRedo state. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a MobX reaction in createDocumentModel that broadcasts clearUndo/ clearRedo notifications to all plugins when the undo or redo stack is emptied (canUndo/canRedo transitions from true to false). This matches V2 proactive notification behavior. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use a type guard function for plugin undo/redo patch validation, following the pattern established in data-set-undo.ts. Extract patcher into a named constant with explicit ICustomUndoRedoPatcher type. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tifications Test that undo/redo sends the correct undoAction/redoAction message to the originating plugin tile via broadcastMessage. Test that clearUndo and clearRedo notifications are broadcast to all plugins when the undo/redo stacks are emptied. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Document the shared-stack undo/redo architecture, the protocol between CODAP and plugins, why DI API mutations bypass undo, and the five requirements plugins must satisfy to participate correctly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
113a41c to
051269d
Compare
There was a problem hiding this comment.
Pull request overview
Implements Data Interactive plugin undo/redo support in V3 by recording plugin-originated undoable actions as custom history entries on CODAP’s shared undo stack, and relaying undo/redo execution back to the originating plugin.
Changes:
- Implement
undoChangeNoticenotify handler forundoableActionPerformed,undoButtonPress, andredoButtonPressusing a custom undo/redo patcher that sendsundoAction/redoActionto the plugin tile. - Add a document-level MobX reaction that proactively broadcasts
clearUndo/clearRedowhen stacks become empty. - Align DI “delete all cases” behavior by removing undo/redo strings so plugin-triggered mutations don’t create undo entries; add unit tests and architecture docs.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| v3/src/models/history/undo-store.test.ts | Adds coverage for history entries consisting only of custom patches (pure side effects). |
| v3/src/models/document/create-document-model.ts | Adds MobX reaction broadcasting clearUndo/clearRedo notifications to plugins. |
| v3/src/data-interactive/handlers/undo-change-notice-handler.ts | Implements plugin undo/redo DI protocol and registers custom undo/redo patcher. |
| v3/src/data-interactive/handlers/undo-change-notice-handler.test.ts | Adds tests for handler operations, targeted undo/redo messaging, and clear notifications. |
| v3/src/data-interactive/handlers/all-cases-handler.ts | Removes undo/redo strings to prevent plugin-triggered “delete all cases” from creating undo entries. |
| v3/doc/plugin-undo-redo.md | Adds detailed analysis/plan documenting the shared-stack approach and rationale. |
| v3/doc/plugin-undo-redo-architecture.md | Adds architecture/protocol documentation and plugin requirements for correct behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Add optional chaining on prev in undo/redo notification reaction for defensive safety. Inline unused documentContent variable in handler. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…eation Add Known Limitation #4 documenting the identity stability issue when plugin-created tiles are undone and redone: the recreated tile gets a new ID, breaking subsequent undo entries that reference the original ID. Note that this limitation also exists in V2 and has not manifested in practice. Outline two complementary future improvements: ID remapping in the undo system and optional id parameter in the DI API. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Good point. We investigated this and confirmed the issue exists in V2 as well — graph modification commands store the component ID in closures, and redo fails if the graph has been destroyed and recreated with a new ID. It hasn't manifested in practice because SageModeler doesn't make tile creation undoable (tile creation is fire-and-forget, and SageModeler's undo/redo operates on its own internal model, not CODAP tiles). We've documented this as Known Limitation #4 in the architecture doc (
|
dougmartin
left a comment
There was a problem hiding this comment.
Looks good 👍 @emcelroy and I reviewed it over Zoom.
Summary
undoChangeNoticehandler with all three operations:undoableActionPerformed,undoButtonPress,redoButtonPresswithCustomUndoRedo— no MST state changes, just side-effect callbacks that sendundoAction/redoActionmessages to the originating pluginclearUndo/clearRedonotifications broadcast to all plugins when undo/redo stacks are emptiedall-cases-handlerdelete, which was the only DI handler that incorrectly created undo entries for plugin-triggered mutationsv3/doc/plugin-undo-redo-architecture.md) covering the shared-stack design, the protocol, and the five requirements plugins must satisfyv3/doc/plugin-undo-redo.mdRefs CODAP-1118
Test plan
undoAction/redoActionmessages are sent to the correct plugin tile viabroadcastMessageclearUndo/clearRedonotifications are broadcast when stacks are emptiedGenerated with Claude Code