From aa2bd7e5d7215931d9392f5b5ba082138b235d94 Mon Sep 17 00:00:00 2001 From: Jan-Henrik Bruhn Date: Sun, 12 Oct 2025 11:11:32 +0200 Subject: [PATCH] fix: preserve timeline states in document import/export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed critical bug where importing a document would lose all timeline states except the current one. Also cleaned up unused legacy persistence functions. Changes: - Import now preserves complete document structure with all timeline states - Export already worked correctly, serializing all timeline states - Updated fileIO to return full ConstellationDocument instead of just graph - Removed unused legacy functions: hasSavedState, getLastSavedTimestamp, clearSavedState - Removed unused graphStore export/import (replaced by workspace-level system) - Updated type definitions to reflect removed functions The import process now correctly: 1. Accepts full ConstellationDocument from imported JSON 2. Preserves all timeline states and relationships 3. Loads complete timeline into timelineStore 4. Maintains document title and metadata 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/hooks/useGraphWithHistory.ts | 7 ++++--- src/stores/graphStore.ts | 31 +++---------------------------- src/stores/persistence/fileIO.ts | 28 +++++++--------------------- src/stores/persistence/loader.ts | 11 ++--------- src/stores/persistence/saver.ts | 8 +++----- src/stores/workspaceStore.ts | 22 ++++++++-------------- src/types/index.ts | 4 ++-- 7 files changed, 29 insertions(+), 82 deletions(-) diff --git a/src/hooks/useGraphWithHistory.ts b/src/hooks/useGraphWithHistory.ts index 1a4cbf8..b7abed3 100644 --- a/src/hooks/useGraphWithHistory.ts +++ b/src/hooks/useGraphWithHistory.ts @@ -24,7 +24,7 @@ import type { Actor, Relation, NodeTypeConfig, EdgeTypeConfig, RelationData } fr * Read-only pass-through operations (no history): * - setNodes, setEdges (used for bulk updates during undo/redo/document loading) * - nodes, edges, nodeTypes, edgeTypes (state access) - * - exportToFile, importFromFile, loadGraphState + * - loadGraphState * * Usage: * const { addNode, updateNode, deleteNode, ... } = useGraphWithHistory(); @@ -278,10 +278,11 @@ export function useGraphWithHistory() { setEdges: graphStore.setEdges, setNodeTypes: graphStore.setNodeTypes, setEdgeTypes: graphStore.setEdgeTypes, - exportToFile: graphStore.exportToFile, - importFromFile: graphStore.importFromFile, loadGraphState: graphStore.loadGraphState, + // NOTE: exportToFile and importFromFile have been removed + // Import/export is now handled by the workspace-level system (useWorkspaceStore) + // Expose flag for detecting restore operations isRestoringRef, }; diff --git a/src/stores/graphStore.ts b/src/stores/graphStore.ts index 2d4a724..b04cfd0 100644 --- a/src/stores/graphStore.ts +++ b/src/stores/graphStore.ts @@ -9,7 +9,6 @@ import type { GraphActions } from '../types'; import { loadGraphState } from './persistence/loader'; -import { exportGraphToFile, selectFileForImport } from './persistence/fileIO'; /** * ⚠️ IMPORTANT: DO NOT USE THIS STORE DIRECTLY IN COMPONENTS ⚠️ @@ -187,33 +186,9 @@ export const useGraphStore = create((set) => ({ edgeTypes, }), - // File import/export operations - exportToFile: () => { - const state = useGraphStore.getState(); - exportGraphToFile(state.nodes, state.edges, state.nodeTypes, state.edgeTypes); - }, - - importFromFile: (onError?: (error: string) => void) => { - selectFileForImport( - (data) => { - // Load the imported data into the store - set({ - nodes: data.nodes, - edges: data.edges, - nodeTypes: data.nodeTypes, - edgeTypes: data.edgeTypes, - }); - }, - (error) => { - console.error('Import failed:', error); - if (onError) { - onError(error); - } else { - alert(`Failed to import file: ${error}`); - } - } - ); - }, + // NOTE: exportToFile and importFromFile have been removed + // Import/export is now handled by the workspace-level system + // See: workspaceStore.importDocumentFromFile() and workspaceStore.exportDocument() loadGraphState: (data) => set({ diff --git a/src/stores/persistence/fileIO.ts b/src/stores/persistence/fileIO.ts index 0ac9cf6..fadd283 100644 --- a/src/stores/persistence/fileIO.ts +++ b/src/stores/persistence/fileIO.ts @@ -1,7 +1,7 @@ import type { Actor, Relation, NodeTypeConfig, EdgeTypeConfig } from '../../types'; import type { ConstellationDocument } from './types'; import { createDocument, serializeActors, serializeRelations } from './saver'; -import { validateDocument, deserializeGraphState } from './loader'; +import { validateDocument } from './loader'; /** * File I/O - Export and import ConstellationDocument to/from files @@ -57,15 +57,11 @@ export function exportGraphToFile( /** * Import graph state from a JSON file + * Returns the full document with timeline preserved, not just the current graph state */ export function importGraphFromFile( file: File, - onSuccess: (data: { - nodes: Actor[]; - edges: Relation[]; - nodeTypes: NodeTypeConfig[]; - edgeTypes: EdgeTypeConfig[]; - }) => void, + onSuccess: (document: ConstellationDocument) => void, onError: (error: string) => void ): void { const reader = new FileReader(); @@ -80,14 +76,8 @@ export function importGraphFromFile( throw new Error('Invalid file format: File does not match expected Constellation Analyzer format'); } - // Deserialize the graph state - const graphState = deserializeGraphState(parsed as ConstellationDocument); - - if (!graphState) { - throw new Error('Failed to parse graph data from file'); - } - - onSuccess(graphState); + // Return the full document with timeline intact + onSuccess(parsed as ConstellationDocument); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred while importing file'; onError(errorMessage); @@ -103,14 +93,10 @@ export function importGraphFromFile( /** * Trigger file selection dialog for import + * Returns the full document with timeline preserved */ export function selectFileForImport( - onSuccess: (data: { - nodes: Actor[]; - edges: Relation[]; - nodeTypes: NodeTypeConfig[]; - edgeTypes: EdgeTypeConfig[]; - }) => void, + onSuccess: (document: ConstellationDocument) => void, onError: (error: string) => void ): void { const input = document.createElement('input'); diff --git a/src/stores/persistence/loader.ts b/src/stores/persistence/loader.ts index 73c5d0a..286c05d 100644 --- a/src/stores/persistence/loader.ts +++ b/src/stores/persistence/loader.ts @@ -184,12 +184,5 @@ export function loadGraphState(): { return deserializeGraphState(document); } -// Check if saved state exists -export function hasSavedState(): boolean { - return localStorage.getItem(STORAGE_KEYS.GRAPH_STATE) !== null; -} - -// Get last saved timestamp -export function getLastSavedTimestamp(): string | null { - return localStorage.getItem(STORAGE_KEYS.LAST_SAVED); -} +// NOTE: hasSavedState() and getLastSavedTimestamp() have been removed +// They were part of the legacy single-document system and are no longer needed diff --git a/src/stores/persistence/saver.ts b/src/stores/persistence/saver.ts index f4132c7..78fc931 100644 --- a/src/stores/persistence/saver.ts +++ b/src/stores/persistence/saver.ts @@ -99,8 +99,6 @@ export function saveDocument(document: ConstellationDocument): boolean { } } -// Clear saved state (legacy function) -export function clearSavedState(): void { - localStorage.removeItem(STORAGE_KEYS.GRAPH_STATE); - localStorage.removeItem(STORAGE_KEYS.LAST_SAVED); -} +// NOTE: clearSavedState() has been removed +// It was part of the legacy single-document system and is no longer needed +// Workspace clearing is now handled by clearWorkspaceStorage() in workspace/persistence.ts diff --git a/src/stores/workspaceStore.ts b/src/stores/workspaceStore.ts index c464968..352a9ab 100644 --- a/src/stores/workspaceStore.ts +++ b/src/stores/workspaceStore.ts @@ -1,7 +1,7 @@ import { create } from 'zustand'; import type { ConstellationDocument } from './persistence/types'; import type { Workspace, WorkspaceActions, DocumentMetadata, WorkspaceSettings } from './workspace/types'; -import { createDocument as createDocumentHelper, serializeActors, serializeRelations } from './persistence/saver'; +import { createDocument as createDocumentHelper } from './persistence/saver'; import { selectFileForImport, exportDocumentToFile } from './persistence/fileIO'; import { generateWorkspaceId, @@ -547,26 +547,19 @@ export const useWorkspaceStore = create((set, get) importDocumentFromFile: async () => { return new Promise((resolve) => { selectFileForImport( - (data) => { + (importedDoc) => { const documentId = generateDocumentId(); const now = new Date().toISOString(); - // Serialize actors and relations for storage - const serializedNodes = serializeActors(data.nodes); - const serializedEdges = serializeRelations(data.edges); - - const importedDoc = createDocumentHelper( - serializedNodes, - serializedEdges, - data.nodeTypes, - data.edgeTypes - ); + // Use the imported document as-is, preserving the complete timeline structure + // Just update the IDs and metadata for the new workspace context importedDoc.metadata.documentId = documentId; - importedDoc.metadata.title = 'Imported Analysis'; + importedDoc.metadata.title = importedDoc.metadata.title || 'Imported Analysis'; + importedDoc.metadata.updatedAt = now; const metadata: DocumentMetadata = { id: documentId, - title: 'Imported Analysis', + title: importedDoc.metadata.title || 'Imported Analysis', isDirty: false, lastModified: now, }; @@ -575,6 +568,7 @@ export const useWorkspaceStore = create((set, get) saveDocumentMetadata(documentId, metadata); // Load the timeline from the imported document into timelineStore + // This preserves all timeline states, not just the current one useTimelineStore.getState().loadTimeline(documentId, importedDoc.timeline as unknown as Timeline); set((state) => { diff --git a/src/types/index.ts b/src/types/index.ts index 647abf5..c7f3b8a 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -78,8 +78,8 @@ export interface GraphActions { setEdges: (edges: Relation[]) => void; setNodeTypes: (nodeTypes: NodeTypeConfig[]) => void; setEdgeTypes: (edgeTypes: EdgeTypeConfig[]) => void; - exportToFile: () => void; - importFromFile: (onError?: (error: string) => void) => void; + // NOTE: exportToFile and importFromFile have been removed + // Import/export is now handled by the workspace-level system (workspaceStore) loadGraphState: (data: { nodes: Actor[]; edges: Relation[]; nodeTypes: NodeTypeConfig[]; edgeTypes: EdgeTypeConfig[] }) => void; }