workflow底层UI库整合至项目,优化构建逻辑

This commit is contained in:
2026-02-24 11:20:18 +08:00
parent 094b185c49
commit 12accb2575
91 changed files with 6820 additions and 115 deletions

View File

@@ -0,0 +1,12 @@
export const genShortId = (length = 16) => {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const array = new Uint8Array(length);
crypto.getRandomValues(array);
return Array.from(array, (byte) => chars[byte % chars.length]).join('');
};
export const genUuid = () => {
return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c: any) =>
(c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16)
);
};

View File

@@ -0,0 +1,26 @@
import {getContext} from 'svelte';
import {useNodesData, useSvelteFlow} from '@xyflow/svelte';
import type {TinyflowOptions} from '#types';
export const getCurrentNodeId = () => {
return getContext<string>('svelteflow__node_id');
};
export const getOptions = () => {
return getContext<TinyflowOptions>('tinyflow_options');
};
export const useCurrentNodeData = () => {
const currentNodeId = getCurrentNodeId();
return useNodesData<any>(currentNodeId);
};
export const useUpdateNodeData = () => {
const { updateNodeData } = useSvelteFlow();
const currentNodeId = getCurrentNodeId();
return {
updateNodeData: (data: any) => {
updateNodeData(currentNodeId, data);
}
};
};

View File

@@ -0,0 +1,193 @@
import {
arrow,
computePosition,
flip,
type FlipOptions,
offset,
type OffsetOptions,
type Placement,
shift,
type ShiftOptions
} from '@floating-ui/dom';
export type FloatingOptions = {
trigger: string | HTMLElement;
triggerEvent?: string[];
floatContent: string | HTMLElement;
placement?: Placement;
offsetOptions?: OffsetOptions;
flipOptions?: FlipOptions;
shiftOptions?: ShiftOptions;
interactive?: boolean;
showArrow?: boolean;
};
export type FloatingInstance = {
destroy: () => void;
hide: () => void;
isVisible: () => boolean;
};
export const createFloating = ({
trigger,
triggerEvent,
floatContent,
placement = 'bottom',
offsetOptions,
flipOptions,
shiftOptions,
interactive,
showArrow
}: FloatingOptions): FloatingInstance => {
if (typeof trigger === 'string') {
const triggerEl = document.querySelector(trigger);
if (!triggerEl) {
throw new Error("element not found by document.querySelector('" + trigger + "')");
} else {
trigger = triggerEl as HTMLElement;
}
}
let floating: HTMLElement;
if (typeof floatContent === 'string') {
const floatContentEl = document.querySelector(floatContent);
if (!floatContentEl) {
throw new Error("element not found by document.querySelector('" + floatContent + "')");
} else {
floating = floatContentEl as HTMLElement;
}
} else {
floating = floatContent as HTMLElement;
}
let arrowElement: HTMLElement;
if (showArrow) {
arrowElement = document.createElement('div');
arrowElement.style.position = 'absolute';
arrowElement.style.backgroundColor = '#222';
arrowElement.style.width = '8px';
arrowElement.style.height = '8px';
arrowElement.style.transform = 'rotate(45deg)';
arrowElement.style.display = 'none';
floating.firstElementChild!.before(arrowElement);
}
function updatePosition() {
computePosition(trigger as Element, floating, {
placement: placement,
middleware: [
offset(offsetOptions), // 手动偏移配置
flip(flipOptions), //自动翻转
shift(shiftOptions), //自动偏移(使得浮动元素能够进入视野)
...(showArrow ? [arrow({ element: arrowElement })] : [])
]
}).then(({ x, y, placement, middlewareData }) => {
Object.assign(floating.style, {
left: `${x}px`,
top: `${y}px`
});
if (showArrow) {
const { x: arrowX, y: arrowY } = middlewareData.arrow as { x: number; y: number };
const staticSide = {
top: 'bottom',
right: 'left',
bottom: 'top',
left: 'right'
}[placement.split('-')[0]] as string;
Object.assign(arrowElement.style, {
zIndex: -1,
left: arrowX != null ? `${arrowX}px` : '',
top: arrowY != null ? `${arrowY}px` : '',
right: '',
bottom: '',
[staticSide]: '2px'
});
}
});
}
let visible = false;
function showTooltip() {
floating.style.display = 'block';
floating.style.visibility = 'block';
floating.style.position = 'absolute';
if (showArrow) {
arrowElement.style.display = 'block';
}
visible = true;
updatePosition();
}
function hideTooltip() {
floating.style.display = 'none';
if (showArrow) {
arrowElement.style.display = 'none';
}
visible = false;
}
function onTrigger(event: any) {
event.stopPropagation();
if (!visible) {
showTooltip();
} else {
hideTooltip();
}
}
function hideTooltipCompute(event: any) {
if (floating.contains(event.target as Node)) {
return;
}
hideTooltip();
}
if (!triggerEvent || triggerEvent.length == 0) {
if (interactive) {
triggerEvent = ['click'];
} else {
triggerEvent = ['mouseenter', 'focus'];
}
}
triggerEvent.forEach((event) => {
(trigger as HTMLElement).addEventListener(event, onTrigger);
});
if (interactive) {
document.addEventListener('click', hideTooltipCompute);
} else {
['mouseleave', 'blur'].forEach((event) => {
trigger.addEventListener(event, hideTooltip);
});
}
return {
destroy() {
triggerEvent.forEach((event) => {
(trigger as HTMLElement).removeEventListener(event, onTrigger);
});
if (interactive) {
document.removeEventListener('click', hideTooltipCompute);
} else {
['mouseleave', 'blur'].forEach((event) => {
trigger.removeEventListener(event, hideTooltip);
});
}
},
hide() {
hideTooltip();
},
isVisible() {
return visible;
}
};
};

