mirror of
https://github.com/OFFIS-ESC/constellation-analyzer
synced 2026-01-27 07:43:41 +00:00
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:
parent
d775cb8863
commit
aa2bd7e5d7
7 changed files with 29 additions and 82 deletions
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue