refactor node context menu III, added zustand

This commit is contained in:
2025-11-11 01:25:59 +01:00
parent 8e630839a0
commit cd4963a4bd
7 changed files with 124 additions and 139 deletions

View File

@@ -1,13 +1,13 @@
import { useEffect, useRef, useState } from "react";
import { createContext, useEffect, useRef, useState } from "react";
import Viz from 'viz.js';
import { Module, render } from 'viz.js/full.render.js';
import * as d3 from 'd3';
import { graphToDot } from "../Graphviz";
import { type MenuProps } from "antd";
import type { BreadcrumbItemType } from "antd/es/breadcrumb/Breadcrumb";
import { cloneDeep } from "lodash";
import NodeContextMenu from "./NodeContextMenu";
import NodeRenameModal from "./NodeRenameModal";
import { useGraphsStore } from "./GraphsCollection";
export class GraphModel {
nodes: NodeModel[] = [];
@@ -42,8 +42,13 @@ export interface NodeContext {
nodeId: string;
nodeName?: string;
coords: { x: number, y: number };
}
export interface GraphContext {
graphId: string;
selectGraphId: React.Dispatch<React.SetStateAction<string>>;
graph: GraphModel;
setGraph: React.Dispatch<React.SetStateAction<GraphModel>>
setGraph: React.Dispatch<React.SetStateAction<GraphModel>>;
}
export interface OpenNodeContext {
@@ -52,42 +57,24 @@ export interface OpenNodeContext {
}
const viz = new Viz({ Module, render });
export const graphContext = createContext<GraphContext | null>(null);
const items: MenuProps['items'] = [
{
key: 'rename',
label: 'Rename',
extra: 'ctrl + n',
},
{
key: 'subgraph',
label: 'Subgraph',
extra: 'ctrl + s',
},
{
key: 'remove',
label: 'Remove',
extra: 'ctrl + r'
}
]
const graphsById = new Map<string, GraphModel>();
export default function Graph({ setGraphPath }) {
export default function Graph({ setGraphPath }: { setGraphPath: React.Dispatch<React.SetStateAction<BreadcrumbItemType[]>> }) {
const containerRef = useRef(null);
const [graph, setGraph] = useState(defaultGraph());
const [contextMenuOpened, openContextMenu] = useState(false);
const [renameModalOpened, openRenameModal] = useState(false);
const [coords, setCoords] = useState({ x: 0, y: 0 });
const [nodeId, selectNodeId] = useState('');
const [nodeName, setSelectedNodeName] = useState('');
const [renameModalOpened, openRenameModal] = useState(false);
const [graphId, selectGraphId] = useState('main');
const [graphsPath, setGraphsPath] = useState([createPathSegment('main', 'Main')])
const [nodeContext, openNodeContext] = useState<null | NodeContext>(null)
const [nodeContextMenuOpened, openNodeContextMenu] = useState<OpenNodeContext>({ nodeContext: undefined, opened: false })
const [nodeContext, openNodeContext] = useState<null | NodeContext>(null);
const graphContextValue = {
graphId: graphId,
selectGraphId: selectGraphId,
graph: graph,
setGraph: setGraph
};
useEffect(() => {
console.info(graphsPath);
setGraphPath(graphsPath);
}, [graphsPath])
@@ -102,6 +89,19 @@ export default function Graph({ setGraphPath }) {
}
}, [nodeContext]);
useEffect(() => {
if (nodeContext?.nodeId === graphId) {
graphsPath.push(createPathSegment(nodeContext.nodeId, nodeContext.nodeName));
setGraphsPath([...graphsPath])
}
const state = useGraphsStore.getState();
const graphsById = state.graphsById;
const graph = graphsById.get(graphId);
if (graph) {
setGraph(graph);
}
}, [graphId]);
async function renderGraph() {
const dot = graphToDot(graph);
@@ -135,13 +135,10 @@ export default function Graph({ setGraphPath }) {
}
event.preventDefault();
console.info(node.label);
openNodeContext({
nodeId: id,
nodeName: node.label,
coords: { x: event.clientX, y: event.clientY },
setGraph: setGraph,
graph: graph
})
});
}
@@ -151,90 +148,21 @@ export default function Graph({ setGraphPath }) {
setGraph(prev => ({ ...prev, nodes: [...prev.nodes, { id, label: 'New node' }], edges: [...prev.edges, { from: parentId, to: id }] }));
}
function removeNode(id: string) {
setGraph(prev => ({ ...prev, nodes: [...prev.nodes.filter(n => n.id !== nodeId)], edges: removeEdgesOfNode(id, prev.edges) }));
}
function removeEdgesOfNode(nodeId: string, edges: EdgeModel[]): EdgeModel[] {
const parentConnection = edges.find(e => e.to === nodeId);
if (parentConnection) {
edges.filter(e => e.from === nodeId).forEach(e => {
e.from = parentConnection.from;
});
edges = edges.filter(e => e.to !== nodeId);
}
if (!parentConnection) {
edges = edges.filter(e => e.from !== nodeId);
}
const result = [...edges];
return result;
}
function renameNode() {
const node = graph.nodes.find(n => n.id === nodeId);
if (!node) {
return;
}
node.label = nodeName;
setGraph(prev => ({ ...prev, nodes: graph.nodes }));
openRenameModal(false);
}
const onMenuClick: MenuProps['onClick'] = ({ key }) => {
switch (key) {
case 'rename': {
openRenameModal(true);
break;
}
case 'remove': {
removeNode(nodeId);
break;
}
case 'subgraph': {
const selectedNode = graph.nodes.find(n => n.id === nodeId);
const selectedNodeName = selectedNode?.label;
graphsById.set(graphId, cloneDeep(graph));
selectGraphId(nodeId);
let selectedGraph = graphsById.get(nodeId);
if (!selectedGraph) {
selectedGraph = defaultGraph();
}
setGraph(selectedGraph);
renderGraph();
graphsPath.push(createPathSegment(nodeId, selectedNodeName));
setGraphsPath([...graphsPath])
break;
}
}
};
function createPathSegment(pathSegmentId: string, selectedNodeName: string | undefined): BreadcrumbItemType {
function createPathSegment(
pathSegmentId: string,
selectedNodeName: string | undefined)
: BreadcrumbItemType {
return {
title: selectedNodeName,
key: pathSegmentId,
onClick: (e) => {
console.info(e);
onClick: () => {
const index = graphsPath.findIndex(p => p.key === pathSegmentId);
setGraphsPath(prev => {
prev.splice(index + 1);
return [...prev];
});
const graph = graphsById.get(pathSegmentId);
if (graph) {
selectGraphId(pathSegmentId);
setGraph(graph);
renderGraph();
} else {
throw Error(`No graph with id '${pathSegmentId}'`);
}
selectGraphId(pathSegmentId);
}
} as BreadcrumbItemType;
}
@@ -244,22 +172,24 @@ export default function Graph({ setGraphPath }) {
<div ref={containerRef} className="w-full h-full bg-white rounded shadow" style={{ minHeight: '600px', overflow: 'auto' }}>
</div>
<NodeContextMenu
nodeContext={nodeContext!}
contextMenuOpened={contextMenuOpened}
openContextMenu={openContextMenu}
openRenameModal={openRenameModal}>
</NodeContextMenu>
<NodeRenameModal
nodeContext={nodeContext!}
renameModalOpened={renameModalOpened}
openRenameModal={openRenameModal}>
</NodeRenameModal>
<graphContext.Provider value={graphContextValue}>
<NodeContextMenu
nodeContext={nodeContext!}
contextMenuOpened={contextMenuOpened}
openContextMenu={openContextMenu}
openRenameModal={openRenameModal}>
</NodeContextMenu>
<NodeRenameModal
nodeContext={nodeContext!}
renameModalOpened={renameModalOpened}
openRenameModal={openRenameModal}>
</NodeRenameModal>
</graphContext.Provider>
</div>
)
}
function defaultGraph(): GraphModel {
export function defaultGraph(): GraphModel {
const start = crypto.randomUUID();
const end = crypto.randomUUID();