View File

@@ -0,0 +1,3 @@
export const deepClone = <T>(obj: T): T => {
return JSON.parse(JSON.stringify(obj));
};

View File

@@ -0,0 +1,37 @@
export const deepEqual = <T>(obj1: T, obj2: T) => {
if (obj1 === obj2) return true;
// 处理 null 和 object 类型
if (typeof obj1 !== 'object' || obj1 === null || typeof obj2 !== 'object' || obj2 === null) {
return false;
}
// 判断是否都是数组
const isArray1 = Array.isArray(obj1);
const isArray2 = Array.isArray(obj2);
if (isArray1 !== isArray2) return false; // 一个是数组另一个不是,不相等
// 数组的情况
if (isArray1 && isArray2) {
if (obj1.length !== obj2.length) return false;
for (let i = 0; i < obj1.length; i++) {
if (!deepEqual(obj1[i], obj2[i])) return false;
}
return true;
}
// 普通对象的情况
else {
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) return false;
for (const key of keys1) {
if (!(key in obj2)) return false;
if (!deepEqual((obj1 as any)[key], (obj2 as any)[key])) return false;
}
return true;
}
};

View File

@@ -0,0 +1,16 @@
/**
* 判断当前焦点是否位于可编辑元素中(如 input、textarea 或 contenteditable 区域)。
* 适用于快捷键、全局事件监听等需要避免干扰用户输入的场景。
*/
export const isInEditableElement = () => {
const el = document.activeElement;
if (!el || !(el instanceof HTMLElement)) {
return false;
}
return (
el instanceof HTMLInputElement ||
el instanceof HTMLTextAreaElement ||
el.isContentEditable
);
};

View File

@@ -0,0 +1,63 @@
import {genShortId} from './IdGen';
import {useSvelteFlow} from '@xyflow/svelte';
import type {Parameter} from '#types';
export const fillParameterId = (parameters?: Parameter[]) => {
if (!parameters || parameters.length == 0) {
return parameters;
}
parameters.forEach((parameter) => {
if (!parameter.id) {
parameter.id = genShortId();
}
fillParameterId(parameter.children);
});
return parameters;
};
export const useAddParameter = () => {
const { updateNodeData } = useSvelteFlow();
return {
addParameter: (
nodeId: string,
dataKey: string = 'parameters',
parameter?: Parameter | Parameter[]
) => {
if (Array.isArray(parameter)) {
parameter.forEach((p) => fillParameterId(p?.children));
} else {
fillParameterId(parameter?.children);
}
function createNewParameter(parameter: Parameter) {
return {
name: '',
dataType: 'String',
refType: 'ref',
...parameter,
id: genShortId()
};
}
const newParameters: Parameter[] = [];
if (Array.isArray(parameter)) {
newParameters.push(...parameter.map(createNewParameter));
} else {
newParameters.push(createNewParameter(parameter as Parameter));
}
updateNodeData(nodeId, (node) => {
let parameters = node.data[dataKey] as Array<any>;
if (parameters) {
parameters.push(...newParameters);
} else {
parameters = [...newParameters];
}
return {
[dataKey]: [...parameters]
};
});
}
};
};

View File

@@ -0,0 +1,27 @@
import {genShortId} from './IdGen';
import {store} from '#store/stores.svelte';
export const useCopyNode = () => {
const copyNode = (id: string) => {
const node = store.getNode(id);
if (node) {
const newNodeId = genShortId();
const newNode = {
...node,
id: newNodeId,
position: {
x: node.position.x + 50,
y: node.position.y + 50
}
};
store.updateNodes((nodes) => {
const newNodes = nodes.map((n) => ({ ...n, selected: false }));
return [...newNodes, newNode];
});
}
};
return {
copyNode
};
};

View File

@@ -0,0 +1,282 @@
import {store} from '#store/stores.svelte';
import {genShortId} from '#components/utils/IdGen';
import {type Edge, type Node, useSvelteFlow} from '@xyflow/svelte';
interface ClipboardData {
tinyflowNodes: Node[];
tinyflowEdges?: Edge[];
version: string;
}
/**
* 清理节点中不可序列化的字段,确保可安全 JSON.stringify
*/
function sanitizeNode(node: Node): Node {
const { id, type, position, data, parentId } = node;
return {
id,
type,
position: { x: position.x, y: position.y },
parentId: parentId ? parentId : undefined,
data: data ? JSON.parse(JSON.stringify(data)) : {},
...(parentId !== undefined && { parentId })
};
}
/**
* 对 nodes 数组排序,确保每个父节点出现在其所有子节点之前。
* 使用 Kahn 算法(拓扑排序)处理任意嵌套层级。
*/
export function sortNodesForSvelteFlow(nodes: Node[]): Node[] {
const nodeMap = new Map<string, Node>();
const inDegree = new Map<string, number>(); // 每个节点的“依赖数”(即是否为子节点)
const childrenMap = new Map<string, string[]>(); // parentId -> childIds
// 初始化
for (const node of nodes) {
nodeMap.set(node.id, node);
inDegree.set(node.id, 0); // 默认无依赖
if (node.parentId) {
// 子节点依赖父节点
inDegree.set(node.id, 1);
if (!childrenMap.has(node.parentId)) {
childrenMap.set(node.parentId, []);
}
childrenMap.get(node.parentId)!.push(node.id);
}
}
// 所有根节点(无 parentId 或父不存在)入队
const queue: Node[] = [];
for (const node of nodes) {
if (!node.parentId || !nodeMap.has(node.parentId)) {
queue.push(node);
}
}
const result: Node[] = [];
const visited = new Set<string>();
// BFS 拓扑排序
while (queue.length > 0) {
const node = queue.shift()!;
if (visited.has(node.id)) continue;
visited.add(node.id);
result.push(node);
// 将该节点的所有直接子节点入队(如果其父已处理)
const children = childrenMap.get(node.id) || [];
for (const childId of children) {
if (!visited.has(childId)) {
queue.push(nodeMap.get(childId)!);
}
}
}
// 补充可能遗漏的节点(如循环引用或孤立子节点)
for (const node of nodes) {
if (!visited.has(node.id)) {
result.push(node);
}
}
return result;
}
/**
* 清理边中不可序列化的字段
*/
function sanitizeEdge(edge: Edge): Edge {
const { id, source, target, sourceHandle, targetHandle, type, data } = edge;
return {
id,
source,
target,
...(sourceHandle !== undefined && { sourceHandle }),
...(targetHandle !== undefined && { targetHandle }),
...(type !== undefined && { type }),
data: data ? JSON.parse(JSON.stringify(data)) : {}
};
}
/**
* 递归遍历对象,仅当遇到 { refType: 'ref', ref: string } 时重写 ref 的 id
*/
function rewriteRefsInData(obj: any, idMap: Map<string, string>): any {
if (obj === null || obj === undefined) {
return obj;
}
// 如果是数组,递归处理每个元素
if (Array.isArray(obj)) {
return obj.map((item) => rewriteRefsInData(item, idMap));
}
// 如果是对象,检查是否为引用对象
if (typeof obj === 'object') {
// 检查是否是引用定义refType === 'ref' 且有 ref 字段
if (obj.refType === 'ref' && typeof obj.ref === 'string') {
const match = obj.ref.match(/^([^.\s]+)\.(.+)$/);
if (match) {
const [_, oldNodeId, paramId] = match;
const newNodeId = idMap.get(oldNodeId);
if (newNodeId !== undefined) {
// 返回新对象,避免修改原数据
return {
...obj,
ref: `${newNodeId}.${paramId}`
};
}
}
}
// 递归处理所有子属性
const result: Record<string, any> = {};
for (const key in obj) {
if (Object.hasOwn(obj, key)) {
result[key] = rewriteRefsInData(obj[key], idMap);
}
}
return result;
}
// 原始类型string/number/boolean直接返回
return obj;
}
/**
* 复制粘贴处理器 Hook
*/
export const useCopyPasteHandler = () => {
const svelteFlow = useSvelteFlow();
const copyHandler = async (event: ClipboardEvent | KeyboardEvent) => {
const selectedNodes = store.getNodes().filter((node) => node.selected);
if (selectedNodes.length === 0) return;
// 获取完全包含在选中节点之间的边(起点和终点都被选中)
const allEdges = store.getEdges();
const relatedEdges = allEdges.filter(
(edge) =>
selectedNodes.some((n) => n.id === edge.source) &&
selectedNodes.some((n) => n.id === edge.target)
);
const serializableNodes = selectedNodes.map(sanitizeNode);
const serializableEdges = relatedEdges.map(sanitizeEdge);
const clipboardData: ClipboardData = {
tinyflowNodes: serializableNodes,
tinyflowEdges: serializableEdges,
version: '1.0'
};
const jsonStr = JSON.stringify(clipboardData, null, 0);
try {
// 优先使用 event.clipboardData在 copy 事件中可用,无需权限)
if ('clipboardData' in event && event.clipboardData) {
event.clipboardData.setData('text/plain', jsonStr);
if (event instanceof ClipboardEvent) {
event.preventDefault();
}
} else {
// 降级到 navigator.clipboard需用户手势
await navigator.clipboard.writeText(jsonStr);
}
console.log('Copied nodes and edges to clipboard');
} catch (err) {
console.error('Failed to copy:', err);
// 可选:同源降级存储
try {
sessionStorage.setItem('tinyflow_clipboard', jsonStr);
} catch {}
}
};
const pasteHandler = (event: ClipboardEvent) => {
const text = event.clipboardData?.getData('text/plain');
if (!text) return;
let parsed: ClipboardData | null = null;
try {
parsed = JSON.parse(text);
} catch {
return; // 忽略非 JSON 内容
}
if (!parsed?.tinyflowNodes || !Array.isArray(parsed.tinyflowNodes)) {
return;
}
event.preventDefault();
const pastedNodes = sortNodesForSvelteFlow(parsed.tinyflowNodes);
const pastedEdges = parsed.tinyflowEdges || [];
// 创建新节点(带新 ID 和偏移)
const newNodeIdMap = new Map<string, string>();
const newNodes: Node[] = [];
for (const node of pastedNodes) {
const newId = `node_${genShortId()}`;
newNodeIdMap.set(node.id, newId);
}
// 构建新节点(含重写后的 data
for (const node of pastedNodes) {
const newId = newNodeIdMap.get(node.id)!;
const newParentId =
node.parentId !== undefined
? newNodeIdMap.get(node.parentId) // 安全:即使父不在粘贴范围内,也会是 undefined
: undefined;
const newData = rewriteRefsInData(node.data, newNodeIdMap);
newNodes.push({
...node,
id: newId,
parentId: newParentId,
data: newData,
position: {
x: (node.position?.x ?? 0) + 50,
y: (node.position?.y ?? 0) + 50
},
selected: true
});
}
// 创建新边(仅当两端都在粘贴范围内)
const newEdges: Edge[] = [];
for (const edge of pastedEdges) {
const newSource = newNodeIdMap.get(edge.source);
const newTarget = newNodeIdMap.get(edge.target);
if (newSource && newTarget) {
newEdges.push({
...edge,
id: `edge_${genShortId()}`,
source: newSource,
target: newTarget
});
}
}
// 更新 store取消其他节点选中添加新内容
store.updateNodes((nodes) => {
const unselected = nodes.map((n) => ({ ...n, selected: false }));
return [...unselected, ...newNodes];
});
store.updateEdges((edges) => {
const unselected = edges.map((n) => ({ ...n, selected: false }));
return [...unselected, ...newEdges];
});
};
return {
copyHandler,
pasteHandler
};
};

