fix(canvas): use resolved ids for drag edge splits
This commit is contained in:
@@ -32,6 +32,7 @@ type HarnessProps = {
|
|||||||
runSplitEdgeAtExistingNodeMutation: ReturnType<typeof vi.fn>;
|
runSplitEdgeAtExistingNodeMutation: ReturnType<typeof vi.fn>;
|
||||||
onInvalidConnection: ReturnType<typeof vi.fn<(reason: CanvasConnectionValidationReason) => void>>;
|
onInvalidConnection: ReturnType<typeof vi.fn<(reason: CanvasConnectionValidationReason) => void>>;
|
||||||
syncPendingMoveForClientRequest: ReturnType<typeof vi.fn>;
|
syncPendingMoveForClientRequest: ReturnType<typeof vi.fn>;
|
||||||
|
resolvedRealIdEntries?: Array<[string, Id<"nodes">]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const latestHandlersRef: {
|
const latestHandlersRef: {
|
||||||
@@ -48,7 +49,9 @@ function HookHarness(props: HarnessProps) {
|
|||||||
const pendingLocalPositionUntilConvexMatchesRef = useRef(new Map());
|
const pendingLocalPositionUntilConvexMatchesRef = useRef(new Map());
|
||||||
const preferLocalPositionNodeIdsRef = useRef(new Set<string>());
|
const preferLocalPositionNodeIdsRef = useRef(new Set<string>());
|
||||||
const pendingMoveAfterCreateRef = useRef(new Map());
|
const pendingMoveAfterCreateRef = useRef(new Map());
|
||||||
const resolvedRealIdByClientRequestRef = useRef(new Map());
|
const resolvedRealIdByClientRequestRef = useRef(
|
||||||
|
new Map(props.resolvedRealIdEntries ?? []),
|
||||||
|
);
|
||||||
const pendingEdgeSplitByClientRequestRef = useRef(new Map());
|
const pendingEdgeSplitByClientRequestRef = useRef(new Map());
|
||||||
|
|
||||||
const handlers = useCanvasNodeInteractions({
|
const handlers = useCanvasNodeInteractions({
|
||||||
@@ -159,4 +162,133 @@ describe("useCanvasNodeInteractions", () => {
|
|||||||
positionY: 180,
|
positionY: 180,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not split an edge that already touches a resolved optimistic node", async () => {
|
||||||
|
const runMoveNodeMutation = vi.fn(async () => undefined);
|
||||||
|
const runBatchMoveNodesMutation = vi.fn(async () => undefined);
|
||||||
|
const runResizeNodeMutation = vi.fn(async () => undefined);
|
||||||
|
const runSplitEdgeAtExistingNodeMutation = vi.fn(async () => undefined);
|
||||||
|
const onInvalidConnection = vi.fn<(reason: CanvasConnectionValidationReason) => void>();
|
||||||
|
const syncPendingMoveForClientRequest = vi.fn(async () => undefined);
|
||||||
|
|
||||||
|
container = document.createElement("div");
|
||||||
|
document.body.appendChild(container);
|
||||||
|
root = createRoot(container);
|
||||||
|
|
||||||
|
const draggedNode: RFNode = {
|
||||||
|
id: "optimistic_req-1",
|
||||||
|
type: "image",
|
||||||
|
position: { x: 320, y: 180 },
|
||||||
|
data: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root?.render(
|
||||||
|
<HookHarness
|
||||||
|
nodes={[
|
||||||
|
draggedNode,
|
||||||
|
{ id: "node-real", type: "image", position: { x: 0, y: 0 }, data: {} },
|
||||||
|
{ id: "node-text", type: "text", position: { x: 400, y: 120 }, data: {} },
|
||||||
|
]}
|
||||||
|
edges={[
|
||||||
|
{
|
||||||
|
id: "edge-image-curves",
|
||||||
|
source: "node-real",
|
||||||
|
target: "node-text",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
runMoveNodeMutation={runMoveNodeMutation}
|
||||||
|
runBatchMoveNodesMutation={runBatchMoveNodesMutation}
|
||||||
|
runResizeNodeMutation={runResizeNodeMutation}
|
||||||
|
runSplitEdgeAtExistingNodeMutation={runSplitEdgeAtExistingNodeMutation}
|
||||||
|
onInvalidConnection={onInvalidConnection}
|
||||||
|
syncPendingMoveForClientRequest={syncPendingMoveForClientRequest}
|
||||||
|
resolvedRealIdEntries={[["req-1", "node-real" as Id<"nodes">]]}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
latestHandlersRef.current?.onNodeDrag({} as React.MouseEvent, draggedNode);
|
||||||
|
latestHandlersRef.current?.onNodeDragStop(
|
||||||
|
{} as React.MouseEvent,
|
||||||
|
draggedNode,
|
||||||
|
[draggedNode],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(runSplitEdgeAtExistingNodeMutation).not.toHaveBeenCalled();
|
||||||
|
expect(onInvalidConnection).not.toHaveBeenCalled();
|
||||||
|
expect(syncPendingMoveForClientRequest).toHaveBeenCalledWith("req-1");
|
||||||
|
expect(runMoveNodeMutation).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still splits a valid edge with the resolved optimistic node id", async () => {
|
||||||
|
const runMoveNodeMutation = vi.fn(async () => undefined);
|
||||||
|
const runBatchMoveNodesMutation = vi.fn(async () => undefined);
|
||||||
|
const runResizeNodeMutation = vi.fn(async () => undefined);
|
||||||
|
const runSplitEdgeAtExistingNodeMutation = vi.fn(async () => undefined);
|
||||||
|
const onInvalidConnection = vi.fn<(reason: CanvasConnectionValidationReason) => void>();
|
||||||
|
const syncPendingMoveForClientRequest = vi.fn(async () => undefined);
|
||||||
|
|
||||||
|
container = document.createElement("div");
|
||||||
|
document.body.appendChild(container);
|
||||||
|
root = createRoot(container);
|
||||||
|
|
||||||
|
const draggedNode: RFNode = {
|
||||||
|
id: "optimistic_req-2",
|
||||||
|
type: "video",
|
||||||
|
position: { x: 320, y: 180 },
|
||||||
|
data: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root?.render(
|
||||||
|
<HookHarness
|
||||||
|
nodes={[
|
||||||
|
draggedNode,
|
||||||
|
{ id: "node-image", type: "image", position: { x: 0, y: 0 }, data: {} },
|
||||||
|
{ id: "node-text", type: "text", position: { x: 400, y: 120 }, data: {} },
|
||||||
|
{ id: "node-real-middle", type: "video", position: { x: 320, y: 180 }, data: {} },
|
||||||
|
]}
|
||||||
|
edges={[
|
||||||
|
{
|
||||||
|
id: "edge-image-curves",
|
||||||
|
source: "node-image",
|
||||||
|
target: "node-text",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
runMoveNodeMutation={runMoveNodeMutation}
|
||||||
|
runBatchMoveNodesMutation={runBatchMoveNodesMutation}
|
||||||
|
runResizeNodeMutation={runResizeNodeMutation}
|
||||||
|
runSplitEdgeAtExistingNodeMutation={runSplitEdgeAtExistingNodeMutation}
|
||||||
|
onInvalidConnection={onInvalidConnection}
|
||||||
|
syncPendingMoveForClientRequest={syncPendingMoveForClientRequest}
|
||||||
|
resolvedRealIdEntries={[["req-2", "node-real-middle" as Id<"nodes">]]}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
latestHandlersRef.current?.onNodeDrag({} as React.MouseEvent, draggedNode);
|
||||||
|
latestHandlersRef.current?.onNodeDragStop(
|
||||||
|
{} as React.MouseEvent,
|
||||||
|
draggedNode,
|
||||||
|
[draggedNode],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(runSplitEdgeAtExistingNodeMutation).toHaveBeenCalledWith({
|
||||||
|
canvasId: "canvas-1",
|
||||||
|
splitEdgeId: "edge-image-curves",
|
||||||
|
middleNodeId: "node-real-middle",
|
||||||
|
splitSourceHandle: undefined,
|
||||||
|
splitTargetHandle: undefined,
|
||||||
|
newNodeSourceHandle: undefined,
|
||||||
|
newNodeTargetHandle: undefined,
|
||||||
|
positionX: 320,
|
||||||
|
positionY: 180,
|
||||||
|
});
|
||||||
|
expect(onInvalidConnection).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -190,6 +190,27 @@ export function useCanvasNodeInteractions(args: {
|
|||||||
setHighlightedIntersectionEdge(null);
|
setHighlightedIntersectionEdge(null);
|
||||||
}, [setHighlightedIntersectionEdge]);
|
}, [setHighlightedIntersectionEdge]);
|
||||||
|
|
||||||
|
const getEffectiveSplitMiddleNode = useCallback(
|
||||||
|
(node: RFNode): RFNode => {
|
||||||
|
const clientRequestId = clientRequestIdFromOptimisticNodeId(node.id);
|
||||||
|
if (!clientRequestId) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedRealId =
|
||||||
|
resolvedRealIdByClientRequestRef.current.get(clientRequestId);
|
||||||
|
if (!resolvedRealId) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
id: resolvedRealId,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[resolvedRealIdByClientRequestRef],
|
||||||
|
);
|
||||||
|
|
||||||
const onNodesChange = useCallback(
|
const onNodesChange = useCallback(
|
||||||
(changes: NodeChange[]) => {
|
(changes: NodeChange[]) => {
|
||||||
for (const change of changes) {
|
for (const change of changes) {
|
||||||
@@ -285,7 +306,12 @@ export function useCanvasNodeInteractions(args: {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (intersectedEdge.source === node.id || intersectedEdge.target === node.id) {
|
const effectiveMiddleNode = getEffectiveSplitMiddleNode(node);
|
||||||
|
|
||||||
|
if (
|
||||||
|
intersectedEdge.source === effectiveMiddleNode.id ||
|
||||||
|
intersectedEdge.target === effectiveMiddleNode.id
|
||||||
|
) {
|
||||||
clearHighlightedIntersectionEdge();
|
clearHighlightedIntersectionEdge();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -299,7 +325,12 @@ export function useCanvasNodeInteractions(args: {
|
|||||||
overlappedEdgeRef.current = intersectedEdge.id;
|
overlappedEdgeRef.current = intersectedEdge.id;
|
||||||
setHighlightedIntersectionEdge(intersectedEdge.id);
|
setHighlightedIntersectionEdge(intersectedEdge.id);
|
||||||
},
|
},
|
||||||
[clearHighlightedIntersectionEdge, edges, setHighlightedIntersectionEdge],
|
[
|
||||||
|
clearHighlightedIntersectionEdge,
|
||||||
|
edges,
|
||||||
|
getEffectiveSplitMiddleNode,
|
||||||
|
setHighlightedIntersectionEdge,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onNodeDragStop = useCallback(
|
const onNodeDragStop = useCallback(
|
||||||
@@ -315,6 +346,7 @@ export function useCanvasNodeInteractions(args: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const effectivePrimaryNode = getEffectiveSplitMiddleNode(primaryNode);
|
||||||
const intersectedEdge = intersectedEdgeId
|
const intersectedEdge = intersectedEdgeId
|
||||||
? edges.find(
|
? edges.find(
|
||||||
(edge) =>
|
(edge) =>
|
||||||
@@ -328,8 +360,8 @@ export function useCanvasNodeInteractions(args: {
|
|||||||
const splitEligible =
|
const splitEligible =
|
||||||
intersectedEdge !== undefined &&
|
intersectedEdge !== undefined &&
|
||||||
splitHandles !== undefined &&
|
splitHandles !== undefined &&
|
||||||
intersectedEdge.source !== primaryNode.id &&
|
intersectedEdge.source !== effectivePrimaryNode.id &&
|
||||||
intersectedEdge.target !== primaryNode.id &&
|
intersectedEdge.target !== effectivePrimaryNode.id &&
|
||||||
hasHandleKey(splitHandles, "source") &&
|
hasHandleKey(splitHandles, "source") &&
|
||||||
hasHandleKey(splitHandles, "target");
|
hasHandleKey(splitHandles, "target");
|
||||||
|
|
||||||
@@ -339,7 +371,7 @@ export function useCanvasNodeInteractions(args: {
|
|||||||
nodes,
|
nodes,
|
||||||
edges,
|
edges,
|
||||||
splitEdge: intersectedEdge,
|
splitEdge: intersectedEdge,
|
||||||
middleNode: primaryNode,
|
middleNode: effectivePrimaryNode,
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@@ -383,7 +415,7 @@ export function useCanvasNodeInteractions(args: {
|
|||||||
const multiClientRequestId = clientRequestIdFromOptimisticNodeId(
|
const multiClientRequestId = clientRequestIdFromOptimisticNodeId(
|
||||||
primaryNode.id,
|
primaryNode.id,
|
||||||
);
|
);
|
||||||
let middleId = primaryNode.id as Id<"nodes">;
|
let middleId = effectivePrimaryNode.id as Id<"nodes">;
|
||||||
if (multiClientRequestId) {
|
if (multiClientRequestId) {
|
||||||
const resolvedId =
|
const resolvedId =
|
||||||
resolvedRealIdByClientRequestRef.current.get(multiClientRequestId);
|
resolvedRealIdByClientRequestRef.current.get(multiClientRequestId);
|
||||||
@@ -518,6 +550,7 @@ export function useCanvasNodeInteractions(args: {
|
|||||||
onInvalidConnection,
|
onInvalidConnection,
|
||||||
pendingEdgeSplitByClientRequestRef,
|
pendingEdgeSplitByClientRequestRef,
|
||||||
pendingMoveAfterCreateRef,
|
pendingMoveAfterCreateRef,
|
||||||
|
getEffectiveSplitMiddleNode,
|
||||||
resolvedRealIdByClientRequestRef,
|
resolvedRealIdByClientRequestRef,
|
||||||
runBatchMoveNodesMutation,
|
runBatchMoveNodesMutation,
|
||||||
runMoveNodeMutation,
|
runMoveNodeMutation,
|
||||||
|
|||||||
Reference in New Issue
Block a user