feature: loading graph structure
This commit is contained in:
16
src/App.tsx
16
src/App.tsx
@@ -7,8 +7,10 @@ import {
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
SaveOutlined,
|
||||
FolderOpenOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { saveConceptSketch } from './utils/saveGraph';
|
||||
import { loadConceptSketch } from './utils/loadGraph';
|
||||
import { useGraphLayersTreeStore } from './stores/TreeStore';
|
||||
|
||||
const { Header, Content, Sider } = Layout;
|
||||
@@ -72,7 +74,19 @@ const App: React.FC = () => {
|
||||
</div>
|
||||
<Breadcrumb items={graphLevel} />
|
||||
</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
|
||||
type="text"
|
||||
icon={<SaveOutlined />}
|
||||
|
||||
@@ -8,6 +8,7 @@ import NodeContextMenu from "./NodeContextMenu";
|
||||
import NodeRenameModal from "./NodeRenameModal";
|
||||
import { useGraphsStore } from "../stores/GraphsStore";
|
||||
import { useKeysdownStore, useSelectedNodesStore } from "../stores/ArrayStore";
|
||||
import { useLoadStore } from "../stores/LoadStore";
|
||||
|
||||
|
||||
export class GraphModel {
|
||||
@@ -85,6 +86,7 @@ export default function Graph({ setGraphPath }: { setGraphPath: React.Dispatch<R
|
||||
const isNodeSelected = useSelectedNodesStore(store => store.has);
|
||||
const anyNodeSelected = useSelectedNodesStore(store => store.hasAny);
|
||||
const deselectAllNodes = useSelectedNodesStore(store => store.clear);
|
||||
const loadCount = useLoadStore(state => state.loadCount);
|
||||
|
||||
useEffect(() => {
|
||||
setGraphPath(graphsPath);
|
||||
@@ -110,6 +112,18 @@ export default function Graph({ setGraphPath }: { setGraphPath: React.Dispatch<R
|
||||
}
|
||||
}, [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(() => {
|
||||
if (nodeContext?.nodeId === graphId) {
|
||||
graphsPath.push(createPathSegment(nodeContext.nodeId, nodeContext.nodeName));
|
||||
|
||||
11
src/stores/LoadStore.ts
Normal file
11
src/stores/LoadStore.ts
Normal 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 })),
|
||||
}));
|
||||
@@ -10,6 +10,7 @@ export interface TreeStore {
|
||||
tree: TreeDataNode[];
|
||||
add: (childNode: NodeContext, parentNodeId: string | undefined) => void;
|
||||
remove: (nodeId: string) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
export const useGraphLayersTreeStore = create<TreeStore>()((set) => ({
|
||||
@@ -87,7 +88,13 @@ export const useGraphLayersTreeStore = create<TreeStore>()((set) => ({
|
||||
parentIdByChildId: state.parentIdByChildId,
|
||||
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[] {
|
||||
|
||||
64
src/utils/loadGraph.ts
Normal file
64
src/utils/loadGraph.ts
Normal 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();
|
||||
}
|
||||
Reference in New Issue
Block a user