View File

@@ -0,0 +1,10 @@
import {store} from '#store/stores.svelte';
export const useDeleteEdge = () => {
const deleteEdge = (id: string) => {
store.removeEdge( id);
};
return {
deleteEdge
};
};

View File

@@ -0,0 +1,11 @@
import {store} from '#store/stores.svelte';
export const useDeleteNode = () => {
const deleteNode = (id: string) => {
store.removeNode(id);
store.updateEdges(edges => edges.filter(edge => edge.source !== id && edge.target !== id))
};
return {
deleteNode
};
};

View File

@@ -0,0 +1,43 @@
import {store} from '../../store/stores.svelte';
export const useEnsureParentInNodesBefore = () => {
const ensureParentInNodesBefore = (parentNodeId: string, childNodeId: string) => {
store.updateNodes((nodeArray) => {
let parentIndex = -1;
for (let i = 0; i < nodeArray.length; i++) {
if (nodeArray[i].id === parentNodeId) {
parentIndex = i;
break;
}
}
if (parentIndex <= 0) {
return nodeArray;
}
let firstChildIndex = -1;
for (let i = 0; i < parentIndex; i++) {
if (nodeArray[i].parentId === parentNodeId || nodeArray[i].id === childNodeId) {
firstChildIndex = i;
break;
}
}
if (firstChildIndex == -1) {
return nodeArray;
}
const parentNode = nodeArray[parentIndex];
for (let i = parentIndex; i > firstChildIndex; i--) {
nodeArray[i] = nodeArray[i - 1];
}
nodeArray[firstChildIndex] = parentNode;
return nodeArray;
});
};
return {
ensureParentInNodesBefore
};
};

