feat: enhance canvas connection handling with custom animation and edge management

- Added a custom connection line component with animation for improved visual feedback during node interactions.
- Implemented CSS animations for temporary connection lines, enhancing the user experience in the canvas.
- Refactored edge creation and removal logic to support optimistic updates, improving performance during node manipulations.
- Introduced a utility function to compute edge reconnections after node deletions, streamlining edge management.
This commit is contained in:
Matthias
2026-03-28 13:26:47 +01:00
parent e5f27d7d29
commit fb24205da0
5 changed files with 425 additions and 91 deletions

View File

@@ -138,6 +138,23 @@
} }
} }
/* Temporäre XYFlow-Verbindungslinie (custom connectionLineComponent) */
@keyframes ls-connection-dash-offset {
to {
stroke-dashoffset: -18;
}
}
.ls-connection-line-marching {
animation: ls-connection-dash-offset 0.4s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
.ls-connection-line-marching {
animation: none;
}
}
@layer utilities { @layer utilities {
.animate-shimmer { .animate-shimmer {
animation: shimmer 1.5s ease-in-out infinite; animation: shimmer 1.5s ease-in-out infinite;

View File

@@ -9,7 +9,7 @@ import {
} from "react"; } from "react";
import type { ReactMutation } from "convex/react"; import type { ReactMutation } from "convex/react";
import type { FunctionReference } from "convex/server"; import type { FunctionReference } from "convex/server";
import { useReactFlow, useStore, type Edge as RFEdge } from "@xyflow/react"; import { useStore, type Edge as RFEdge } from "@xyflow/react";
import type { Id } from "@/convex/_generated/dataModel"; import type { Id } from "@/convex/_generated/dataModel";
import { NODE_DEFAULTS, NODE_HANDLE_MAP } from "@/lib/canvas-utils"; import { NODE_DEFAULTS, NODE_HANDLE_MAP } from "@/lib/canvas-utils";
@@ -89,6 +89,10 @@ type CreateNodeWithIntersectionInput = {
width?: number; width?: number;
height?: number; height?: number;
data?: Record<string, unknown>; data?: Record<string, unknown>;
/**
* Optionaler Bildschirmpunkt für Hit-Test auf eine Kante. Nur wenn gesetzt,
* kann eine bestehende Kante gesplittet werden — ohne dieses Feld niemals.
*/
clientPosition?: FlowPoint; clientPosition?: FlowPoint;
zIndex?: number; zIndex?: number;
/** Correlate optimistic node id with server id after create (see canvas move flush). */ /** Correlate optimistic node id with server id after create (see canvas move flush). */
@@ -183,7 +187,6 @@ export function CanvasPlacementProvider({
onCreateNodeSettled, onCreateNodeSettled,
children, children,
}: CanvasPlacementProviderProps) { }: CanvasPlacementProviderProps) {
const { flowToScreenPosition } = useReactFlow();
const edges = useStore((store) => store.edges); const edges = useStore((store) => store.edges);
const createNodeWithIntersection = useCallback( const createNodeWithIntersection = useCallback(
@@ -205,17 +208,10 @@ export function CanvasPlacementProvider({
const effectiveWidth = width ?? defaults.width; const effectiveWidth = width ?? defaults.width;
const effectiveHeight = height ?? defaults.height; const effectiveHeight = height ?? defaults.height;
const centerClientPosition = flowToScreenPosition({
x: position.x + effectiveWidth / 2,
y: position.y + effectiveHeight / 2,
});
const hitEdgeFromClientPosition = clientPosition const hitEdge = clientPosition
? getIntersectedPersistedEdge(clientPosition, edges) ? getIntersectedPersistedEdge(clientPosition, edges)
: undefined; : undefined;
const hitEdge =
hitEdgeFromClientPosition ??
getIntersectedPersistedEdge(centerClientPosition, edges);
const baseNodePayload = { const baseNodePayload = {
canvasId, canvasId,
@@ -279,7 +275,6 @@ export function CanvasPlacementProvider({
createNode, createNode,
createNodeWithEdgeSplit, createNodeWithEdgeSplit,
edges, edges,
flowToScreenPosition,
onCreateNodeSettled, onCreateNodeSettled,
], ],
); );

View File

@@ -31,6 +31,7 @@ import { authClient } from "@/lib/auth-client";
import { nodeTypes } from "./node-types"; import { nodeTypes } from "./node-types";
import { import {
computeBridgeCreatesForDeletedNodes,
convexNodeDocWithMergedStorageUrl, convexNodeDocWithMergedStorageUrl,
convexNodeToRF, convexNodeToRF,
convexEdgeToRF, convexEdgeToRF,
@@ -48,6 +49,7 @@ import {
import CanvasToolbar from "@/components/canvas/canvas-toolbar"; import CanvasToolbar from "@/components/canvas/canvas-toolbar";
import { CanvasCommandPalette } from "@/components/canvas/canvas-command-palette"; import { CanvasCommandPalette } from "@/components/canvas/canvas-command-palette";
import { CanvasPlacementProvider } from "@/components/canvas/canvas-placement-context"; import { CanvasPlacementProvider } from "@/components/canvas/canvas-placement-context";
import CustomConnectionLine from "@/components/canvas/custom-connection-line";
interface CanvasInnerProps { interface CanvasInnerProps {
canvasId: Id<"canvases">; canvasId: Id<"canvases">;
@@ -66,6 +68,17 @@ function clientRequestIdFromOptimisticNodeId(id: string): string | null {
return suffix.length > 0 ? suffix : null; return suffix.length > 0 ? suffix : null;
} }
/** Kanten-Split nach Drag: wartet auf echte Node-ID, wenn der Knoten noch optimistisch ist. */
type PendingEdgeSplit = {
intersectedEdgeId: Id<"edges">;
sourceNodeId: Id<"nodes">;
targetNodeId: Id<"nodes">;
intersectedSourceHandle?: string;
intersectedTargetHandle?: string;
middleSourceHandle?: string;
middleTargetHandle?: string;
};
function withResolvedCompareData(nodes: RFNode[], edges: RFEdge[]): RFNode[] { function withResolvedCompareData(nodes: RFNode[], edges: RFEdge[]): RFNode[] {
const persistedEdges = edges.filter((edge) => edge.className !== "temp"); const persistedEdges = edges.filter((edge) => edge.className !== "temp");
let hasNodeUpdates = false; let hasNodeUpdates = false;
@@ -380,40 +393,8 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
new Map<string, { positionX: number; positionY: number }>(), new Map<string, { positionX: number; positionY: number }>(),
); );
const resolvedRealIdByClientRequestRef = useRef(new Map<string, Id<"nodes">>()); const resolvedRealIdByClientRequestRef = useRef(new Map<string, Id<"nodes">>());
const pendingEdgeSplitByClientRequestRef = useRef(
/** Pairing: create kann vor oder nach Drag-Ende fertig sein — was zuerst kommt, speichert; das andere triggert moveNode. */ new Map<string, PendingEdgeSplit>(),
const syncPendingMoveForClientRequest = useCallback(
(clientRequestId: string | undefined, realId?: Id<"nodes">) => {
if (!clientRequestId) return;
if (realId !== undefined) {
const pending = pendingMoveAfterCreateRef.current.get(clientRequestId);
if (pending) {
pendingMoveAfterCreateRef.current.delete(clientRequestId);
resolvedRealIdByClientRequestRef.current.delete(clientRequestId);
void moveNode({
nodeId: realId,
positionX: pending.positionX,
positionY: pending.positionY,
});
return;
}
resolvedRealIdByClientRequestRef.current.set(clientRequestId, realId);
return;
}
const r = resolvedRealIdByClientRequestRef.current.get(clientRequestId);
const p = pendingMoveAfterCreateRef.current.get(clientRequestId);
if (!r || !p) return;
pendingMoveAfterCreateRef.current.delete(clientRequestId);
resolvedRealIdByClientRequestRef.current.delete(clientRequestId);
void moveNode({
nodeId: r,
positionX: p.positionX,
positionY: p.positionY,
});
},
[moveNode],
); );
const createNode = useMutation(api.nodes.create).withOptimisticUpdate( const createNode = useMutation(api.nodes.create).withOptimisticUpdate(
@@ -513,9 +494,165 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
}); });
const createNodeWithEdgeSplit = useMutation(api.nodes.createWithEdgeSplit); const createNodeWithEdgeSplit = useMutation(api.nodes.createWithEdgeSplit);
const batchRemoveNodes = useMutation(api.nodes.batchRemove);
const createEdge = useMutation(api.edges.create); const batchRemoveNodes = useMutation(api.nodes.batchRemove).withOptimisticUpdate(
const removeEdge = useMutation(api.edges.remove); (localStore, args) => {
const nodeList = localStore.getQuery(api.nodes.list, { canvasId });
const edgeList = localStore.getQuery(api.edges.list, { canvasId });
if (nodeList === undefined || edgeList === undefined) return;
const removeSet = new Set<string>(args.nodeIds.map((id) => id as string));
localStore.setQuery(
api.nodes.list,
{ canvasId },
nodeList.filter((n) => !removeSet.has(n._id)),
);
localStore.setQuery(
api.edges.list,
{ canvasId },
edgeList.filter(
(e) =>
!removeSet.has(e.sourceNodeId) && !removeSet.has(e.targetNodeId),
),
);
},
);
const createEdge = useMutation(api.edges.create).withOptimisticUpdate(
(localStore, args) => {
const edgeList = localStore.getQuery(api.edges.list, {
canvasId: args.canvasId,
});
if (edgeList === undefined) return;
const tempId = `${OPTIMISTIC_EDGE_PREFIX}${Date.now()}_${Math.random().toString(36).slice(2, 11)}` as Id<"edges">;
const synthetic: Doc<"edges"> = {
_id: tempId,
_creationTime: Date.now(),
canvasId: args.canvasId,
sourceNodeId: args.sourceNodeId,
targetNodeId: args.targetNodeId,
sourceHandle: args.sourceHandle,
targetHandle: args.targetHandle,
};
localStore.setQuery(
api.edges.list,
{ canvasId: args.canvasId },
[...edgeList, synthetic],
);
},
);
const removeEdge = useMutation(api.edges.remove).withOptimisticUpdate(
(localStore, args) => {
const edgeList = localStore.getQuery(api.edges.list, { canvasId });
if (edgeList === undefined) return;
localStore.setQuery(
api.edges.list,
{ canvasId },
edgeList.filter((e) => e._id !== args.edgeId),
);
},
);
const commitEdgeIntersectionSplit = useCallback(
async (
middleNodeId: Id<"nodes">,
intersectedEdge: RFEdge,
handles: NonNullable<(typeof NODE_HANDLE_MAP)[string]>,
) => {
await Promise.all([
createEdge({
canvasId,
sourceNodeId: intersectedEdge.source as Id<"nodes">,
targetNodeId: middleNodeId,
sourceHandle: normalizeHandle(intersectedEdge.sourceHandle),
targetHandle: normalizeHandle(handles.target),
}),
createEdge({
canvasId,
sourceNodeId: middleNodeId,
targetNodeId: intersectedEdge.target as Id<"nodes">,
sourceHandle: normalizeHandle(handles.source),
targetHandle: normalizeHandle(intersectedEdge.targetHandle),
}),
removeEdge({ edgeId: intersectedEdge.id as Id<"edges"> }),
]);
},
[canvasId, createEdge, removeEdge],
);
const flushPendingEdgeSplit = useCallback(
(clientRequestId: string, realMiddleNodeId: Id<"nodes">) => {
const pending = pendingEdgeSplitByClientRequestRef.current.get(
clientRequestId,
);
if (!pending) return;
pendingEdgeSplitByClientRequestRef.current.delete(clientRequestId);
void Promise.all([
createEdge({
canvasId,
sourceNodeId: pending.sourceNodeId,
targetNodeId: realMiddleNodeId,
sourceHandle: pending.intersectedSourceHandle,
targetHandle: pending.middleTargetHandle,
}),
createEdge({
canvasId,
sourceNodeId: realMiddleNodeId,
targetNodeId: pending.targetNodeId,
sourceHandle: pending.middleSourceHandle,
targetHandle: pending.intersectedTargetHandle,
}),
removeEdge({ edgeId: pending.intersectedEdgeId }),
]).catch((error: unknown) => {
console.error("[Canvas pending edge split failed]", {
clientRequestId,
realMiddleNodeId,
error: String(error),
});
});
},
[canvasId, createEdge, removeEdge],
);
/** Pairing: create kann vor oder nach Drag-Ende fertig sein — was zuerst kommt, speichert; das andere triggert moveNode. Zusätzlich: Kanten-Split erst mit echter Node-ID (nach create). */
const syncPendingMoveForClientRequest = useCallback(
(clientRequestId: string | undefined, realId?: Id<"nodes">) => {
if (!clientRequestId) return;
if (realId !== undefined) {
const pending = pendingMoveAfterCreateRef.current.get(clientRequestId);
if (pending) {
pendingMoveAfterCreateRef.current.delete(clientRequestId);
resolvedRealIdByClientRequestRef.current.delete(clientRequestId);
void moveNode({
nodeId: realId,
positionX: pending.positionX,
positionY: pending.positionY,
});
flushPendingEdgeSplit(clientRequestId, realId);
return;
}
resolvedRealIdByClientRequestRef.current.set(clientRequestId, realId);
flushPendingEdgeSplit(clientRequestId, realId);
return;
}
const r = resolvedRealIdByClientRequestRef.current.get(clientRequestId);
const p = pendingMoveAfterCreateRef.current.get(clientRequestId);
if (!r || !p) return;
pendingMoveAfterCreateRef.current.delete(clientRequestId);
resolvedRealIdByClientRequestRef.current.delete(clientRequestId);
void moveNode({
nodeId: r,
positionX: p.positionX,
positionY: p.positionY,
});
flushPendingEdgeSplit(clientRequestId, r);
},
[moveNode, flushPendingEdgeSplit],
);
// ─── Lokaler State (für flüssiges Dragging) ─────────────────── // ─── Lokaler State (für flüssiges Dragging) ───────────────────
const [nodes, setNodes] = useState<RFNode[]>([]); const [nodes, setNodes] = useState<RFNode[]>([]);
@@ -1075,23 +1212,36 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
return; return;
} }
await createEdge({ const optimisticCid = clientRequestIdFromOptimisticNodeId(node.id);
canvasId, let middleNodeId = node.id as Id<"nodes">;
if (optimisticCid) {
const resolvedMiddle =
resolvedRealIdByClientRequestRef.current.get(optimisticCid);
if (resolvedMiddle) {
middleNodeId = resolvedMiddle;
} else {
pendingEdgeSplitByClientRequestRef.current.set(optimisticCid, {
intersectedEdgeId: intersectedEdge.id as Id<"edges">,
sourceNodeId: intersectedEdge.source as Id<"nodes">, sourceNodeId: intersectedEdge.source as Id<"nodes">,
targetNodeId: node.id as Id<"nodes">,
sourceHandle: normalizeHandle(intersectedEdge.sourceHandle),
targetHandle: normalizeHandle(handles.target),
});
await createEdge({
canvasId,
sourceNodeId: node.id as Id<"nodes">,
targetNodeId: intersectedEdge.target as Id<"nodes">, targetNodeId: intersectedEdge.target as Id<"nodes">,
sourceHandle: normalizeHandle(handles.source), intersectedSourceHandle: normalizeHandle(
targetHandle: normalizeHandle(intersectedEdge.targetHandle), intersectedEdge.sourceHandle,
),
intersectedTargetHandle: normalizeHandle(
intersectedEdge.targetHandle,
),
middleSourceHandle: normalizeHandle(handles.source),
middleTargetHandle: normalizeHandle(handles.target),
}); });
return;
}
}
await removeEdge({ edgeId: intersectedEdge.id as Id<"edges"> }); await commitEdgeIntersectionSplit(
middleNodeId,
intersectedEdge,
handles,
);
} catch (error) { } catch (error) {
console.error("[Canvas edge intersection split failed]", { console.error("[Canvas edge intersection split failed]", {
canvasId, canvasId,
@@ -1110,10 +1260,9 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
[ [
batchMoveNodes, batchMoveNodes,
canvasId, canvasId,
createEdge, commitEdgeIntersectionSplit,
edges, edges,
moveNode, moveNode,
removeEdge,
setHighlightedIntersectionEdge, setHighlightedIntersectionEdge,
syncPendingMoveForClientRequest, syncPendingMoveForClientRequest,
], ],
@@ -1147,28 +1296,20 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
deletingNodeIds.current.add(id); deletingNodeIds.current.add(id);
} }
// Auto-Reconnect: Für jeden gelöschten Node eingehende und ausgehende Edges verbinden const bridgeCreates = computeBridgeCreatesForDeletedNodes(
const edgePromises: Promise<unknown>[] = []; deletedNodes,
for (const node of deletedNodes) { nodes,
const incomingEdges = edges.filter((e) => e.target === node.id); edges,
const outgoingEdges = edges.filter((e) => e.source === node.id); );
const edgePromises = bridgeCreates.map((b) =>
if (incomingEdges.length > 0 && outgoingEdges.length > 0) {
for (const incoming of incomingEdges) {
for (const outgoing of outgoingEdges) {
edgePromises.push(
createEdge({ createEdge({
canvasId, canvasId,
sourceNodeId: incoming.source as Id<"nodes">, sourceNodeId: b.sourceNodeId,
targetNodeId: outgoing.target as Id<"nodes">, targetNodeId: b.targetNodeId,
sourceHandle: incoming.sourceHandle ?? undefined, sourceHandle: b.sourceHandle,
targetHandle: outgoing.targetHandle ?? undefined, targetHandle: b.targetHandle,
}), }),
); );
}
}
}
}
// Batch-Delete + Auto-Reconnect parallel, dann deletingNodeIds aufräumen // Batch-Delete + Auto-Reconnect parallel, dann deletingNodeIds aufräumen
void Promise.all([ void Promise.all([
@@ -1195,7 +1336,7 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
toast.info(title); toast.info(title);
} }
}, },
[edges, batchRemoveNodes, createEdge, canvasId], [nodes, edges, batchRemoveNodes, createEdge, canvasId],
); );
// ─── Edge löschen → Convex ──────────────────────────────────── // ─── Edge löschen → Convex ────────────────────────────────────
@@ -1294,6 +1435,7 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
edges={edges} edges={edges}
onlyRenderVisibleElements onlyRenderVisibleElements
defaultEdgeOptions={DEFAULT_EDGE_OPTIONS} defaultEdgeOptions={DEFAULT_EDGE_OPTIONS}
connectionLineComponent={CustomConnectionLine}
nodeTypes={nodeTypes} nodeTypes={nodeTypes}
onNodesChange={onNodesChange} onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange} onEdgesChange={onEdgesChange}

View File

@@ -0,0 +1,72 @@
"use client";
import {
ConnectionLineType,
getBezierPath,
getSimpleBezierPath,
getSmoothStepPath,
getStraightPath,
type ConnectionLineComponentProps,
} from "@xyflow/react";
import { connectionLineAccentRgb } from "@/lib/canvas-utils";
export default function CustomConnectionLine({
connectionLineType,
fromNode,
fromHandle,
fromX,
fromY,
toX,
toY,
fromPosition,
toPosition,
connectionStatus,
}: ConnectionLineComponentProps) {
const pathParams = {
sourceX: fromX,
sourceY: fromY,
sourcePosition: fromPosition,
targetX: toX,
targetY: toY,
targetPosition: toPosition,
};
let path = "";
switch (connectionLineType) {
case ConnectionLineType.Bezier:
[path] = getBezierPath(pathParams);
break;
case ConnectionLineType.SimpleBezier:
[path] = getSimpleBezierPath(pathParams);
break;
case ConnectionLineType.Step:
[path] = getSmoothStepPath({
...pathParams,
borderRadius: 0,
});
break;
case ConnectionLineType.SmoothStep:
[path] = getSmoothStepPath(pathParams);
break;
default:
[path] = getStraightPath(pathParams);
}
const [r, g, b] = connectionLineAccentRgb(fromNode.type, fromHandle.id);
const opacity = connectionStatus === "invalid" ? 0.45 : 1;
return (
<path
d={path}
fill="none"
className="ls-connection-line-marching"
style={{
stroke: `rgb(${r}, ${g}, ${b})`,
strokeWidth: 2.5,
strokeLinecap: "round",
strokeDasharray: "10 8",
opacity,
}}
/>
);
}

View File

@@ -1,5 +1,11 @@
import type { Node as RFNode, Edge as RFEdge } from "@xyflow/react"; import {
import type { Doc } from "@/convex/_generated/dataModel"; getConnectedEdges,
getIncomers,
getOutgoers,
type Node as RFNode,
type Edge as RFEdge,
} from "@xyflow/react";
import type { Doc, Id } from "@/convex/_generated/dataModel";
/** /**
* Convex Node → React Flow Node * Convex Node → React Flow Node
@@ -100,6 +106,35 @@ const SOURCE_NODE_GLOW_RGB: Record<string, readonly [number, number, number]> =
compare: [100, 116, 139], compare: [100, 116, 139],
}; };
/** Compare: Ziel-Handles blau/smaragd, Quelle compare-out grau (wie in compare-node.tsx). */
const COMPARE_HANDLE_CONNECTION_RGB: Record<
string,
readonly [number, number, number]
> = {
left: [59, 130, 246],
right: [16, 185, 129],
"compare-out": [100, 116, 139],
};
const CONNECTION_LINE_FALLBACK_RGB: readonly [number, number, number] = [
13, 148, 136,
];
/**
* RGB für die temporäre Verbindungslinie (Quell-Node + optional Handle, z. B. Reconnect).
*/
export function connectionLineAccentRgb(
nodeType: string | undefined,
handleId: string | null | undefined,
): readonly [number, number, number] {
if (nodeType === "compare" && handleId) {
const byHandle = COMPARE_HANDLE_CONNECTION_RGB[handleId];
if (byHandle) return byHandle;
}
if (!nodeType) return CONNECTION_LINE_FALLBACK_RGB;
return SOURCE_NODE_GLOW_RGB[nodeType] ?? CONNECTION_LINE_FALLBACK_RGB;
}
export type EdgeGlowColorMode = "light" | "dark"; export type EdgeGlowColorMode = "light" | "dark";
function sourceGlowFilterForNodeType( function sourceGlowFilterForNodeType(
@@ -253,3 +288,76 @@ export function computeMediaNodeSize(
aspectRatio, aspectRatio,
}; };
} }
function reconnectEdgeKey(edge: RFEdge): string {
return `${edge.source}\0${edge.target}\0${edge.sourceHandle ?? ""}\0${edge.targetHandle ?? ""}`;
}
export type BridgeCreatePayload = {
sourceNodeId: Id<"nodes">;
targetNodeId: Id<"nodes">;
sourceHandle?: string;
targetHandle?: string;
};
/**
* Nach Löschen mittlerer Knoten: Kanten wie im React-Flow-Beispiel
* „Delete Middle Node“ fortschreiben; nur Kanten zurückgeben, die neu
* angelegt werden müssen (nicht bereits vor dem Löschen vorhanden).
*/
export function computeBridgeCreatesForDeletedNodes(
deletedNodes: RFNode[],
allNodes: RFNode[],
allEdges: RFEdge[],
): BridgeCreatePayload[] {
if (deletedNodes.length === 0) return [];
const initialPersisted = allEdges.filter((e) => e.className !== "temp");
const initialKeys = new Set(initialPersisted.map(reconnectEdgeKey));
let remainingNodes = [...allNodes];
let acc = [...initialPersisted];
for (const node of deletedNodes) {
const incomers = getIncomers(node, remainingNodes, acc);
const outgoers = getOutgoers(node, remainingNodes, acc);
const connectedEdges = getConnectedEdges([node], acc);
const remainingEdges = acc.filter((e) => !connectedEdges.includes(e));
const createdEdges: RFEdge[] = [];
for (const inc of incomers) {
for (const out of outgoers) {
const inEdge = connectedEdges.find(
(e) => e.source === inc.id && e.target === node.id,
);
const outEdge = connectedEdges.find(
(e) => e.source === node.id && e.target === out.id,
);
if (!inEdge || !outEdge || inc.id === out.id) continue;
createdEdges.push({
id: `reconnect-${inc.id}-${out.id}-${node.id}-${createdEdges.length}`,
source: inc.id,
target: out.id,
sourceHandle: inEdge.sourceHandle,
targetHandle: outEdge.targetHandle,
});
}
}
acc = [...remainingEdges, ...createdEdges];
remainingNodes = remainingNodes.filter((rn) => rn.id !== node.id);
}
const result: BridgeCreatePayload[] = [];
for (const e of acc) {
if (!initialKeys.has(reconnectEdgeKey(e))) {
result.push({
sourceNodeId: e.source as Id<"nodes">,
targetNodeId: e.target as Id<"nodes">,
sourceHandle: e.sourceHandle ?? undefined,
targetHandle: e.targetHandle ?? undefined,
});
}
}
return result;
}