feature: loading graph structure
This commit is contained in:
16
src/App.tsx
16
src/App.tsx
@@ -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 />}
|
||||||
|
|||||||
@@ -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
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[];
|
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
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