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):
* - 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,
};

View file

@ -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<GraphStore & GraphActions>((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({

View file

@ -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');

View file

@ -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

View file

@ -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

View file

@ -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<Workspace & WorkspaceActions>((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<Workspace & WorkspaceActions>((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) => {

View file

@ -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;
}