mirror of
https://github.com/OFFIS-ESC/constellation-analyzer
synced 2026-01-27 07:43:41 +00:00
refactor: establish document as source of truth for node/edge types
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>
This commit is contained in:
parent
5275b52f0a
commit
89117415ed
5 changed files with 248 additions and 31 deletions
|
|
@ -47,7 +47,8 @@ export function useDocumentHistory() {
|
|||
}
|
||||
|
||||
// Get current state from stores
|
||||
const graphStore = useGraphStore.getState();
|
||||
const workspaceStore = useWorkspaceStore.getState();
|
||||
const activeDoc = workspaceStore.getActiveDocument();
|
||||
const timelineStore = useTimelineStore.getState();
|
||||
const timeline = timelineStore.timelines.get(activeDocumentId);
|
||||
|
||||
|
|
@ -56,15 +57,21 @@ export function useDocumentHistory() {
|
|||
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: graphStore.nodeTypes,
|
||||
edgeTypes: graphStore.edgeTypes,
|
||||
nodeTypes: activeDoc.nodeTypes,
|
||||
edgeTypes: activeDoc.edgeTypes,
|
||||
};
|
||||
|
||||
// Push to history
|
||||
|
|
@ -87,7 +94,8 @@ export function useDocumentHistory() {
|
|||
}
|
||||
|
||||
// Capture current state BEFORE undoing
|
||||
const graphStore = useGraphStore.getState();
|
||||
const workspaceStore = useWorkspaceStore.getState();
|
||||
const activeDoc = workspaceStore.getActiveDocument();
|
||||
const timelineStore = useTimelineStore.getState();
|
||||
const timeline = timelineStore.timelines.get(activeDocumentId);
|
||||
|
||||
|
|
@ -96,14 +104,20 @@ export function useDocumentHistory() {
|
|||
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: graphStore.nodeTypes,
|
||||
edgeTypes: graphStore.edgeTypes,
|
||||
nodeTypes: activeDoc.nodeTypes,
|
||||
edgeTypes: activeDoc.edgeTypes,
|
||||
};
|
||||
|
||||
const restoredState = historyStore.undo(activeDocumentId, currentSnapshot);
|
||||
|
|
@ -112,7 +126,11 @@ export function useDocumentHistory() {
|
|||
// Restore complete document state (timeline + types)
|
||||
timelineStore.loadTimeline(activeDocumentId, restoredState.timeline);
|
||||
|
||||
// Update graph store types
|
||||
// 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);
|
||||
|
||||
|
|
@ -146,7 +164,8 @@ export function useDocumentHistory() {
|
|||
}
|
||||
|
||||
// Capture current state BEFORE redoing
|
||||
const graphStore = useGraphStore.getState();
|
||||
const workspaceStore = useWorkspaceStore.getState();
|
||||
const activeDoc = workspaceStore.getActiveDocument();
|
||||
const timelineStore = useTimelineStore.getState();
|
||||
const timeline = timelineStore.timelines.get(activeDocumentId);
|
||||
|
||||
|
|
@ -155,14 +174,20 @@ export function useDocumentHistory() {
|
|||
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: graphStore.nodeTypes,
|
||||
edgeTypes: graphStore.edgeTypes,
|
||||
nodeTypes: activeDoc.nodeTypes,
|
||||
edgeTypes: activeDoc.edgeTypes,
|
||||
};
|
||||
|
||||
const restoredState = historyStore.redo(activeDocumentId, currentSnapshot);
|
||||
|
|
@ -171,7 +196,11 @@ export function useDocumentHistory() {
|
|||
// Restore complete document state (timeline + types)
|
||||
timelineStore.loadTimeline(activeDocumentId, restoredState.timeline);
|
||||
|
||||
// Update graph store types
|
||||
// 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);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useCallback, useRef, useEffect } from 'react';
|
||||
import { useGraphStore } from '../stores/graphStore';
|
||||
import { useWorkspaceStore } from '../stores/workspaceStore';
|
||||
import { useDocumentHistory } from './useDocumentHistory';
|
||||
import type { Actor, Relation, NodeTypeConfig, EdgeTypeConfig, RelationData } from '../types';
|
||||
|
||||
|
|
@ -38,6 +39,13 @@ import type { Actor, Relation, NodeTypeConfig, EdgeTypeConfig, RelationData } fr
|
|||
*/
|
||||
export function useGraphWithHistory() {
|
||||
const graphStore = useGraphStore();
|
||||
const activeDocumentId = useWorkspaceStore((state) => state.activeDocumentId);
|
||||
const addNodeTypeToDocument = useWorkspaceStore((state) => state.addNodeTypeToDocument);
|
||||
const updateNodeTypeInDocument = useWorkspaceStore((state) => state.updateNodeTypeInDocument);
|
||||
const deleteNodeTypeFromDocument = useWorkspaceStore((state) => state.deleteNodeTypeFromDocument);
|
||||
const addEdgeTypeToDocument = useWorkspaceStore((state) => state.addEdgeTypeToDocument);
|
||||
const updateEdgeTypeInDocument = useWorkspaceStore((state) => state.updateEdgeTypeInDocument);
|
||||
const deleteEdgeTypeFromDocument = useWorkspaceStore((state) => state.deleteEdgeTypeFromDocument);
|
||||
const { pushToHistory } = useDocumentHistory();
|
||||
|
||||
// Track if we're currently restoring from history to prevent recursive history pushes
|
||||
|
|
@ -169,76 +177,100 @@ export function useGraphWithHistory() {
|
|||
|
||||
const addNodeType = useCallback(
|
||||
(nodeType: NodeTypeConfig) => {
|
||||
if (!activeDocumentId) {
|
||||
console.warn('No active document');
|
||||
return;
|
||||
}
|
||||
if (isRestoringRef.current) {
|
||||
graphStore.addNodeType(nodeType);
|
||||
return;
|
||||
}
|
||||
pushToHistory(`Add Node Type: ${nodeType.label}`); // Synchronous push BEFORE mutation
|
||||
graphStore.addNodeType(nodeType);
|
||||
addNodeTypeToDocument(activeDocumentId, nodeType);
|
||||
},
|
||||
[graphStore, pushToHistory]
|
||||
[activeDocumentId, graphStore, pushToHistory, addNodeTypeToDocument]
|
||||
);
|
||||
|
||||
const updateNodeType = useCallback(
|
||||
(id: string, updates: Partial<Omit<NodeTypeConfig, 'id'>>) => {
|
||||
if (!activeDocumentId) {
|
||||
console.warn('No active document');
|
||||
return;
|
||||
}
|
||||
if (isRestoringRef.current) {
|
||||
graphStore.updateNodeType(id, updates);
|
||||
return;
|
||||
}
|
||||
pushToHistory('Update Node Type'); // Synchronous push BEFORE mutation
|
||||
graphStore.updateNodeType(id, updates);
|
||||
updateNodeTypeInDocument(activeDocumentId, id, updates);
|
||||
},
|
||||
[graphStore, pushToHistory]
|
||||
[activeDocumentId, graphStore, pushToHistory, updateNodeTypeInDocument]
|
||||
);
|
||||
|
||||
const deleteNodeType = useCallback(
|
||||
(id: string) => {
|
||||
if (!activeDocumentId) {
|
||||
console.warn('No active document');
|
||||
return;
|
||||
}
|
||||
if (isRestoringRef.current) {
|
||||
graphStore.deleteNodeType(id);
|
||||
return;
|
||||
}
|
||||
const nodeType = graphStore.nodeTypes.find((nt) => nt.id === id);
|
||||
pushToHistory(`Delete Node Type: ${nodeType?.label || id}`); // Synchronous push BEFORE mutation
|
||||
graphStore.deleteNodeType(id);
|
||||
deleteNodeTypeFromDocument(activeDocumentId, id);
|
||||
},
|
||||
[graphStore, pushToHistory]
|
||||
[activeDocumentId, graphStore, pushToHistory, deleteNodeTypeFromDocument]
|
||||
);
|
||||
|
||||
const addEdgeType = useCallback(
|
||||
(edgeType: EdgeTypeConfig) => {
|
||||
if (!activeDocumentId) {
|
||||
console.warn('No active document');
|
||||
return;
|
||||
}
|
||||
if (isRestoringRef.current) {
|
||||
graphStore.addEdgeType(edgeType);
|
||||
return;
|
||||
}
|
||||
pushToHistory(`Add Edge Type: ${edgeType.label}`); // Synchronous push BEFORE mutation
|
||||
graphStore.addEdgeType(edgeType);
|
||||
addEdgeTypeToDocument(activeDocumentId, edgeType);
|
||||
},
|
||||
[graphStore, pushToHistory]
|
||||
[activeDocumentId, graphStore, pushToHistory, addEdgeTypeToDocument]
|
||||
);
|
||||
|
||||
const updateEdgeType = useCallback(
|
||||
(id: string, updates: Partial<Omit<EdgeTypeConfig, 'id'>>) => {
|
||||
if (!activeDocumentId) {
|
||||
console.warn('No active document');
|
||||
return;
|
||||
}
|
||||
if (isRestoringRef.current) {
|
||||
graphStore.updateEdgeType(id, updates);
|
||||
return;
|
||||
}
|
||||
pushToHistory('Update Edge Type'); // Synchronous push BEFORE mutation
|
||||
graphStore.updateEdgeType(id, updates);
|
||||
updateEdgeTypeInDocument(activeDocumentId, id, updates);
|
||||
},
|
||||
[graphStore, pushToHistory]
|
||||
[activeDocumentId, graphStore, pushToHistory, updateEdgeTypeInDocument]
|
||||
);
|
||||
|
||||
const deleteEdgeType = useCallback(
|
||||
(id: string) => {
|
||||
if (!activeDocumentId) {
|
||||
console.warn('No active document');
|
||||
return;
|
||||
}
|
||||
if (isRestoringRef.current) {
|
||||
graphStore.deleteEdgeType(id);
|
||||
return;
|
||||
}
|
||||
const edgeType = graphStore.edgeTypes.find((et) => et.id === id);
|
||||
pushToHistory(`Delete Edge Type: ${edgeType?.label || id}`); // Synchronous push BEFORE mutation
|
||||
graphStore.deleteEdgeType(id);
|
||||
deleteEdgeTypeFromDocument(activeDocumentId, id);
|
||||
},
|
||||
[graphStore, pushToHistory]
|
||||
[activeDocumentId, graphStore, pushToHistory, deleteEdgeTypeFromDocument]
|
||||
);
|
||||
|
||||
const clearGraph = useCallback(
|
||||
|
|
|
|||
|
|
@ -87,6 +87,14 @@ export interface WorkspaceActions {
|
|||
markDocumentDirty: (documentId: string) => void;
|
||||
saveDocument: (documentId: string) => void;
|
||||
|
||||
// Type management (document-level)
|
||||
addNodeTypeToDocument: (documentId: string, nodeType: NodeTypeConfig) => void;
|
||||
updateNodeTypeInDocument: (documentId: string, typeId: string, updates: Partial<Omit<NodeTypeConfig, 'id'>>) => void;
|
||||
deleteNodeTypeFromDocument: (documentId: string, typeId: string) => void;
|
||||
addEdgeTypeToDocument: (documentId: string, edgeType: EdgeTypeConfig) => void;
|
||||
updateEdgeTypeInDocument: (documentId: string, typeId: string, updates: Partial<Omit<EdgeTypeConfig, 'id'>>) => void;
|
||||
deleteEdgeTypeFromDocument: (documentId: string, typeId: string) => void;
|
||||
|
||||
// Viewport operations
|
||||
saveViewport: (documentId: string, viewport: { x: number; y: number; zoom: number }) => void;
|
||||
getViewport: (documentId: string) => { x: number; y: number; zoom: number } | undefined;
|
||||
|
|
|
|||
|
|
@ -143,17 +143,17 @@ export function useActiveDocument() {
|
|||
}
|
||||
|
||||
// Mark document as dirty when graph changes
|
||||
// NOTE: We only track nodes/edges here. Type changes are handled by workspaceStore's
|
||||
// type management actions, which directly mark the document as dirty.
|
||||
const hasChanges =
|
||||
JSON.stringify(graphNodes) !== JSON.stringify(lastSyncedStateRef.current.nodes) ||
|
||||
JSON.stringify(graphEdges) !== JSON.stringify(lastSyncedStateRef.current.edges) ||
|
||||
JSON.stringify(graphNodeTypes) !== JSON.stringify(lastSyncedStateRef.current.nodeTypes) ||
|
||||
JSON.stringify(graphEdgeTypes) !== JSON.stringify(lastSyncedStateRef.current.edgeTypes);
|
||||
JSON.stringify(graphEdges) !== JSON.stringify(lastSyncedStateRef.current.edges);
|
||||
|
||||
if (hasChanges) {
|
||||
console.log(`Document ${activeDocumentId} has changes, marking as dirty`);
|
||||
markDocumentDirty(activeDocumentId);
|
||||
|
||||
// Update the last synced state
|
||||
// Update the last synced state (keep types for reference, but don't track them for changes)
|
||||
lastSyncedStateRef.current = {
|
||||
documentId: activeDocumentId,
|
||||
nodes: graphNodes as Actor[],
|
||||
|
|
|
|||
|
|
@ -715,10 +715,9 @@ export const useWorkspaceStore = create<Workspace & WorkspaceActions>((set, get)
|
|||
if (doc) {
|
||||
doc.metadata.updatedAt = new Date().toISOString();
|
||||
|
||||
// Save global node and edge types from graph store
|
||||
const graphStore = useGraphStore.getState();
|
||||
doc.nodeTypes = graphStore.nodeTypes;
|
||||
doc.edgeTypes = graphStore.edgeTypes;
|
||||
// NOTE: nodeTypes and edgeTypes are already part of the document structure
|
||||
// and are managed via workspaceStore's type management actions.
|
||||
// We do NOT copy them from graphStore because the document is the source of truth.
|
||||
|
||||
// Save timeline data if exists
|
||||
const timelineState = useTimelineStore.getState();
|
||||
|
|
@ -882,4 +881,153 @@ export const useWorkspaceStore = create<Workspace & WorkspaceActions>((set, get)
|
|||
const metadata = state.documentMetadata.get(documentId);
|
||||
return metadata?.viewport;
|
||||
},
|
||||
|
||||
// Type management - document-level operations
|
||||
addNodeTypeToDocument: (documentId: string, nodeType) => {
|
||||
const state = get();
|
||||
const doc = state.documents.get(documentId);
|
||||
|
||||
if (!doc) {
|
||||
console.error(`Document ${documentId} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add to document's node types
|
||||
doc.nodeTypes = [...doc.nodeTypes, nodeType];
|
||||
|
||||
// Save document
|
||||
saveDocumentToStorage(documentId, doc);
|
||||
|
||||
// Mark as dirty
|
||||
get().markDocumentDirty(documentId);
|
||||
|
||||
// If this is the active document, sync to graphStore
|
||||
if (documentId === state.activeDocumentId) {
|
||||
useGraphStore.getState().setNodeTypes(doc.nodeTypes);
|
||||
}
|
||||
},
|
||||
|
||||
updateNodeTypeInDocument: (documentId: string, typeId: string, updates) => {
|
||||
const state = get();
|
||||
const doc = state.documents.get(documentId);
|
||||
|
||||
if (!doc) {
|
||||
console.error(`Document ${documentId} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update in document's node types
|
||||
doc.nodeTypes = doc.nodeTypes.map((type) =>
|
||||
type.id === typeId ? { ...type, ...updates } : type
|
||||
);
|
||||
|
||||
// Save document
|
||||
saveDocumentToStorage(documentId, doc);
|
||||
|
||||
// Mark as dirty
|
||||
get().markDocumentDirty(documentId);
|
||||
|
||||
// If this is the active document, sync to graphStore
|
||||
if (documentId === state.activeDocumentId) {
|
||||
useGraphStore.getState().setNodeTypes(doc.nodeTypes);
|
||||
}
|
||||
},
|
||||
|
||||
deleteNodeTypeFromDocument: (documentId: string, typeId: string) => {
|
||||
const state = get();
|
||||
const doc = state.documents.get(documentId);
|
||||
|
||||
if (!doc) {
|
||||
console.error(`Document ${documentId} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove from document's node types
|
||||
doc.nodeTypes = doc.nodeTypes.filter((type) => type.id !== typeId);
|
||||
|
||||
// Save document
|
||||
saveDocumentToStorage(documentId, doc);
|
||||
|
||||
// Mark as dirty
|
||||
get().markDocumentDirty(documentId);
|
||||
|
||||
// If this is the active document, sync to graphStore
|
||||
if (documentId === state.activeDocumentId) {
|
||||
useGraphStore.getState().setNodeTypes(doc.nodeTypes);
|
||||
}
|
||||
},
|
||||
|
||||
addEdgeTypeToDocument: (documentId: string, edgeType) => {
|
||||
const state = get();
|
||||
const doc = state.documents.get(documentId);
|
||||
|
||||
if (!doc) {
|
||||
console.error(`Document ${documentId} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add to document's edge types
|
||||
doc.edgeTypes = [...doc.edgeTypes, edgeType];
|
||||
|
||||
// Save document
|
||||
saveDocumentToStorage(documentId, doc);
|
||||
|
||||
// Mark as dirty
|
||||
get().markDocumentDirty(documentId);
|
||||
|
||||
// If this is the active document, sync to graphStore
|
||||
if (documentId === state.activeDocumentId) {
|
||||
useGraphStore.getState().setEdgeTypes(doc.edgeTypes);
|
||||
}
|
||||
},
|
||||
|
||||
updateEdgeTypeInDocument: (documentId: string, typeId: string, updates) => {
|
||||
const state = get();
|
||||
const doc = state.documents.get(documentId);
|
||||
|
||||
if (!doc) {
|
||||
console.error(`Document ${documentId} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update in document's edge types
|
||||
doc.edgeTypes = doc.edgeTypes.map((type) =>
|
||||
type.id === typeId ? { ...type, ...updates } : type
|
||||
);
|
||||
|
||||
// Save document
|
||||
saveDocumentToStorage(documentId, doc);
|
||||
|
||||
// Mark as dirty
|
||||
get().markDocumentDirty(documentId);
|
||||
|
||||
// If this is the active document, sync to graphStore
|
||||
if (documentId === state.activeDocumentId) {
|
||||
useGraphStore.getState().setEdgeTypes(doc.edgeTypes);
|
||||
}
|
||||
},
|
||||
|
||||
deleteEdgeTypeFromDocument: (documentId: string, typeId: string) => {
|
||||
const state = get();
|
||||
const doc = state.documents.get(documentId);
|
||||
|
||||
if (!doc) {
|
||||
console.error(`Document ${documentId} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove from document's edge types
|
||||
doc.edgeTypes = doc.edgeTypes.filter((type) => type.id !== typeId);
|
||||
|
||||
// Save document
|
||||
saveDocumentToStorage(documentId, doc);
|
||||
|
||||
// Mark as dirty
|
||||
get().markDocumentDirty(documentId);
|
||||
|
||||
// If this is the active document, sync to graphStore
|
||||
if (documentId === state.activeDocumentId) {
|
||||
useGraphStore.getState().setEdgeTypes(doc.edgeTypes);
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
|
|
|||
Loading…
Reference in a new issue