Enable offline edge split sync and stabilize local edge state
This commit is contained in:
@@ -122,10 +122,10 @@ Compare-Node hat zusätzlich Handle-spezifische Farben (`left` → Blau, `right`
|
||||
|
||||
- Key-Schema: `lemonspace.canvas:snapshot:v1:<canvasId>` und `lemonspace.canvas:ops:v1:<canvasId>`
|
||||
- Snapshot = letzter bekannter State (Nodes + Edges) für schnellen initialen Render
|
||||
- Ops-Queue ist aktiv für: `createNode*`, `createEdge`, `moveNode`, `resizeNode`, `updateData`, `removeEdge`, `batchRemoveNodes`.
|
||||
- Ops-Queue ist aktiv für: `createNode*` (inkl. `createNodeWithEdgeSplit`), `createEdge`, `splitEdgeAtExistingNode`, `moveNode`, `resizeNode`, `updateData`, `removeEdge`, `batchRemoveNodes`.
|
||||
- Reconnect synchronisiert als `createEdge + removeEdge` (statt rein lokalem UI-Umbiegen).
|
||||
- ID-Handover `optimistic_* → realId` remappt Folge-Operationen in Queue + localStorage-Mirror, damit Verbindungen während/ nach Replay stabil bleiben.
|
||||
- Unsupported offline (weiterhin online-only): `createWithEdgeSplit`, Datei-Upload/Storage-Mutations, AI-Generierung.
|
||||
- Unsupported offline (weiterhin online-only): Datei-Upload/Storage-Mutations, AI-Generierung.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -366,6 +366,43 @@ function isBatchMoveNodesOpPayload(
|
||||
return record.moves.every(isMoveNodeOpPayload);
|
||||
}
|
||||
|
||||
function isSplitEdgeAtExistingNodeOpPayload(
|
||||
payload: unknown,
|
||||
): payload is {
|
||||
middleNodeId: Id<"nodes">;
|
||||
positionX?: number;
|
||||
positionY?: number;
|
||||
} {
|
||||
if (typeof payload !== "object" || payload === null) return false;
|
||||
const record = payload as Record<string, unknown>;
|
||||
if (typeof record.middleNodeId !== "string") return false;
|
||||
const hasPositionX =
|
||||
record.positionX === undefined || typeof record.positionX === "number";
|
||||
const hasPositionY =
|
||||
record.positionY === undefined || typeof record.positionY === "number";
|
||||
return hasPositionX && hasPositionY;
|
||||
}
|
||||
|
||||
function isRemoveEdgeOpPayload(
|
||||
payload: unknown,
|
||||
): payload is {
|
||||
edgeId: Id<"edges">;
|
||||
} {
|
||||
if (typeof payload !== "object" || payload === null) return false;
|
||||
const record = payload as Record<string, unknown>;
|
||||
return typeof record.edgeId === "string";
|
||||
}
|
||||
|
||||
function isSplitEdgeOpPayload(
|
||||
payload: unknown,
|
||||
): payload is {
|
||||
splitEdgeId: Id<"edges">;
|
||||
} {
|
||||
if (typeof payload !== "object" || payload === null) return false;
|
||||
const record = payload as Record<string, unknown>;
|
||||
return typeof record.splitEdgeId === "string";
|
||||
}
|
||||
|
||||
export function getPendingMovePinsFromLocalOps(
|
||||
canvasId: string,
|
||||
): Map<string, { x: number; y: number }> {
|
||||
@@ -385,11 +422,43 @@ export function getPendingMovePinsFromLocalOps(
|
||||
y: move.positionY,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
op.type === "splitEdgeAtExistingNode" &&
|
||||
isSplitEdgeAtExistingNodeOpPayload(op.payload) &&
|
||||
op.payload.positionX !== undefined &&
|
||||
op.payload.positionY !== undefined
|
||||
) {
|
||||
pins.set(op.payload.middleNodeId as string, {
|
||||
x: op.payload.positionX,
|
||||
y: op.payload.positionY,
|
||||
});
|
||||
}
|
||||
}
|
||||
return pins;
|
||||
}
|
||||
|
||||
export function getPendingRemovedEdgeIdsFromLocalOps(
|
||||
canvasId: string,
|
||||
): Set<string> {
|
||||
const edgeIds = new Set<string>();
|
||||
for (const op of readCanvasOps(canvasId)) {
|
||||
if (op.type === "removeEdge" && isRemoveEdgeOpPayload(op.payload)) {
|
||||
edgeIds.add(op.payload.edgeId as string);
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
(op.type === "createNodeWithEdgeSplit" ||
|
||||
op.type === "splitEdgeAtExistingNode") &&
|
||||
isSplitEdgeOpPayload(op.payload)
|
||||
) {
|
||||
edgeIds.add(op.payload.splitEdgeId as string);
|
||||
}
|
||||
}
|
||||
return edgeIds;
|
||||
}
|
||||
|
||||
export function mergeNodesPreservingLocalState(
|
||||
previousNodes: RFNode[],
|
||||
incomingNodes: RFNode[],
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useStore, type Edge as RFEdge } from "@xyflow/react";
|
||||
|
||||
import type { Id } from "@/convex/_generated/dataModel";
|
||||
import { NODE_DEFAULTS, NODE_HANDLE_MAP } from "@/lib/canvas-utils";
|
||||
import { isOptimisticEdgeId } from "./canvas-helpers";
|
||||
|
||||
type CreateNodeArgs = {
|
||||
canvasId: Id<"canvases">;
|
||||
@@ -40,6 +41,7 @@ type CreateNodeWithEdgeSplitArgs = {
|
||||
newNodeSourceHandle?: string;
|
||||
splitSourceHandle?: string;
|
||||
splitTargetHandle?: string;
|
||||
clientRequestId?: string;
|
||||
};
|
||||
|
||||
type CreateNodeWithEdgeFromSourceArgs = CreateNodeArgs & {
|
||||
@@ -143,7 +145,9 @@ function getIntersectedPersistedEdge(
|
||||
if (!edgeId) return undefined;
|
||||
|
||||
const edge = edges.find((candidate) => candidate.id === edgeId);
|
||||
if (!edge || edge.className === "temp") return undefined;
|
||||
if (!edge || edge.className === "temp" || isOptimisticEdgeId(edge.id)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return edge;
|
||||
}
|
||||
@@ -253,16 +257,11 @@ export function CanvasPlacementProvider({
|
||||
newNodeSourceHandle: normalizeHandle(handles.source),
|
||||
splitSourceHandle: normalizeHandle(hitEdge.sourceHandle),
|
||||
splitTargetHandle: normalizeHandle(hitEdge.targetHandle),
|
||||
...(clientRequestId !== undefined ? { clientRequestId } : {}),
|
||||
});
|
||||
notifySettled(realId);
|
||||
return realId;
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message === "offline-unsupported"
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
console.error("[Canvas placement] edge split failed", {
|
||||
edgeId: hitEdge.id,
|
||||
type,
|
||||
|
||||
@@ -105,6 +105,7 @@ import {
|
||||
getMiniMapNodeStrokeColor,
|
||||
getNodeCenterClientPosition,
|
||||
getIntersectedEdgeId,
|
||||
getPendingRemovedEdgeIdsFromLocalOps,
|
||||
getPendingMovePinsFromLocalOps,
|
||||
hasHandleKey,
|
||||
inferPendingConnectionNodeHandoff,
|
||||
@@ -408,7 +409,7 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
|
||||
]);
|
||||
});
|
||||
|
||||
const createNodeWithEdgeSplit = useMutation(api.nodes.createWithEdgeSplit);
|
||||
const createNodeWithEdgeSplitMut = useMutation(api.nodes.createWithEdgeSplit);
|
||||
|
||||
const createEdge = useMutation(api.edges.create).withOptimisticUpdate(
|
||||
(localStore, args) => {
|
||||
@@ -445,12 +446,18 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
|
||||
const createNodeWithEdgeToTargetRaw = useMutation(
|
||||
api.nodes.createWithEdgeToTarget,
|
||||
);
|
||||
const createNodeWithEdgeSplitRaw = useMutation(api.nodes.createWithEdgeSplit);
|
||||
const createEdgeRaw = useMutation(api.edges.create);
|
||||
const batchRemoveNodesRaw = useMutation(api.nodes.batchRemove);
|
||||
const removeEdgeRaw = useMutation(api.edges.remove);
|
||||
const splitEdgeAtExistingNodeRaw = useMutation(
|
||||
api.nodes.splitEdgeAtExistingNode,
|
||||
);
|
||||
|
||||
const [nodes, setNodes] = useState<RFNode[]>([]);
|
||||
const [edges, setEdges] = useState<RFEdge[]>([]);
|
||||
const edgesRef = useRef(edges);
|
||||
edgesRef.current = edges;
|
||||
const [pendingSyncCount, setPendingSyncCount] = useState(0);
|
||||
const [isSyncing, setIsSyncing] = useState(false);
|
||||
const [isBrowserOnline, setIsBrowserOnline] = useState(
|
||||
@@ -534,6 +541,84 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
|
||||
return optimisticEdgeId as Id<"edges">;
|
||||
}, []);
|
||||
|
||||
const applyEdgeSplitLocally = useCallback((args: {
|
||||
clientRequestId: string;
|
||||
splitEdgeId: Id<"edges">;
|
||||
middleNodeId: Id<"nodes">;
|
||||
splitSourceHandle?: string;
|
||||
splitTargetHandle?: string;
|
||||
newNodeSourceHandle?: string;
|
||||
newNodeTargetHandle?: string;
|
||||
positionX?: number;
|
||||
positionY?: number;
|
||||
}): boolean => {
|
||||
const splitEdgeId = args.splitEdgeId as string;
|
||||
const splitEdge = edgesRef.current.find(
|
||||
(edge) =>
|
||||
edge.id === splitEdgeId &&
|
||||
edge.className !== "temp" &&
|
||||
!isOptimisticEdgeId(edge.id),
|
||||
);
|
||||
if (!splitEdge) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const optimisticSplitEdgeBase = `${OPTIMISTIC_EDGE_PREFIX}${args.clientRequestId}`;
|
||||
const optimisticSplitEdgeAId = `${optimisticSplitEdgeBase}_split_a`;
|
||||
const optimisticSplitEdgeBId = `${optimisticSplitEdgeBase}_split_b`;
|
||||
|
||||
setEdges((current) => {
|
||||
const existingSplitEdge = current.find((edge) => edge.id === splitEdgeId);
|
||||
if (!existingSplitEdge) {
|
||||
return current;
|
||||
}
|
||||
|
||||
const next = current.filter(
|
||||
(edge) =>
|
||||
edge.id !== splitEdgeId &&
|
||||
edge.id !== optimisticSplitEdgeAId &&
|
||||
edge.id !== optimisticSplitEdgeBId,
|
||||
);
|
||||
|
||||
next.push(
|
||||
{
|
||||
id: optimisticSplitEdgeAId,
|
||||
source: existingSplitEdge.source,
|
||||
target: args.middleNodeId as string,
|
||||
sourceHandle: args.splitSourceHandle,
|
||||
targetHandle: args.newNodeTargetHandle,
|
||||
},
|
||||
{
|
||||
id: optimisticSplitEdgeBId,
|
||||
source: args.middleNodeId as string,
|
||||
target: existingSplitEdge.target,
|
||||
sourceHandle: args.newNodeSourceHandle,
|
||||
targetHandle: args.splitTargetHandle,
|
||||
},
|
||||
);
|
||||
|
||||
return next;
|
||||
});
|
||||
|
||||
if (args.positionX !== undefined && args.positionY !== undefined) {
|
||||
const x = args.positionX;
|
||||
const y = args.positionY;
|
||||
const middleNodeId = args.middleNodeId as string;
|
||||
setNodes((current) =>
|
||||
current.map((node) =>
|
||||
node.id === middleNodeId
|
||||
? {
|
||||
...node,
|
||||
position: { x, y },
|
||||
}
|
||||
: node,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
const removeOptimisticCreateLocally = useCallback((args: {
|
||||
clientRequestId: string;
|
||||
removeNode?: boolean;
|
||||
@@ -555,8 +640,13 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
|
||||
}
|
||||
|
||||
if (args.removeEdge) {
|
||||
const optimisticEdgePrefix = `${optimisticEdgeId}_`;
|
||||
setEdges((current) =>
|
||||
current.filter((edge) => edge.id !== optimisticEdgeId),
|
||||
current.filter(
|
||||
(edge) =>
|
||||
edge.id !== optimisticEdgeId &&
|
||||
!edge.id.startsWith(optimisticEdgePrefix),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -702,14 +792,47 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
|
||||
);
|
||||
|
||||
const runCreateNodeWithEdgeSplitOnlineOnly = useCallback(
|
||||
async (args: Parameters<typeof createNodeWithEdgeSplit>[0]) => {
|
||||
if (!isSyncOnline) {
|
||||
notifyOfflineUnsupported("Kanten-Split");
|
||||
throw new Error("offline-unsupported");
|
||||
async (args: Parameters<typeof createNodeWithEdgeSplitMut>[0]) => {
|
||||
const clientRequestId = args.clientRequestId ?? crypto.randomUUID();
|
||||
const payload = { ...args, clientRequestId };
|
||||
|
||||
if (isSyncOnline) {
|
||||
return await createNodeWithEdgeSplitMut(payload);
|
||||
}
|
||||
return await createNodeWithEdgeSplit(args);
|
||||
|
||||
const optimisticNodeId = addOptimisticNodeLocally(payload);
|
||||
const splitApplied = applyEdgeSplitLocally({
|
||||
clientRequestId,
|
||||
splitEdgeId: payload.splitEdgeId,
|
||||
middleNodeId: optimisticNodeId,
|
||||
splitSourceHandle: payload.splitSourceHandle,
|
||||
splitTargetHandle: payload.splitTargetHandle,
|
||||
newNodeSourceHandle: payload.newNodeSourceHandle,
|
||||
newNodeTargetHandle: payload.newNodeTargetHandle,
|
||||
positionX: payload.positionX,
|
||||
positionY: payload.positionY,
|
||||
});
|
||||
|
||||
if (splitApplied) {
|
||||
await enqueueSyncMutationRef.current("createNodeWithEdgeSplit", payload);
|
||||
} else {
|
||||
await enqueueSyncMutationRef.current("createNode", {
|
||||
canvasId: payload.canvasId,
|
||||
type: payload.type,
|
||||
positionX: payload.positionX,
|
||||
positionY: payload.positionY,
|
||||
width: payload.width,
|
||||
height: payload.height,
|
||||
data: payload.data,
|
||||
parentId: payload.parentId,
|
||||
zIndex: payload.zIndex,
|
||||
clientRequestId,
|
||||
});
|
||||
}
|
||||
|
||||
return optimisticNodeId;
|
||||
},
|
||||
[createNodeWithEdgeSplit, isSyncOnline, notifyOfflineUnsupported],
|
||||
[addOptimisticNodeLocally, applyEdgeSplitLocally, createNodeWithEdgeSplitMut, isSyncOnline],
|
||||
);
|
||||
|
||||
const refreshPendingSyncCount = useCallback(async () => {
|
||||
@@ -771,12 +894,23 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
|
||||
realId,
|
||||
);
|
||||
setEdgeSyncNonce((value) => value + 1);
|
||||
} else if (op.type === "createNodeWithEdgeSplit") {
|
||||
const realId = await createNodeWithEdgeSplitRaw(op.payload);
|
||||
await remapOptimisticNodeLocally(op.payload.clientRequestId, realId);
|
||||
await syncPendingMoveForClientRequestRef.current(
|
||||
op.payload.clientRequestId,
|
||||
realId,
|
||||
);
|
||||
setEdgeSyncNonce((value) => value + 1);
|
||||
} else if (op.type === "createEdge") {
|
||||
await createEdgeRaw(op.payload);
|
||||
} else if (op.type === "removeEdge") {
|
||||
await removeEdgeRaw(op.payload);
|
||||
} else if (op.type === "batchRemoveNodes") {
|
||||
await batchRemoveNodesRaw(op.payload);
|
||||
} else if (op.type === "splitEdgeAtExistingNode") {
|
||||
await splitEdgeAtExistingNodeRaw(op.payload);
|
||||
setEdgeSyncNonce((value) => value + 1);
|
||||
} else if (op.type === "moveNode") {
|
||||
await moveNode(op.payload);
|
||||
} else if (op.type === "resizeNode") {
|
||||
@@ -814,11 +948,24 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
|
||||
removeNode: true,
|
||||
removeEdge: true,
|
||||
});
|
||||
} else if (op.type === "createNodeWithEdgeSplit") {
|
||||
removeOptimisticCreateLocally({
|
||||
clientRequestId: op.payload.clientRequestId,
|
||||
removeNode: true,
|
||||
removeEdge: true,
|
||||
});
|
||||
setEdgeSyncNonce((value) => value + 1);
|
||||
} else if (op.type === "createEdge") {
|
||||
removeOptimisticCreateLocally({
|
||||
clientRequestId: op.payload.clientRequestId,
|
||||
removeEdge: true,
|
||||
});
|
||||
} else if (op.type === "splitEdgeAtExistingNode") {
|
||||
removeOptimisticCreateLocally({
|
||||
clientRequestId: op.payload.clientRequestId,
|
||||
removeEdge: true,
|
||||
});
|
||||
setEdgeSyncNonce((value) => value + 1);
|
||||
}
|
||||
await ackCanvasSyncOp(op.id);
|
||||
resolveCanvasOp(canvasId as string, op.id);
|
||||
@@ -842,6 +989,7 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
|
||||
createEdgeRaw,
|
||||
createNodeRaw,
|
||||
createNodeWithEdgeFromSourceRaw,
|
||||
createNodeWithEdgeSplitRaw,
|
||||
createNodeWithEdgeToTargetRaw,
|
||||
isSyncOnline,
|
||||
moveNode,
|
||||
@@ -850,6 +998,7 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
|
||||
removeEdgeRaw,
|
||||
removeOptimisticCreateLocally,
|
||||
resizeNode,
|
||||
splitEdgeAtExistingNodeRaw,
|
||||
updateNodeData,
|
||||
]);
|
||||
|
||||
@@ -1124,13 +1273,34 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
|
||||
|
||||
const runSplitEdgeAtExistingNodeMutation = useCallback(
|
||||
async (args: Parameters<typeof splitEdgeAtExistingNodeMut>[0]) => {
|
||||
if (!isSyncOnline) {
|
||||
notifyOfflineUnsupported("Kanten-Split");
|
||||
const clientRequestId = args.clientRequestId ?? crypto.randomUUID();
|
||||
const payload = { ...args, clientRequestId };
|
||||
if (isSyncOnline) {
|
||||
await splitEdgeAtExistingNodeMut(payload);
|
||||
return;
|
||||
}
|
||||
await splitEdgeAtExistingNodeMut(args);
|
||||
|
||||
const splitApplied = applyEdgeSplitLocally({
|
||||
clientRequestId,
|
||||
splitEdgeId: payload.splitEdgeId,
|
||||
middleNodeId: payload.middleNodeId,
|
||||
splitSourceHandle: payload.splitSourceHandle,
|
||||
splitTargetHandle: payload.splitTargetHandle,
|
||||
newNodeSourceHandle: payload.newNodeSourceHandle,
|
||||
newNodeTargetHandle: payload.newNodeTargetHandle,
|
||||
positionX: payload.positionX,
|
||||
positionY: payload.positionY,
|
||||
});
|
||||
if (!splitApplied) return;
|
||||
|
||||
await enqueueSyncMutation("splitEdgeAtExistingNode", payload);
|
||||
},
|
||||
[isSyncOnline, notifyOfflineUnsupported, splitEdgeAtExistingNodeMut],
|
||||
[
|
||||
applyEdgeSplitLocally,
|
||||
enqueueSyncMutation,
|
||||
isSyncOnline,
|
||||
splitEdgeAtExistingNodeMut,
|
||||
],
|
||||
);
|
||||
|
||||
/** Freepik-Panel: State canvas-weit, damit es den optimistic_… → Real-ID-Wechsel überlebt. */
|
||||
@@ -1334,8 +1504,6 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
|
||||
return { flowPanOnDrag: panMiddleRight, flowSelectionOnDrag: true };
|
||||
}, [scissorsMode, navTool]);
|
||||
|
||||
const edgesRef = useRef(edges);
|
||||
edgesRef.current = edges;
|
||||
const scissorsModeRef = useRef(scissorsMode);
|
||||
scissorsModeRef.current = scissorsMode;
|
||||
|
||||
@@ -1408,6 +1576,9 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
|
||||
}
|
||||
|
||||
const tempEdges = prev.filter((e) => e.className === "temp");
|
||||
const pendingRemovedEdgeIds = getPendingRemovedEdgeIdsFromLocalOps(
|
||||
canvasId as string,
|
||||
);
|
||||
const sourceTypeByNodeId =
|
||||
convexNodes !== undefined
|
||||
? new Map<string, string>(
|
||||
@@ -1415,15 +1586,17 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
|
||||
)
|
||||
: undefined;
|
||||
const glowMode = resolvedTheme === "dark" ? "dark" : "light";
|
||||
const mapped = convexEdges.map((edge: Doc<"edges">) =>
|
||||
sourceTypeByNodeId
|
||||
? convexEdgeToRFWithSourceGlow(
|
||||
edge,
|
||||
sourceTypeByNodeId.get(edge.sourceNodeId),
|
||||
glowMode,
|
||||
)
|
||||
: convexEdgeToRF(edge),
|
||||
);
|
||||
const mapped = convexEdges
|
||||
.filter((edge: Doc<"edges">) => !pendingRemovedEdgeIds.has(edge._id as string))
|
||||
.map((edge: Doc<"edges">) =>
|
||||
sourceTypeByNodeId
|
||||
? convexEdgeToRFWithSourceGlow(
|
||||
edge,
|
||||
sourceTypeByNodeId.get(edge.sourceNodeId),
|
||||
glowMode,
|
||||
)
|
||||
: convexEdgeToRF(edge),
|
||||
);
|
||||
|
||||
const mappedSignatures = new Set(mapped.map(rfEdgeConnectionSignature));
|
||||
const convexNodeIds =
|
||||
@@ -1562,7 +1735,7 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
|
||||
|
||||
return [...mapped, ...carriedOptimistic, ...tempEdges];
|
||||
});
|
||||
}, [convexEdges, convexNodes, resolvedTheme, edgeSyncNonce]);
|
||||
}, [canvasId, convexEdges, convexNodes, resolvedTheme, edgeSyncNonce]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!convexNodes || isResizing.current) return;
|
||||
@@ -1776,7 +1949,10 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
|
||||
}
|
||||
|
||||
const intersectedEdge = edges.find(
|
||||
(edge) => edge.id === intersectedEdgeId && edge.className !== "temp",
|
||||
(edge) =>
|
||||
edge.id === intersectedEdgeId &&
|
||||
edge.className !== "temp" &&
|
||||
!isOptimisticEdgeId(edge.id),
|
||||
);
|
||||
if (!intersectedEdge) {
|
||||
overlappedEdgeRef.current = null;
|
||||
@@ -1836,7 +2012,9 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
|
||||
const intersectedEdge = intersectedEdgeId
|
||||
? edges.find(
|
||||
(edge) =>
|
||||
edge.id === intersectedEdgeId && edge.className !== "temp",
|
||||
edge.id === intersectedEdgeId &&
|
||||
edge.className !== "temp" &&
|
||||
!isOptimisticEdgeId(edge.id),
|
||||
)
|
||||
: undefined;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user