constellation-analyzer/src/stores/persistence/saver.ts
Jan-Henrik Bruhn d98acf963b feat: implement label system and redesign filtering with positive filters
Implements a comprehensive label system and completely redesigns all
filtering (labels, actor types, relation types) to use intuitive positive
filtering where empty selection shows all items.

Label System Features:
- Create, edit, delete labels with names, colors, and scope (actors/relations/both)
- Inline editing with click-to-edit UI for quick modifications
- Quick-add label forms in config modals
- Autocomplete label selector with inline label creation
- Label badges rendered on nodes and edges (no overflow limits)
- Full undo/redo support for label operations
- Label validation and cleanup when labels are deleted
- Labels stored per-document in workspace system

Filter System Redesign:
- Changed from negative to positive filtering for all filter types
- Empty selection = show all items (intuitive default)
- Selected items = show only those items (positive filter)
- Consistent behavior across actor types, relation types, and labels
- Clear visual feedback with selection counts and helper text
- Auto-zoom viewport adjustment works for all filter types including labels

Label Cleanup & Validation:
- When label deleted, automatically removed from all nodes/edges across all timeline states
- Label references validated during node/edge updates
- Unknown label IDs filtered out to maintain data integrity

UI Improvements:
- All labels rendered without overflow limits (removed +N more indicators)
- Filter checkboxes start unchecked (select to filter, not hide)
- Helper text explains current filter state
- Selection counts displayed in filter section headers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 10:40:00 +02:00

106 lines
3.3 KiB
TypeScript

import type { ConstellationDocument, SerializedActor, SerializedRelation } from './types';
import type { Actor, Relation, NodeTypeConfig, EdgeTypeConfig, LabelConfig } from '../../types';
import { STORAGE_KEYS, SCHEMA_VERSION, APP_NAME } from './constants';
/**
* Saver - Handles serialization and saving to localStorage
*/
// Serialize actors for storage (strip React Flow internals)
export function serializeActors(actors: Actor[]): SerializedActor[] {
return actors.map(actor => ({
id: actor.id,
type: actor.type || 'custom', // Default to 'custom' if undefined
position: actor.position,
data: actor.data,
}));
}
// Serialize relations for storage (strip React Flow internals)
export function serializeRelations(relations: Relation[]): SerializedRelation[] {
return relations.map(relation => ({
id: relation.id,
source: relation.source,
target: relation.target,
type: relation.type,
data: relation.data,
sourceHandle: relation.sourceHandle,
targetHandle: relation.targetHandle,
}));
}
// Generate unique state ID
function generateStateId(): string {
return `state_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
// Create a complete document from graph data
// Creates a document with a single initial timeline state containing the provided graph
export function createDocument(
nodes: SerializedActor[],
edges: SerializedRelation[],
nodeTypes: NodeTypeConfig[],
edgeTypes: EdgeTypeConfig[],
labels?: LabelConfig[],
existingDocument?: ConstellationDocument
): ConstellationDocument {
const now = new Date().toISOString();
const rootStateId = generateStateId();
// Create the initial timeline state with the provided graph (nodes and edges only)
const initialState = {
id: rootStateId,
label: 'Initial State',
parentStateId: undefined,
graph: {
nodes,
edges,
},
createdAt: now,
updatedAt: now,
};
// Create document with global types, labels, and timeline containing the initial state
return {
metadata: {
version: SCHEMA_VERSION,
appName: APP_NAME,
createdAt: existingDocument?.metadata?.createdAt || now,
updatedAt: now,
lastSavedBy: 'browser',
},
nodeTypes,
edgeTypes,
labels: labels || [],
timeline: {
states: {
[rootStateId]: initialState,
},
currentStateId: rootStateId,
rootStateId: rootStateId,
},
};
}
// Save document to localStorage (legacy function for old single-document system)
// NOTE: This is only used for migration purposes. Workspace documents are saved
// via workspace/persistence.ts
export function saveDocument(document: ConstellationDocument): boolean {
try {
const json = JSON.stringify(document);
localStorage.setItem(STORAGE_KEYS.GRAPH_STATE, json);
localStorage.setItem(STORAGE_KEYS.LAST_SAVED, document.metadata.updatedAt);
return true;
} catch (error) {
if (error instanceof Error && error.name === 'QuotaExceededError') {
console.error('Storage quota exceeded');
} else {
console.error('Failed to save document:', error);
}
return false;
}
}
// 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