fix: preserve timeline states in document import/export

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 <noreply@anthropic.com>
This commit is contained in:
Jan-Henrik Bruhn 2025-10-12 11:11:32 +02:00
parent d775cb8863
commit aa2bd7e5d7
7 changed files with 29 additions and 82 deletions

View file

@ -24,7 +24,7 @@ import type { Actor, Relation, NodeTypeConfig, EdgeTypeConfig, RelationData } fr
* Read-only pass-through operations (no history): * Read-only pass-through operations (no history):
* - setNodes, setEdges (used for bulk updates during undo/redo/document loading) * - setNodes, setEdges (used for bulk updates during undo/redo/document loading)
* - nodes, edges, nodeTypes, edgeTypes (state access) * - nodes, edges, nodeTypes, edgeTypes (state access)
* - exportToFile, importFromFile, loadGraphState * - loadGraphState
* *
* Usage: * Usage:
* const { addNode, updateNode, deleteNode, ... } = useGraphWithHistory(); * const { addNode, updateNode, deleteNode, ... } = useGraphWithHistory();
@ -278,10 +278,11 @@ export function useGraphWithHistory() {
setEdges: graphStore.setEdges, setEdges: graphStore.setEdges,
setNodeTypes: graphStore.setNodeTypes, setNodeTypes: graphStore.setNodeTypes,
setEdgeTypes: graphStore.setEdgeTypes, setEdgeTypes: graphStore.setEdgeTypes,
exportToFile: graphStore.exportToFile,
importFromFile: graphStore.importFromFile,
loadGraphState: graphStore.loadGraphState, 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 // Expose flag for detecting restore operations
isRestoringRef, isRestoringRef,
}; };

View file

@ -9,7 +9,6 @@ import type {
GraphActions GraphActions
} from '../types'; } from '../types';
import { loadGraphState } from './persistence/loader'; import { loadGraphState } from './persistence/loader';
import { exportGraphToFile, selectFileForImport } from './persistence/fileIO';
/** /**
* IMPORTANT: DO NOT USE THIS STORE DIRECTLY IN COMPONENTS * IMPORTANT: DO NOT USE THIS STORE DIRECTLY IN COMPONENTS
@ -187,33 +186,9 @@ export const useGraphStore = create<GraphStore & GraphActions>((set) => ({
edgeTypes, edgeTypes,
}), }),
// File import/export operations // NOTE: exportToFile and importFromFile have been removed
exportToFile: () => { // Import/export is now handled by the workspace-level system
const state = useGraphStore.getState(); // See: workspaceStore.importDocumentFromFile() and workspaceStore.exportDocument()
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}`);
}
}
);
},
loadGraphState: (data) => loadGraphState: (data) =>
set({ set({

View file

@ -1,7 +1,7 @@
import type { Actor, Relation, NodeTypeConfig, EdgeTypeConfig } from '../../types'; import type { Actor, Relation, NodeTypeConfig, EdgeTypeConfig } from '../../types';
import type { ConstellationDocument } from './types'; import type { ConstellationDocument } from './types';
import { createDocument, serializeActors, serializeRelations } from './saver'; 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 * File I/O - Export and import ConstellationDocument to/from files
@ -57,15 +57,11 @@ export function exportGraphToFile(
/** /**
* Import graph state from a JSON file * Import graph state from a JSON file
* Returns the full document with timeline preserved, not just the current graph state
*/ */
export function importGraphFromFile( export function importGraphFromFile(
file: File, file: File,
onSuccess: (data: { onSuccess: (document: ConstellationDocument) => void,
nodes: Actor[];
edges: Relation[];
nodeTypes: NodeTypeConfig[];
edgeTypes: EdgeTypeConfig[];
}) => void,
onError: (error: string) => void onError: (error: string) => void
): void { ): void {
const reader = new FileReader(); 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'); throw new Error('Invalid file format: File does not match expected Constellation Analyzer format');
} }
// Deserialize the graph state // Return the full document with timeline intact
const graphState = deserializeGraphState(parsed as ConstellationDocument); onSuccess(parsed as ConstellationDocument);
if (!graphState) {
throw new Error('Failed to parse graph data from file');
}
onSuccess(graphState);
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred while importing file'; const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred while importing file';
onError(errorMessage); onError(errorMessage);
@ -103,14 +93,10 @@ export function importGraphFromFile(
/** /**
* Trigger file selection dialog for import * Trigger file selection dialog for import
* Returns the full document with timeline preserved
*/ */
export function selectFileForImport( export function selectFileForImport(
onSuccess: (data: { onSuccess: (document: ConstellationDocument) => void,
nodes: Actor[];
edges: Relation[];
nodeTypes: NodeTypeConfig[];
edgeTypes: EdgeTypeConfig[];
}) => void,
onError: (error: string) => void onError: (error: string) => void
): void { ): void {
const input = document.createElement('input'); const input = document.createElement('input');

View file

@ -184,12 +184,5 @@ export function loadGraphState(): {
return deserializeGraphState(document); return deserializeGraphState(document);
} }
// Check if saved state exists // NOTE: hasSavedState() and getLastSavedTimestamp() have been removed
export function hasSavedState(): boolean { // They were part of the legacy single-document system and are no longer needed
return localStorage.getItem(STORAGE_KEYS.GRAPH_STATE) !== null;
}
// Get last saved timestamp
export function getLastSavedTimestamp(): string | null {
return localStorage.getItem(STORAGE_KEYS.LAST_SAVED);
}

View file

@ -99,8 +99,6 @@ export function saveDocument(document: ConstellationDocument): boolean {
} }
} }
// Clear saved state (legacy function) // NOTE: clearSavedState() has been removed
export function clearSavedState(): void { // It was part of the legacy single-document system and is no longer needed
localStorage.removeItem(STORAGE_KEYS.GRAPH_STATE); // Workspace clearing is now handled by clearWorkspaceStorage() in workspace/persistence.ts
localStorage.removeItem(STORAGE_KEYS.LAST_SAVED);
}

