feat(canvas): enhance edge insertion and local node data handling

- Added support for new edge insertion features, including default edge types and improved layout calculations.
- Introduced local node data persistence during flow reconciliation to ensure data integrity.
- Updated connection drop menu to handle edge insertions and node interactions more effectively.
- Enhanced testing for edge insert layout and local node data management.
This commit is contained in:
2026-04-05 21:26:20 +02:00
parent de37b63b2b
commit 7c34da45b4
24 changed files with 2404 additions and 63 deletions

View File

@@ -4,19 +4,34 @@ import type { Id } from "@/convex/_generated/dataModel";
import {
CANVAS_NODE_DND_MIME,
} from "@/lib/canvas-connection-policy";
import { NODE_DEFAULTS } from "@/lib/canvas-utils";
import { NODE_DEFAULTS, NODE_HANDLE_MAP } from "@/lib/canvas-utils";
import {
isCanvasNodeType,
type CanvasNodeType,
} from "@/lib/canvas-node-types";
import { toast } from "@/lib/toast";
import {
getIntersectedEdgeId,
hasHandleKey,
isOptimisticEdgeId,
logCanvasConnectionDebug,
normalizeHandle,
} from "./canvas-helpers";
import { getImageDimensions } from "./canvas-media-utils";
type UseCanvasDropParams = {
canvasId: Id<"canvases">;
isSyncOnline: boolean;
t: (key: string) => string;
edges: Array<{
id: string;
source: string;
target: string;
className?: string;
sourceHandle?: string | null;
targetHandle?: string | null;
}>;
screenToFlowPosition: (position: { x: number; y: number }) => { x: number; y: number };
generateUploadUrl: () => Promise<string>;
runCreateNodeOnlineOnly: (args: {
@@ -29,6 +44,21 @@ type UseCanvasDropParams = {
data: Record<string, unknown>;
clientRequestId?: string;
}) => Promise<Id<"nodes">>;
runCreateNodeWithEdgeSplitOnlineOnly: (args: {
canvasId: Id<"canvases">;
type: CanvasNodeType;
positionX: number;
positionY: number;
width: number;
height: number;
data: Record<string, unknown>;
splitEdgeId: Id<"edges">;
newNodeTargetHandle?: string;
newNodeSourceHandle?: string;
splitSourceHandle?: string;
splitTargetHandle?: string;
clientRequestId?: string;
}) => Promise<Id<"nodes">>;
notifyOfflineUnsupported: (featureLabel: string) => void;
syncPendingMoveForClientRequest: (
clientRequestId: string,
@@ -66,9 +96,11 @@ export function useCanvasDrop({
canvasId,
isSyncOnline,
t,
edges,
screenToFlowPosition,
generateUploadUrl,
runCreateNodeOnlineOnly,
runCreateNodeWithEdgeSplitOnlineOnly,
notifyOfflineUnsupported,
syncPendingMoveForClientRequest,
}: UseCanvasDropParams) {
@@ -169,23 +201,92 @@ export function useCanvasDrop({
x: event.clientX,
y: event.clientY,
});
const intersectedEdgeId =
typeof document !== "undefined" &&
typeof document.elementsFromPoint === "function"
? getIntersectedEdgeId({
x: event.clientX,
y: event.clientY,
})
: null;
const defaults = NODE_DEFAULTS[parsedPayload.nodeType] ?? {
width: 200,
height: 100,
data: {},
};
const clientRequestId = crypto.randomUUID();
const hitEdge = intersectedEdgeId
? edges.find(
(edge) =>
edge.id === intersectedEdgeId &&
edge.className !== "temp" &&
!isOptimisticEdgeId(edge.id),
)
: undefined;
const handles = NODE_HANDLE_MAP[parsedPayload.nodeType];
const canSplitEdge =
hitEdge !== undefined &&
handles !== undefined &&
hasHandleKey(handles, "source") &&
hasHandleKey(handles, "target");
void runCreateNodeOnlineOnly({
canvasId,
type: parsedPayload.nodeType,
positionX: position.x,
positionY: position.y,
width: defaults.width,
height: defaults.height,
data: { ...defaults.data, ...parsedPayload.payloadData, canvasId },
clientRequestId,
}).then((realId) => {
logCanvasConnectionDebug("node-drop", {
nodeType: parsedPayload.nodeType,
clientPoint: { x: event.clientX, y: event.clientY },
flowPoint: position,
intersectedEdgeId,
hitEdgeId: hitEdge?.id ?? null,
usesEdgeSplitPath: canSplitEdge,
});
const createNodePromise = canSplitEdge
? (() => {
logCanvasConnectionDebug("node-drop:split-edge", {
nodeType: parsedPayload.nodeType,
clientPoint: { x: event.clientX, y: event.clientY },
flowPoint: position,
intersectedEdgeId,
splitEdgeId: hitEdge.id,
});
return runCreateNodeWithEdgeSplitOnlineOnly({
canvasId,
type: parsedPayload.nodeType,
positionX: position.x,
positionY: position.y,
width: defaults.width,
height: defaults.height,
data: { ...defaults.data, ...parsedPayload.payloadData, canvasId },
splitEdgeId: hitEdge.id as Id<"edges">,
newNodeTargetHandle: normalizeHandle(handles.target),
newNodeSourceHandle: normalizeHandle(handles.source),
splitSourceHandle: normalizeHandle(hitEdge.sourceHandle),
splitTargetHandle: normalizeHandle(hitEdge.targetHandle),
clientRequestId,
});
})()
: (() => {
if (intersectedEdgeId) {
logCanvasConnectionDebug("node-drop:edge-detected-no-split", {
nodeType: parsedPayload.nodeType,
clientPoint: { x: event.clientX, y: event.clientY },
flowPoint: position,
intersectedEdgeId,
});
}
return runCreateNodeOnlineOnly({
canvasId,
type: parsedPayload.nodeType,
positionX: position.x,
positionY: position.y,
width: defaults.width,
height: defaults.height,
data: { ...defaults.data, ...parsedPayload.payloadData, canvasId },
clientRequestId,
});
})();
void createNodePromise.then((realId) => {
void syncPendingMoveForClientRequest(clientRequestId, realId).catch(
(error: unknown) => {
console.error("[Canvas] createNode syncPendingMove failed", error);
@@ -195,9 +296,11 @@ export function useCanvasDrop({
},
[
canvasId,
edges,
generateUploadUrl,
isSyncOnline,
notifyOfflineUnsupported,
runCreateNodeWithEdgeSplitOnlineOnly,
runCreateNodeOnlineOnly,
screenToFlowPosition,
syncPendingMoveForClientRequest,