feature: loading graph structure

This commit is contained in:
2026-03-06 14:31:51 +01:00
parent dcdd4d621e
commit 0af50e165a
5 changed files with 112 additions and 2 deletions

View File

@@ -7,8 +7,10 @@ import {
MenuFoldOutlined, MenuFoldOutlined,
MenuUnfoldOutlined, MenuUnfoldOutlined,
SaveOutlined, SaveOutlined,
FolderOpenOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { saveConceptSketch } from './utils/saveGraph'; import { saveConceptSketch } from './utils/saveGraph';
import { loadConceptSketch } from './utils/loadGraph';
import { useGraphLayersTreeStore } from './stores/TreeStore'; import { useGraphLayersTreeStore } from './stores/TreeStore';
const { Header, Content, Sider } = Layout; const { Header, Content, Sider } = Layout;
@@ -72,7 +74,19 @@ const App: React.FC = () => {
</div> </div>
<Breadcrumb items={graphLevel} /> <Breadcrumb items={graphLevel} />
</Space> </Space>
<div style={{ background: '#001529' }}> <div style={{ background: '#001529', display: 'flex' }}>
<Button
type="text"
icon={<FolderOpenOutlined />}
onClick={loadConceptSketch}
title="Load JSON"
style={{
fontSize: '16px',
width: 32,
height: 32,
color: colorBgContainer,
}}
/>
<Button <Button
type="text" type="text"
icon={<SaveOutlined />} icon={<SaveOutlined />}

View File

@@ -8,6 +8,7 @@ import NodeContextMenu from "./NodeContextMenu";
import NodeRenameModal from "./NodeRenameModal"; import NodeRenameModal from "./NodeRenameModal";
import { useGraphsStore } from "../stores/GraphsStore"; import { useGraphsStore } from "../stores/GraphsStore";
import { useKeysdownStore, useSelectedNodesStore } from "../stores/ArrayStore"; import { useKeysdownStore, useSelectedNodesStore } from "../stores/ArrayStore";
import { useLoadStore } from "../stores/LoadStore";
export class GraphModel { export class GraphModel {
@@ -85,6 +86,7 @@ export default function Graph({ setGraphPath }: { setGraphPath: React.Dispatch<R
const isNodeSelected = useSelectedNodesStore(store => store.has); const isNodeSelected = useSelectedNodesStore(store => store.has);
const anyNodeSelected = useSelectedNodesStore(store => store.hasAny); const anyNodeSelected = useSelectedNodesStore(store => store.hasAny);
const deselectAllNodes = useSelectedNodesStore(store => store.clear); const deselectAllNodes = useSelectedNodesStore(store => store.clear);
const loadCount = useLoadStore(state => state.loadCount);
useEffect(() => { useEffect(() => {
setGraphPath(graphsPath); setGraphPath(graphsPath);
@@ -110,6 +112,18 @@ export default function Graph({ setGraphPath }: { setGraphPath: React.Dispatch<R
} }
}, [nodeContext]); }, [nodeContext]);
useEffect(() => {
if (loadCount === 0) return;
const state = useGraphsStore.getState() as { graphsById: Map<string, GraphModel> };
const mainGraph = state.graphsById.get('main');
if (mainGraph) setGraph(mainGraph);
selectGraphId('main');
setGraphsPath([createPathSegment('main', 'Main')]);
openNodeContext(null);
openContextMenu(false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [loadCount]);
useEffect(() => { useEffect(() => {
if (nodeContext?.nodeId === graphId) { if (nodeContext?.nodeId === graphId) {
graphsPath.push(createPathSegment(nodeContext.nodeId, nodeContext.nodeName)); graphsPath.push(createPathSegment(nodeContext.nodeId, nodeContext.nodeName));

11
src/stores/LoadStore.ts Normal file
View File

@@ -0,0 +1,11 @@
import { create } from 'zustand';
interface LoadStore {
loadCount: number;
signal: () => void;
}
export const useLoadStore = create<LoadStore>((set) => ({
loadCount: 0,
signal: () => set((state) => ({ loadCount: state.loadCount + 1 })),
}));

View File

@@ -10,6 +10,7 @@ export interface TreeStore {
tree: TreeDataNode[]; tree: TreeDataNode[];
add: (childNode: NodeContext, parentNodeId: string | undefined) => void; add: (childNode: NodeContext, parentNodeId: string | undefined) => void;
remove: (nodeId: string) => void; remove: (nodeId: string) => void;
reset: () => void;
} }
export const useGraphLayersTreeStore = create<TreeStore>()((set) => ({ export const useGraphLayersTreeStore = create<TreeStore>()((set) => ({
@@ -87,7 +88,13 @@ export const useGraphLayersTreeStore = create<TreeStore>()((set) => ({
parentIdByChildId: state.parentIdByChildId, parentIdByChildId: state.parentIdByChildId,
tree: createTree([...state.rootNodes], nodesFlatById) tree: createTree([...state.rootNodes], nodesFlatById)
} }
}) }),
reset: () => set({
nodesFlatById: new Map<React.Key, TreeDataNode>(),
parentIdByChildId: new Map<React.Key, string>(),
rootNodes: [],
tree: [],
}),
})); }));
export function createTree(nodes: TreeDataNode[], nodesFlatById: Map<React.Key, TreeDataNode>): TreeDataNode[] { export function createTree(nodes: TreeDataNode[], nodesFlatById: Map<React.Key, TreeDataNode>): TreeDataNode[] {

64
src/utils/loadGraph.ts Normal file
View File

@@ -0,0 +1,64 @@
import type { GraphModel, NodeContext } from '../components/Graph';
import { useGraphsStore } from '../stores/GraphsStore';
import { useGraphLayersTreeStore } from '../stores/TreeStore';
import { useLoadStore } from '../stores/LoadStore';
interface SavedEdge { id: string; from: string; to: string; }
interface SavedNode { id: string; label?: string; subgraph?: SavedGraph; }
interface SavedGraph { id: string; nodes: SavedNode[]; edges: SavedEdge[]; }
function populateStores(
graph: SavedGraph,
graphsById: Map<string, GraphModel>,
treeAdd: (node: NodeContext, parentId: string | undefined) => void,
): void {
graphsById.set(graph.id, {
nodes: graph.nodes.map(n => ({ id: n.id, label: n.label })),
edges: graph.edges.map(e => ({ id: e.id, from: e.from, to: e.to })),
});
for (const node of graph.nodes) {
if (node.subgraph) {
const nodeContext: NodeContext = {
nodeId: node.id,
nodeName: node.label,
coords: { x: 0, y: 0 },
};
treeAdd(nodeContext, graph.id === 'main' ? undefined : graph.id);
populateStores(node.subgraph, graphsById, treeAdd);
}
}
}
export function loadConceptSketch(): void {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json,application/json';
input.onchange = () => {
const file = input.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target?.result as string) as SavedGraph;
if (!data.id || !Array.isArray(data.nodes) || !Array.isArray(data.edges)) {
throw new Error('Invalid concept-sketch JSON format');
}
const graphsState = useGraphsStore.getState() as { graphsById: Map<string, GraphModel> };
graphsState.graphsById.clear();
useGraphLayersTreeStore.getState().reset();
populateStores(data, graphsState.graphsById, useGraphLayersTreeStore.getState().add);
useLoadStore.getState().signal();
} catch (err) {
console.error('Failed to load concept sketch:', err);
}
};
reader.readAsText(file);
};
input.click();
}