View File

@@ -0,0 +1,11 @@
import {store} from '#store/stores.svelte';
export const useGetEdgesBySource = () => {
const getEdgesBySource = (target: string) => {
const edges = store.getEdges();
return edges.filter((edge) => edge.source === target);
};
return {
getEdgesBySource
};
};

View File

@@ -0,0 +1,11 @@
import {store} from '#store/stores.svelte';
export const useGetEdgesByTarget = () => {
const getEdgesByTarget = (target: string) => {
const edges = store.getEdges();
return edges.filter((edge) => edge.target === target);
};
return {
getEdgesByTarget
};
};

View File

@@ -0,0 +1,10 @@
import {store} from '#store/stores.svelte';
export const useGetNode = () => {
const getNode = (id: string) => {
return store.getNode(id);
};
return {
getNode
};
};

View File

@@ -0,0 +1,22 @@
import {store} from '#store/stores.svelte';
export const useGetNodeRelativePosition = () => {
const getNodeRelativePosition = (parentNodeId: string) => {
let node = store.getNode(parentNodeId);
const position = { x: 0, y: 0 };
while (node) {
position.x += node.position.x;
position.y += node.position.y;
if (node.parentId) {
node = store.getNode(node.parentId);
} else {
node = undefined;
}
}
return position;
};
return {
getNodeRelativePosition
};
};

View File

@@ -0,0 +1,31 @@
import {store} from '#store/stores.svelte';
import type {Edge, Node} from '@xyflow/svelte';
export const useGetNodesFromSource = () => {
const getEdgesBySource = (target: string, edges: Edge[]) => {
return edges.filter(
// 排除循环节点的子节点,否则在多层循环嵌套时不正确
(edge) => edge.source === target && edge.sourceHandle !== 'loop_handle'
);
};
const getNodesFromSource = (sourceNodeId: string) => {
const edges = store.getEdges();
const result: Node[] = [];
let edgesFromSource = getEdgesBySource(sourceNodeId, edges);
while (edgesFromSource.length > 0) {
const newEdgesFromSource: Edge[] = [];
edgesFromSource.forEach((edge) => {
result.push(store.getNode(edge.target)!);
newEdgesFromSource.push(...getEdgesBySource(edge.target, edges));
});
edgesFromSource = newEdgesFromSource;
}
return result;
};
return {
getNodesFromSource
};
};