View file

@ -1,7 +1,7 @@
import { create } from 'zustand'; import { create } from 'zustand';
import type { ConstellationDocument } from './persistence/types'; import type { ConstellationDocument } from './persistence/types';
import type { Workspace, WorkspaceActions, DocumentMetadata, WorkspaceSettings } from './workspace/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 { selectFileForImport, exportDocumentToFile } from './persistence/fileIO';
import { import {
generateWorkspaceId, generateWorkspaceId,
@ -547,26 +547,19 @@ export const useWorkspaceStore = create<Workspace & WorkspaceActions>((set, get)
importDocumentFromFile: async () => { importDocumentFromFile: async () => {
return new Promise((resolve) => { return new Promise((resolve) => {
selectFileForImport( selectFileForImport(
(data) => { (importedDoc) => {
const documentId = generateDocumentId(); const documentId = generateDocumentId();
const now = new Date().toISOString(); const now = new Date().toISOString();
// Serialize actors and relations for storage // Use the imported document as-is, preserving the complete timeline structure
const serializedNodes = serializeActors(data.nodes); // Just update the IDs and metadata for the new workspace context
const serializedEdges = serializeRelations(data.edges);
const importedDoc = createDocumentHelper(
serializedNodes,
serializedEdges,
data.nodeTypes,
data.edgeTypes
);
importedDoc.metadata.documentId = documentId; importedDoc.metadata.documentId = documentId;
importedDoc.metadata.title = 'Imported Analysis'; importedDoc.metadata.title = importedDoc.metadata.title || 'Imported Analysis';
importedDoc.metadata.updatedAt = now;
const metadata: DocumentMetadata = { const metadata: DocumentMetadata = {
id: documentId, id: documentId,
title: 'Imported Analysis', title: importedDoc.metadata.title || 'Imported Analysis',
isDirty: false, isDirty: false,
lastModified: now, lastModified: now,
}; };
@ -575,6 +568,7 @@ export const useWorkspaceStore = create<Workspace & WorkspaceActions>((set, get)
saveDocumentMetadata(documentId, metadata); saveDocumentMetadata(documentId, metadata);
// Load the timeline from the imported document into timelineStore // 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); useTimelineStore.getState().loadTimeline(documentId, importedDoc.timeline as unknown as Timeline);
set((state) => { set((state) => {

View file

@ -78,8 +78,8 @@ export interface GraphActions {
setEdges: (edges: Relation[]) => void; setEdges: (edges: Relation[]) => void;
setNodeTypes: (nodeTypes: NodeTypeConfig[]) => void; setNodeTypes: (nodeTypes: NodeTypeConfig[]) => void;
setEdgeTypes: (edgeTypes: EdgeTypeConfig[]) => void; setEdgeTypes: (edgeTypes: EdgeTypeConfig[]) => void;
exportToFile: () => void; // NOTE: exportToFile and importFromFile have been removed
importFromFile: (onError?: (error: string) => void) => void; // Import/export is now handled by the workspace-level system (workspaceStore)
loadGraphState: (data: { nodes: Actor[]; edges: Relation[]; nodeTypes: NodeTypeConfig[]; edgeTypes: EdgeTypeConfig[] }) => void; loadGraphState: (data: { nodes: Actor[]; edges: Relation[]; nodeTypes: NodeTypeConfig[]; edgeTypes: EdgeTypeConfig[] }) => void;
} }