mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-12 23:32:55 +01:00
361 lines
9.1 KiB
Markdown
361 lines
9.1 KiB
Markdown
# UndoQueue Usage Patterns
|
|
|
|
This guide documents the correct patterns for using OpenNoodl's undo system.
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
The OpenNoodl undo system consists of two main classes:
|
|
|
|
- **`UndoQueue`**: Manages the global undo/redo stack
|
|
- **`UndoActionGroup`**: Represents a single undoable action (or group of actions)
|
|
|
|
### Critical Bug Warning
|
|
|
|
There's a subtle but dangerous bug in `UndoActionGroup` that causes silent failures. This guide will show you the **correct patterns** that avoid this bug.
|
|
|
|
---
|
|
|
|
## The Golden Rule
|
|
|
|
**✅ ALWAYS USE: `UndoQueue.instance.pushAndDo(new UndoActionGroup({...}))`**
|
|
|
|
**❌ NEVER USE: `undoGroup.push({...}); undoGroup.do();`**
|
|
|
|
Why? The second pattern fails silently due to an internal pointer bug. See [LEARNINGS.md](./LEARNINGS.md#-critical-undoactiongroupdo-silent-failure-dec-2025) for full technical details.
|
|
|
|
---
|
|
|
|
## Pattern 1: Simple Single Action (Recommended)
|
|
|
|
This is the most common pattern and should be used for 95% of cases.
|
|
|
|
```typescript
|
|
import { ProjectModel } from '@noodl-models/projectmodel';
|
|
import { UndoQueue, UndoActionGroup } from '@noodl-models/undo-queue-model';
|
|
|
|
function renameComponent(component: ComponentModel, newName: string) {
|
|
const oldName = component.name;
|
|
|
|
// ✅ CORRECT - Action executes immediately and is added to undo stack
|
|
UndoQueue.instance.pushAndDo(
|
|
new UndoActionGroup({
|
|
label: `Rename ${component.localName} to ${newName}`,
|
|
do: () => {
|
|
ProjectModel.instance.renameComponent(component, newName);
|
|
},
|
|
undo: () => {
|
|
ProjectModel.instance.renameComponent(component, oldName);
|
|
}
|
|
})
|
|
);
|
|
}
|
|
```
|
|
|
|
**What happens:**
|
|
|
|
1. `UndoActionGroup` is created with action in constructor (ptr = 0)
|
|
2. `pushAndDo()` adds it to the queue
|
|
3. `pushAndDo()` calls `action.do()` which executes immediately
|
|
4. User can now undo with Cmd+Z
|
|
|
|
---
|
|
|
|
## Pattern 2: Multiple Related Actions
|
|
|
|
When you need multiple actions in a single undo group:
|
|
|
|
```typescript
|
|
function moveFolder(sourcePath: string, targetPath: string) {
|
|
const componentsToMove = ProjectModel.instance
|
|
.getComponents()
|
|
.filter((comp) => comp.name.startsWith(sourcePath + '/'));
|
|
|
|
const renames: Array<{ component: ComponentModel; oldName: string; newName: string }> = [];
|
|
|
|
componentsToMove.forEach((comp) => {
|
|
const relativePath = comp.name.substring(sourcePath.length);
|
|
const newName = targetPath + relativePath;
|
|
renames.push({ component: comp, oldName: comp.name, newName });
|
|
});
|
|
|
|
// ✅ CORRECT - Single undo group for multiple related actions
|
|
UndoQueue.instance.pushAndDo(
|
|
new UndoActionGroup({
|
|
label: `Move folder ${sourcePath} to ${targetPath}`,
|
|
do: () => {
|
|
renames.forEach(({ component, newName }) => {
|
|
ProjectModel.instance.renameComponent(component, newName);
|
|
});
|
|
},
|
|
undo: () => {
|
|
renames.forEach(({ component, oldName }) => {
|
|
ProjectModel.instance.renameComponent(component, oldName);
|
|
});
|
|
}
|
|
})
|
|
);
|
|
}
|
|
```
|
|
|
|
**What happens:**
|
|
|
|
- All renames execute as one operation
|
|
- Single undo reverts all changes
|
|
- Clean, atomic operation
|
|
|
|
---
|
|
|
|
## Pattern 3: Building Complex Undo Groups (Advanced)
|
|
|
|
Sometimes you need to build undo groups dynamically. Use `pushAndDo` on the group itself:
|
|
|
|
```typescript
|
|
function complexOperation() {
|
|
const undoGroup = new UndoActionGroup({ label: 'Complex operation' });
|
|
|
|
// Add to queue first
|
|
UndoQueue.instance.push(undoGroup);
|
|
|
|
// ✅ CORRECT - Use pushAndDo on the group, not push + do
|
|
undoGroup.pushAndDo({
|
|
do: () => {
|
|
console.log('First action executes');
|
|
// ... do first thing
|
|
},
|
|
undo: () => {
|
|
// ... undo first thing
|
|
}
|
|
});
|
|
|
|
// Another action
|
|
undoGroup.pushAndDo({
|
|
do: () => {
|
|
console.log('Second action executes');
|
|
// ... do second thing
|
|
},
|
|
undo: () => {
|
|
// ... undo second thing
|
|
}
|
|
});
|
|
}
|
|
```
|
|
|
|
**Key Point**: Use `undoGroup.pushAndDo()`, NOT `undoGroup.push()` + `undoGroup.do()`
|
|
|
|
---
|
|
|
|
## Anti-Pattern: What NOT to Do
|
|
|
|
This pattern looks correct but **fails silently**:
|
|
|
|
```typescript
|
|
// ❌ WRONG - DO NOT USE
|
|
function badRename(component: ComponentModel, newName: string) {
|
|
const oldName = component.name;
|
|
|
|
const undoGroup = new UndoActionGroup({
|
|
label: `Rename to ${newName}`
|
|
});
|
|
|
|
UndoQueue.instance.push(undoGroup);
|
|
|
|
undoGroup.push({
|
|
do: () => {
|
|
ProjectModel.instance.renameComponent(component, newName);
|
|
// ☠️ THIS NEVER RUNS ☠️
|
|
},
|
|
undo: () => {
|
|
ProjectModel.instance.renameComponent(component, oldName);
|
|
}
|
|
});
|
|
|
|
undoGroup.do(); // Loop condition is already false
|
|
|
|
// Result:
|
|
// - Function returns successfully ✅
|
|
// - Undo/redo stack is populated ✅
|
|
// - But the action NEVER executes ❌
|
|
// - Component name doesn't change ❌
|
|
}
|
|
```
|
|
|
|
**Why it fails:**
|
|
|
|
1. `undoGroup.push()` increments internal `ptr` to `actions.length`
|
|
2. `undoGroup.do()` loops from `ptr` to `actions.length`
|
|
3. Since they're equal, loop never runs
|
|
4. Action is recorded but never executed
|
|
|
|
---
|
|
|
|
## Pattern Comparison Table
|
|
|
|
| Pattern | Executes? | Undoable? | Use Case |
|
|
| --------------------------------------------------------------- | --------- | --------- | ------------------------------ |
|
|
| `UndoQueue.instance.pushAndDo(new UndoActionGroup({do, undo}))` | ✅ Yes | ✅ Yes | **Use this 95% of the time** |
|
|
| `undoGroup.pushAndDo({do, undo})` | ✅ Yes | ✅ Yes | Building complex groups |
|
|
| `UndoQueue.instance.push(undoGroup); undoGroup.do()` | ❌ No | ⚠️ Yes\* | **Never use - silent failure** |
|
|
| `undoGroup.push({do, undo}); undoGroup.do()` | ❌ No | ⚠️ Yes\* | **Never use - silent failure** |
|
|
|
|
\* Undo/redo works only if action is manually triggered first
|
|
|
|
---
|
|
|
|
## Debugging Tips
|
|
|
|
If your undo action isn't executing:
|
|
|
|
### 1. Add Debug Logging
|
|
|
|
```typescript
|
|
UndoQueue.instance.pushAndDo(
|
|
new UndoActionGroup({
|
|
label: 'My Action',
|
|
do: () => {
|
|
console.log('🔥 ACTION EXECUTING'); // Should print immediately
|
|
// ... your action
|
|
},
|
|
undo: () => {
|
|
console.log('↩️ ACTION UNDOING');
|
|
// ... undo logic
|
|
}
|
|
})
|
|
);
|
|
```
|
|
|
|
If `🔥 ACTION EXECUTING` doesn't print, you have the `push + do` bug.
|
|
|
|
### 2. Check Your Pattern
|
|
|
|
Search your code for:
|
|
|
|
```typescript
|
|
undoGroup.push(
|
|
undoGroup.do(
|
|
```
|
|
|
|
If you find this pattern, you have the bug. Replace with `pushAndDo`.
|
|
|
|
### 3. Verify Success
|
|
|
|
After your action:
|
|
|
|
```typescript
|
|
// Should see immediate result
|
|
console.log('New name:', component.name); // Should be changed
|
|
```
|
|
|
|
---
|
|
|
|
## Migration Guide
|
|
|
|
If you have existing code using the broken pattern:
|
|
|
|
### Before (Broken):
|
|
|
|
```typescript
|
|
const undoGroup = new UndoActionGroup({ label: 'Action' });
|
|
UndoQueue.instance.push(undoGroup);
|
|
undoGroup.push({ do: () => {...}, undo: () => {...} });
|
|
undoGroup.do();
|
|
```
|
|
|
|
### After (Fixed):
|
|
|
|
```typescript
|
|
UndoQueue.instance.pushAndDo(
|
|
new UndoActionGroup({
|
|
label: 'Action',
|
|
do: () => {...},
|
|
undo: () => {...}
|
|
})
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
## Real-World Examples
|
|
|
|
### Example 1: Component Deletion
|
|
|
|
```typescript
|
|
function deleteComponent(component: ComponentModel) {
|
|
const componentJson = component.toJSON(); // Save for undo
|
|
|
|
UndoQueue.instance.pushAndDo(
|
|
new UndoActionGroup({
|
|
label: `Delete ${component.name}`,
|
|
do: () => {
|
|
ProjectModel.instance.removeComponent(component);
|
|
},
|
|
undo: () => {
|
|
const restored = ComponentModel.fromJSON(componentJson);
|
|
ProjectModel.instance.addComponent(restored);
|
|
}
|
|
})
|
|
);
|
|
}
|
|
```
|
|
|
|
### Example 2: Node Property Change
|
|
|
|
```typescript
|
|
function setNodeProperty(node: NodeGraphNode, propertyName: string, newValue: any) {
|
|
const oldValue = node.parameters[propertyName];
|
|
|
|
UndoQueue.instance.pushAndDo(
|
|
new UndoActionGroup({
|
|
label: `Change ${propertyName}`,
|
|
do: () => {
|
|
node.setParameter(propertyName, newValue);
|
|
},
|
|
undo: () => {
|
|
node.setParameter(propertyName, oldValue);
|
|
}
|
|
})
|
|
);
|
|
}
|
|
```
|
|
|
|
### Example 3: Drag and Drop (Multiple Items)
|
|
|
|
```typescript
|
|
function moveComponents(components: ComponentModel[], targetFolder: string) {
|
|
const moves = components.map((comp) => ({
|
|
component: comp,
|
|
oldPath: comp.name,
|
|
newPath: `${targetFolder}/${comp.localName}`
|
|
}));
|
|
|
|
UndoQueue.instance.pushAndDo(
|
|
new UndoActionGroup({
|
|
label: `Move ${components.length} components`,
|
|
do: () => {
|
|
moves.forEach(({ component, newPath }) => {
|
|
ProjectModel.instance.renameComponent(component, newPath);
|
|
});
|
|
},
|
|
undo: () => {
|
|
moves.forEach(({ component, oldPath }) => {
|
|
ProjectModel.instance.renameComponent(component, oldPath);
|
|
});
|
|
}
|
|
})
|
|
);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## See Also
|
|
|
|
- [LEARNINGS.md](./LEARNINGS.md#-critical-undoactiongroupdo-silent-failure-dec-2025) - Full technical explanation of the bug
|
|
- [COMMON-ISSUES.md](./COMMON-ISSUES.md) - Troubleshooting guide
|
|
- `packages/noodl-editor/src/editor/src/models/undo-queue-model.ts` - Source code
|
|
|
|
---
|
|
|
|
**Last Updated**: December 2025 (Phase 0 Foundation Stabilization)
|