mirror of
https://github.com/OFFIS-ESC/constellation-analyzer
synced 2026-01-27 07:43:41 +00:00
Refactors the type management system to properly follow the Workspace→Document→Timeline→Graph hierarchy, where nodeTypes and edgeTypes are owned by the document rather than being independent state in graphStore. Changes: - Add type management actions to workspaceStore (document-level) - Update workspaceStore.saveDocument to stop copying types from graphStore - Modify useActiveDocument to only track node/edge changes for dirty state - Update useGraphWithHistory to route type operations through workspaceStore - Update useDocumentHistory to read/restore types from document Architecture: - Document: Source of truth for types (persistent storage) - graphStore: Synchronized working memory (optimized for React Flow) - useActiveDocument: Bridge that syncs document → graphStore - Type mutations: Always go through workspaceStore, then sync to graphStore This ensures proper data ownership while maintaining graphStore as a performance optimization layer for the UI. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
260 lines
8.1 KiB
TypeScript
260 lines
8.1 KiB
TypeScript
import { useCallback, useEffect } from 'react';
|
|
import { useWorkspaceStore } from '../stores/workspaceStore';
|
|
import { useHistoryStore } from '../stores/historyStore';
|
|
import { useGraphStore } from '../stores/graphStore';
|
|
import { useTimelineStore } from '../stores/timelineStore';
|
|
import type { DocumentSnapshot } from '../stores/historyStore';
|
|
|
|
/**
|
|
* useDocumentHistory Hook
|
|
*
|
|
* Provides undo/redo functionality for the active document.
|
|
* Each document has its own independent history stack (max 50 actions).
|
|
*
|
|
* IMPORTANT: History is per-document. All operations (graph changes, timeline operations)
|
|
* are tracked in a single unified history stack for the entire document.
|
|
*
|
|
* Usage:
|
|
* const { undo, redo, canUndo, canRedo, pushToHistory } = useDocumentHistory();
|
|
*/
|
|
export function useDocumentHistory() {
|
|
const activeDocumentId = useWorkspaceStore((state) => state.activeDocumentId);
|
|
const markDocumentDirty = useWorkspaceStore((state) => state.markDocumentDirty);
|
|
|
|
const setNodeTypes = useGraphStore((state) => state.setNodeTypes);
|
|
const setEdgeTypes = useGraphStore((state) => state.setEdgeTypes);
|
|
|
|
const historyStore = useHistoryStore();
|
|
|
|
// Initialize history for active document
|
|
useEffect(() => {
|
|
if (!activeDocumentId) return;
|
|
|
|
const history = historyStore.histories.get(activeDocumentId);
|
|
if (!history) {
|
|
historyStore.initializeHistory(activeDocumentId);
|
|
}
|
|
}, [activeDocumentId, historyStore]);
|
|
|
|
/**
|
|
* Push current document state to history (timeline + types)
|
|
*/
|
|
const pushToHistory = useCallback(
|
|
(description: string) => {
|
|
if (!activeDocumentId) {
|
|
console.warn('No active document to record action');
|
|
return;
|
|
}
|
|
|
|
// Get current state from stores
|
|
const workspaceStore = useWorkspaceStore.getState();
|
|
const activeDoc = workspaceStore.getActiveDocument();
|
|
const timelineStore = useTimelineStore.getState();
|
|
const timeline = timelineStore.timelines.get(activeDocumentId);
|
|
|
|
if (!timeline) {
|
|
console.warn('No timeline found for active document');
|
|
return;
|
|
}
|
|
|
|
if (!activeDoc) {
|
|
console.warn('Active document not found');
|
|
return;
|
|
}
|
|
|
|
// Create a snapshot of the complete document state
|
|
// NOTE: Read types from the document, not from graphStore
|
|
const snapshot: DocumentSnapshot = {
|
|
timeline: {
|
|
states: new Map(timeline.states), // Clone the Map
|
|
currentStateId: timeline.currentStateId,
|
|
rootStateId: timeline.rootStateId,
|
|
},
|
|
nodeTypes: activeDoc.nodeTypes,
|
|
edgeTypes: activeDoc.edgeTypes,
|
|
};
|
|
|
|
// Push to history
|
|
historyStore.pushAction(activeDocumentId, {
|
|
description,
|
|
timestamp: Date.now(),
|
|
documentState: snapshot,
|
|
});
|
|
},
|
|
[activeDocumentId, historyStore]
|
|
);
|
|
|
|
/**
|
|
* Undo the last action for the active document
|
|
*/
|
|
const undo = useCallback(() => {
|
|
if (!activeDocumentId) {
|
|
console.warn('No active document to undo');
|
|
return;
|
|
}
|
|
|
|
// Capture current state BEFORE undoing
|
|
const workspaceStore = useWorkspaceStore.getState();
|
|
const activeDoc = workspaceStore.getActiveDocument();
|
|
const timelineStore = useTimelineStore.getState();
|
|
const timeline = timelineStore.timelines.get(activeDocumentId);
|
|
|
|
if (!timeline) {
|
|
console.warn('No timeline found for active document');
|
|
return;
|
|
}
|
|
|
|
if (!activeDoc) {
|
|
console.warn('Active document not found');
|
|
return;
|
|
}
|
|
|
|
// NOTE: Read types from the document, not from graphStore
|
|
const currentSnapshot: DocumentSnapshot = {
|
|
timeline: {
|
|
states: new Map(timeline.states),
|
|
currentStateId: timeline.currentStateId,
|
|
rootStateId: timeline.rootStateId,
|
|
},
|
|
nodeTypes: activeDoc.nodeTypes,
|
|
edgeTypes: activeDoc.edgeTypes,
|
|
};
|
|
|
|
const restoredState = historyStore.undo(activeDocumentId, currentSnapshot);
|
|
if (restoredState) {
|
|
|
|
// Restore complete document state (timeline + types)
|
|
timelineStore.loadTimeline(activeDocumentId, restoredState.timeline);
|
|
|
|
// Update document's types (which will sync to graphStore via workspaceStore)
|
|
activeDoc.nodeTypes = restoredState.nodeTypes;
|
|
activeDoc.edgeTypes = restoredState.edgeTypes;
|
|
|
|
// Sync to graph store
|
|
setNodeTypes(restoredState.nodeTypes);
|
|
setEdgeTypes(restoredState.edgeTypes);
|
|
|
|
// Load the current state's graph from the restored timeline
|
|
const currentState = restoredState.timeline.states.get(restoredState.timeline.currentStateId);
|
|
if (currentState) {
|
|
useGraphStore.setState({
|
|
nodes: currentState.graph.nodes,
|
|
edges: currentState.graph.edges,
|
|
});
|
|
}
|
|
|
|
// Mark document as dirty and trigger auto-save
|
|
markDocumentDirty(activeDocumentId);
|
|
|
|
// Auto-save after a short delay
|
|
const { saveDocument } = useWorkspaceStore.getState();
|
|
setTimeout(() => {
|
|
saveDocument(activeDocumentId);
|
|
}, 1000);
|
|
}
|
|
}, [activeDocumentId, historyStore, setNodeTypes, setEdgeTypes, markDocumentDirty]);
|
|
|
|
/**
|
|
* Redo the last undone action for the active document
|
|
*/
|
|
const redo = useCallback(() => {
|
|
if (!activeDocumentId) {
|
|
console.warn('No active document to redo');
|
|
return;
|
|
}
|
|
|
|
// Capture current state BEFORE redoing
|
|
const workspaceStore = useWorkspaceStore.getState();
|
|
const activeDoc = workspaceStore.getActiveDocument();
|
|
const timelineStore = useTimelineStore.getState();
|
|
const timeline = timelineStore.timelines.get(activeDocumentId);
|
|
|
|
if (!timeline) {
|
|
console.warn('No timeline found for active document');
|
|
return;
|
|
}
|
|
|
|
if (!activeDoc) {
|
|
console.warn('Active document not found');
|
|
return;
|
|
}
|
|
|
|
// NOTE: Read types from the document, not from graphStore
|
|
const currentSnapshot: DocumentSnapshot = {
|
|
timeline: {
|
|
states: new Map(timeline.states),
|
|
currentStateId: timeline.currentStateId,
|
|
rootStateId: timeline.rootStateId,
|
|
},
|
|
nodeTypes: activeDoc.nodeTypes,
|
|
edgeTypes: activeDoc.edgeTypes,
|
|
};
|
|
|
|
const restoredState = historyStore.redo(activeDocumentId, currentSnapshot);
|
|
if (restoredState) {
|
|
|
|
// Restore complete document state (timeline + types)
|
|
timelineStore.loadTimeline(activeDocumentId, restoredState.timeline);
|
|
|
|
// Update document's types (which will sync to graphStore via workspaceStore)
|
|
activeDoc.nodeTypes = restoredState.nodeTypes;
|
|
activeDoc.edgeTypes = restoredState.edgeTypes;
|
|
|
|
// Sync to graph store
|
|
setNodeTypes(restoredState.nodeTypes);
|
|
setEdgeTypes(restoredState.edgeTypes);
|
|
|
|
// Load the current state's graph from the restored timeline
|
|
const currentState = restoredState.timeline.states.get(restoredState.timeline.currentStateId);
|
|
if (currentState) {
|
|
useGraphStore.setState({
|
|
nodes: currentState.graph.nodes,
|
|
edges: currentState.graph.edges,
|
|
});
|
|
}
|
|
|
|
// Mark document as dirty and trigger auto-save
|
|
markDocumentDirty(activeDocumentId);
|
|
|
|
// Auto-save after a short delay
|
|
const { saveDocument } = useWorkspaceStore.getState();
|
|
setTimeout(() => {
|
|
saveDocument(activeDocumentId);
|
|
}, 1000);
|
|
}
|
|
}, [activeDocumentId, historyStore, setNodeTypes, setEdgeTypes, markDocumentDirty]);
|
|
|
|
/**
|
|
* Check if undo is available for the active document
|
|
*/
|
|
const canUndo = activeDocumentId ? historyStore.canUndo(activeDocumentId) : false;
|
|
|
|
/**
|
|
* Check if redo is available for the active document
|
|
*/
|
|
const canRedo = activeDocumentId ? historyStore.canRedo(activeDocumentId) : false;
|
|
|
|
/**
|
|
* Get the description of the next undo action
|
|
*/
|
|
const undoDescription = activeDocumentId
|
|
? historyStore.getUndoDescription(activeDocumentId)
|
|
: null;
|
|
|
|
/**
|
|
* Get the description of the next redo action
|
|
*/
|
|
const redoDescription = activeDocumentId
|
|
? historyStore.getRedoDescription(activeDocumentId)
|
|
: null;
|
|
|
|
return {
|
|
undo,
|
|
redo,
|
|
canUndo,
|
|
canRedo,
|
|
undoDescription,
|
|
redoDescription,
|
|
pushToHistory,
|
|
};
|
|
}
|