144 lines
4.7 KiB
TypeScript
144 lines
4.7 KiB
TypeScript
![]() |
import { useCallback, useMemo } from "react";
|
||
|
import { nanoid } from 'nanoid';
|
||
|
import { shallow } from 'zustand/shallow';
|
||
|
import { throttle } from 'lodash';
|
||
|
import { Edge, Node, useReactFlow } from "@xyflow/react";
|
||
|
import { GraphState } from "./types";
|
||
|
import useGraphStore from "./store";
|
||
|
|
||
|
// Store selector
|
||
|
const selector = (store: GraphState) => ({
|
||
|
nodes: store.present.nodes,
|
||
|
edges: store.present.edges,
|
||
|
setNodes: store.setNodes,
|
||
|
setEdges: store.setEdges,
|
||
|
record: store.record
|
||
|
});
|
||
|
|
||
|
// Helper functions
|
||
|
const createNode = (label: string): Node => ({
|
||
|
id: nanoid(6),
|
||
|
type: 'graph-node',
|
||
|
data: { label },
|
||
|
position: { x: 0, y: 0 },
|
||
|
});
|
||
|
|
||
|
const createEdge = (source: string, target: string): Edge => ({
|
||
|
id: nanoid(6),
|
||
|
source,
|
||
|
target,
|
||
|
type: 'graph-edge',
|
||
|
});
|
||
|
|
||
|
export function useGraphOperation() {
|
||
|
const store = useGraphStore(selector, shallow);
|
||
|
const { addEdges, addNodes } = useReactFlow();
|
||
|
|
||
|
const selectedNodes = useMemo(() =>
|
||
|
store.nodes.filter(node => node.selected),
|
||
|
[store.nodes]
|
||
|
);
|
||
|
|
||
|
// Find parent node ID for a given node
|
||
|
const findParentId = useCallback((nodeId: string) => {
|
||
|
const parentEdge = store.edges.find(edge => edge.target === nodeId);
|
||
|
return parentEdge?.source;
|
||
|
}, [store.edges]);
|
||
|
|
||
|
// Update node selection
|
||
|
const updateNodeSelection = useCallback((nodeIds: string[]) => {
|
||
|
return store.nodes.map(node => ({
|
||
|
...node,
|
||
|
selected: nodeIds.includes(node.id)
|
||
|
}));
|
||
|
}, [store.nodes]);
|
||
|
|
||
|
// Create new node and connect it
|
||
|
const createConnectedNode = useCallback((parentId: string, deselectOthers = true) => {
|
||
|
const newNode = createNode(`新节点${store.nodes.length}`);
|
||
|
const newEdge = createEdge(parentId, newNode.id);
|
||
|
|
||
|
store.record(() => {
|
||
|
addNodes({ ...newNode, selected: true });
|
||
|
addEdges(newEdge);
|
||
|
|
||
|
if (deselectOthers) {
|
||
|
store.setNodes(updateNodeSelection([newNode.id]));
|
||
|
}
|
||
|
});
|
||
|
}, [store, addNodes, addEdges, updateNodeSelection]);
|
||
|
|
||
|
// Handle node creation operations
|
||
|
const handleCreateChildNodes = useCallback(() => {
|
||
|
if (selectedNodes.length === 0) return;
|
||
|
|
||
|
throttle(() => {
|
||
|
selectedNodes.forEach(node => {
|
||
|
if (node.id) createConnectedNode(node.id);
|
||
|
});
|
||
|
}, 300)();
|
||
|
}, [selectedNodes, createConnectedNode]);
|
||
|
|
||
|
const handleCreateSiblingNodes = useCallback(() => {
|
||
|
if (selectedNodes.length === 0) return;
|
||
|
|
||
|
throttle(() => {
|
||
|
selectedNodes.forEach(node => {
|
||
|
const parentId = findParentId(node.id) || node.id;
|
||
|
createConnectedNode(parentId);
|
||
|
});
|
||
|
}, 300)();
|
||
|
}, [selectedNodes, findParentId, createConnectedNode]);
|
||
|
|
||
|
const handleDeleteNodes = useCallback(() => {
|
||
|
if (selectedNodes.length === 0) return;
|
||
|
|
||
|
const nodesToDelete = new Set<string>();
|
||
|
|
||
|
// Collect all nodes to delete including children
|
||
|
const collectNodesToDelete = (nodeId: string) => {
|
||
|
nodesToDelete.add(nodeId);
|
||
|
store.edges
|
||
|
.filter(edge => edge.source === nodeId)
|
||
|
.forEach(edge => collectNodesToDelete(edge.target));
|
||
|
};
|
||
|
|
||
|
selectedNodes.forEach(node => collectNodesToDelete(node.id));
|
||
|
|
||
|
store.record(() => {
|
||
|
// Filter out deleted nodes and their edges
|
||
|
const remainingNodes = store.nodes.filter(node => !nodesToDelete.has(node.id));
|
||
|
const remainingEdges = store.edges.filter(edge =>
|
||
|
!nodesToDelete.has(edge.source) && !nodesToDelete.has(edge.target)
|
||
|
);
|
||
|
|
||
|
// Select next node (sibling or parent of first deleted node)
|
||
|
const firstDeletedNode = selectedNodes[0];
|
||
|
const parentId = findParentId(firstDeletedNode.id);
|
||
|
|
||
|
let nextSelectedId: string | undefined;
|
||
|
if (parentId) {
|
||
|
const siblingEdge = store.edges.find(edge =>
|
||
|
edge.source === parentId &&
|
||
|
!nodesToDelete.has(edge.target) &&
|
||
|
edge.target !== firstDeletedNode.id
|
||
|
);
|
||
|
nextSelectedId = siblingEdge?.target || parentId;
|
||
|
}
|
||
|
|
||
|
// Update nodes with new selection and set the remaining nodes
|
||
|
const updatedNodes = remainingNodes.map(node => ({
|
||
|
...node,
|
||
|
selected: node.id === nextSelectedId
|
||
|
}));
|
||
|
|
||
|
store.setNodes(updatedNodes);
|
||
|
store.setEdges(remainingEdges);
|
||
|
});
|
||
|
}, [selectedNodes, store, findParentId]);
|
||
|
return {
|
||
|
handleCreateChildNodes,
|
||
|
handleCreateSiblingNodes,
|
||
|
handleDeleteNodes
|
||
|
};
|
||
|
}
|