View File

@@ -0,0 +1,117 @@
import {type Edge, type Node, useNodesData, useStore} from '@xyflow/svelte';
import type {Parameter} from '#types';
import {getCurrentNodeId} from '#components/utils/NodeUtils';
const fillRefNodeIds = (refNodeIds: string[], currentNodeId: string, edges: Edge[]) => {
for (const edge of edges) {
if (edge.target === currentNodeId && edge.source) {
refNodeIds.push(edge.source);
fillRefNodeIds(refNodeIds, edge.source, edges);
}
}
};
const getChildren = (params: any, parentId: string, nodeIsChildren: boolean) => {
if (!params || params.length === 0) return [];
return params.map((param: any) => ({
label:
param.name +
(nodeIsChildren
? ` (Array<${param.dataType || 'String'}>)`
: ` (${param.dataType || 'String'})`),
value: parentId + '.' + param.name,
children: getChildren(param.children, parentId + '.' + param.name, nodeIsChildren)
}));
};
const nodeToOptions = (node: Node, nodeIsChildren: boolean, currentNode: Node) => {
if (node.type === 'startNode') {
const parameters = node.data.parameters as Array<Parameter>;
const children = [];
if (parameters)
for (const parameter of parameters) {
children.push({
label:
parameter.name +
(nodeIsChildren
? ` (Array<${parameter.dataType || 'String'}>)`
: ` (${parameter.dataType || 'String'})`),
value: node.id + '.' + parameter.name
});
}
return {
label: node.data.title,
value: node.id,
children
};
} else if (node.type === 'loopNode' && currentNode.parentId) {
return {
label: node.data.title,
value: node.id,
children: [
{
label: 'loopItem',
value: node.id + '.loopItem'
},
{
label: 'index (Number)',
value: node.id + '.index'
}
]
};
} else {
const outputDefs = node.data.outputDefs;
if (outputDefs) {
return {
label: node.data.title,
value: node.id,
children: getChildren(outputDefs, node.id, nodeIsChildren)
};
}
}
};
export const useRefOptions: any = (useChildrenOnly: boolean = false) => {
const currentNodeId = getCurrentNodeId();
const currentNode = useNodesData(currentNodeId);
const { nodes, edges, nodeLookup } = $derived(useStore());
let selectItems = $derived.by(() => {
const resultOptions = [];
if (!currentNode.current) {
return [];
}
//通过 nodeLookup.get 才会得到有 parentId 的 node
const cNode = nodeLookup.get(currentNodeId)!;
if (useChildrenOnly) {
for (const node of nodes) {
const nodeIsChildren = node.parentId === currentNode.current.id;
if (nodeIsChildren) {
const nodeOptions = nodeToOptions(node, nodeIsChildren, cNode);
nodeOptions && resultOptions.push(nodeOptions);
}
}
} else {
const refNodeIds: string[] = [];
fillRefNodeIds(refNodeIds, currentNodeId, edges);
for (const node of nodes) {
if (refNodeIds.includes(node.id)) {
const nodeIsChildren = node.parentId === currentNode.current.id;
const nodeOptions = nodeToOptions(node, nodeIsChildren, cNode);
nodeOptions && resultOptions.push(nodeOptions);
}
}
}
return resultOptions;
});
return {
get current() {
return selectItems;
}
};
};

View File

@@ -0,0 +1,25 @@
import {store} from '#store/stores.svelte';
export const useUpdateEdgeData = () => {
const updateEdgeData = (id: string, dataUpdate: any, options?: { replace: boolean }) => {
const edge = store.getEdge(id);
if (!edge) {
return;
}
const nextData = typeof dataUpdate === 'function' ? dataUpdate(edge) : dataUpdate;
edge.data = options?.replace ? nextData : { ...edge.data, ...nextData };
store.updateEdges((edges) => {
return edges.map((e) => {
if (e.id === id) {
return edge;
}
return e;
});
});
};
return {
updateEdgeData
};
};