constellation-analyzer/docs/UNDO_REDO_IMPLEMENTATION.md
Jan-Henrik Bruhn f56f928dcf Initial commit
2025-10-10 11:15:51 +02:00

294 lines
9.7 KiB
Markdown

# Undo/Redo System Implementation
## Overview
The Constellation Analyzer now features a comprehensive per-document undo/redo system that allows users to safely experiment with their graphs without fear of permanent mistakes.
**Key Features:**
-**Per-Document History**: Each document maintains its own independent undo/redo stack (max 50 actions)
-**Keyboard Shortcuts**: Ctrl+Z (undo), Ctrl+Y or Ctrl+Shift+Z (redo)
-**Visual UI**: Undo/Redo buttons in toolbar with disabled states and tooltips
-**Action Descriptions**: Hover tooltips show what action will be undone/redone
-**Automatic Tracking**: All graph operations are automatically tracked
-**Debounced Moves**: Node dragging is debounced to avoid cluttering history
-**Document Switching**: History is preserved when switching between documents
## Architecture
### 1. History Store (`src/stores/historyStore.ts`)
The central store manages history for all documents:
```typescript
{
histories: Map<documentId, DocumentHistory>
maxHistorySize: 50
}
```
Each `DocumentHistory` contains:
- `undoStack`: Array of past actions (most recent at end)
- `redoStack`: Array of undone actions that can be redone
- `currentState`: The current document state snapshot
**Key Methods:**
- `pushAction(documentId, action)`: Records a new action
- `undo(documentId)`: Reverts to previous state
- `redo(documentId)`: Restores undone state
- `canUndo/canRedo(documentId)`: Check if actions available
- `initializeHistory(documentId, initialState)`: Setup history for new document
- `removeHistory(documentId)`: Clean up when document deleted
### 2. Document History Hook (`src/hooks/useDocumentHistory.ts`)
Provides high-level undo/redo functionality for the active document:
```typescript
const { undo, redo, canUndo, canRedo, undoDescription, redoDescription, pushToHistory } = useDocumentHistory();
```
**Responsibilities:**
- Initializes history when document is first loaded
- Provides `pushToHistory(description)` to record actions
- Handles undo/redo by restoring document state
- Updates both graphStore and workspaceStore on undo/redo
- Marks documents as dirty after undo/redo
### 3. Graph Operations with History (`src/hooks/useGraphWithHistory.ts`)
**OPTIONAL WRAPPER**: This hook wraps all graph operations with automatic history tracking.
```typescript
const { addNode, updateNode, deleteNode, addEdge, ... } = useGraphWithHistory();
```
Features:
- Debounces node position changes (500ms) to avoid cluttering history during dragging
- Immediate history push for add/delete operations
- Smart action descriptions (e.g., "Add Person Actor", "Delete Collaborates Relation")
- Prevents recursive history pushes during undo/redo restore
**Note:** This is an alternative to manually calling `pushToHistory()` after each operation.
### 4. Keyboard Shortcuts (`src/hooks/useKeyboardShortcuts.ts`)
Extended to support undo/redo:
```typescript
useKeyboardShortcuts({
onUndo: undo,
onRedo: redo,
// ... other shortcuts
});
```
Handles:
- `Ctrl+Z` / `Cmd+Z`: Undo
- `Ctrl+Y` / `Cmd+Y`: Redo
- `Ctrl+Shift+Z` / `Cmd+Shift+Z`: Alternative redo
### 5. Toolbar UI (`src/components/Toolbar/Toolbar.tsx`)
Displays undo/redo buttons with visual feedback:
- **Undo Button**: Shows "Undo: [action description] (Ctrl+Z)" on hover
- **Redo Button**: Shows "Redo: [action description] (Ctrl+Y)" on hover
- Buttons are disabled (grayed out) when no actions available
- Uses Material-UI icons (UndoIcon, RedoIcon)
### 6. App Integration (`src/App.tsx`)
Connects keyboard shortcuts to undo/redo functionality:
```typescript
const { undo, redo } = useDocumentHistory();
useKeyboardShortcuts({
onUndo: undo,
onRedo: redo,
});
```
## Usage
### Option A: Manual History Tracking
Components can manually record actions:
```typescript
import { useDocumentHistory } from '../hooks/useDocumentHistory';
function MyComponent() {
const { pushToHistory } = useDocumentHistory();
const graphStore = useGraphStore();
const handleAddNode = () => {
graphStore.addNode(newNode);
pushToHistory('Add Person Actor');
};
}
```
### Option B: Automatic with useGraphWithHistory
Replace `useGraphStore()` with `useGraphWithHistory()`:
```typescript
import { useGraphWithHistory } from '../hooks/useGraphWithHistory';
function MyComponent() {
const { addNode } = useGraphWithHistory();
const handleAddNode = () => {
addNode(newNode); // Automatically tracked!
};
}
```
### Current Implementation
The current codebase uses **Option A** (manual tracking). Components like `GraphEditor` and `Toolbar` use `useGraphStore()` directly.
To enable automatic tracking, update components to use `useGraphWithHistory()` instead of `useGraphStore()`.
## How It Works: Undo/Redo Flow
### Recording an Action
1. User performs action (e.g., adds a node)
2. `pushToHistory('Add Person Actor')` is called
3. Current document state is snapshotted
4. Snapshot is pushed to `undoStack`
5. `redoStack` is cleared (since new action invalidates redo)
### Performing Undo
1. User presses Ctrl+Z or clicks Undo button
2. Last action is popped from `undoStack`
3. Current state is pushed to `redoStack`
4. Previous state from action is restored
5. GraphStore and WorkspaceStore are updated
6. Document marked as dirty
### Performing Redo
1. User presses Ctrl+Y or clicks Redo button
2. Last undone action is popped from `redoStack`
3. Current state is pushed to `undoStack`
4. Future state from undone action is restored
5. GraphStore and WorkspaceStore are updated
6. Document marked as dirty
## Per-Document Independence
**Critical Feature:** Each document has completely separate history.
Example workflow:
1. Document A: Add 3 nodes
2. Switch to Document B: Add 2 edges
3. Switch back to Document A: Can still undo those 3 node additions
4. Switch back to Document B: Can still undo those 2 edge additions
History stacks are **preserved** across document switches and **remain independent**.
## Performance Considerations
### Memory Management
- Max 50 actions per document (configurable via `MAX_HISTORY_SIZE`)
- Old actions are automatically removed when limit exceeded
- History is removed when document is deleted
- Document states use deep cloning to prevent mutation issues
### Debouncing
- Node position updates are debounced (500ms) to group multiple moves
- Add/delete operations are immediate (0ms delay)
- Prevents hundreds of history entries when dragging nodes
## Testing Checklist
- [x] Create history store
- [x] Create useDocumentHistory hook
- [x] Add keyboard shortcuts (Ctrl+Z, Ctrl+Y, Ctrl+Shift+Z)
- [x] Add undo/redo buttons to toolbar
- [x] Show action descriptions in tooltips
- [x] Disable buttons when no actions available
- [ ] Test: Add node → Undo → Node disappears
- [ ] Test: Delete node → Undo → Node reappears with connections
- [ ] Test: Move node → Undo → Node returns to original position
- [ ] Test: Add edge → Undo → Edge disappears
- [ ] Test: Update node properties → Undo → Properties restored
- [ ] Test: Multiple operations → Undo multiple times → Redo multiple times
- [ ] Test: Document A changes → Switch to Document B → Changes independent
- [ ] Test: 51 actions → Oldest action removed from history
- [ ] Test: Undo then new action → Redo stack cleared
- [ ] Test: Keyboard shortcuts work (Ctrl+Z, Ctrl+Y)
## Future Enhancements
1. **History Panel**: Show list of all actions with ability to jump to specific point
2. **Persistent History**: Save history to localStorage (survives page refresh)
3. **Collaborative Undo**: Undo operations in multi-user scenarios
4. **Selective Undo**: Undo specific actions, not just chronological
5. **History Branching**: Tree-based history (like Git) instead of linear
6. **Action Grouping**: Combine related actions (e.g., "Add 5 nodes" instead of 5 separate entries)
7. **Undo Metadata**: Store viewport position, selection state with each action
8. **History Analytics**: Track most common actions, undo patterns
## Implementation Notes
### Why Deep Cloning?
Document states are deep cloned using `JSON.parse(JSON.stringify())` to prevent mutation:
```typescript
const snapshot = JSON.parse(JSON.stringify(currentDoc));
```
This ensures that modifying the current state doesn't affect historical snapshots.
### Why Separate undoStack and redoStack?
Standard undo/redo pattern:
- **undoStack**: Stores past states
- **redoStack**: Stores undone states that can be restored
When a new action occurs, redoStack is cleared because the "future" is no longer valid.
### Why Per-Document History?
Users expect each document to maintain independent history, similar to:
- Text editors (each file has own undo stack)
- Image editors (each image has own history)
- IDEs (each file has own history)
This matches user mental model and prevents confusion.
## File Structure
```
src/
├── stores/
│ └── historyStore.ts # Central history management
├── hooks/
│ ├── useDocumentHistory.ts # Per-document undo/redo
│ ├── useGraphWithHistory.ts # Automatic history tracking wrapper
│ └── useKeyboardShortcuts.ts # Keyboard shortcuts (extended)
├── components/
│ └── Toolbar/
│ └── Toolbar.tsx # UI buttons for undo/redo
└── App.tsx # Connects keyboard shortcuts
```
## Conclusion
The undo/redo system provides a safety net for users, encouraging experimentation without fear of permanent mistakes. Each document maintains independent history, operations are automatically tracked, and the UI provides clear feedback about available undo/redo actions.
**Status:** ✅ Implementation Complete (Ready for Testing)
---
**Implemented:** 2025-10-09
**Based on:** UX_ANALYSIS.md recommendations (Priority: CRITICAL)