Skip to content

Commit 1705bcb

Browse files
committed
feat(workflow): creates connection on edge dragged over node
1 parent 75f475e commit 1705bcb

File tree

1 file changed

+153
-81
lines changed

1 file changed

+153
-81
lines changed

components/workflow/workflow-canvas.tsx

Lines changed: 153 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ export function WorkflowCanvas() {
101101
useReactFlow();
102102

103103
const connectingNodeId = useRef<string | null>(null);
104+
const connectingHandleType = useRef<"source" | "target" | null>(null);
104105
const justCreatedNodeFromConnection = useRef(false);
105106
const viewportInitialized = useRef(false);
106107
const [isCanvasReady, setIsCanvasReady] = useState(false);
@@ -227,6 +228,27 @@ export function WorkflowCanvas() {
227228
[]
228229
);
229230

231+
const nodeHasHandle = useCallback(
232+
(nodeId: string, handleType: "source" | "target") => {
233+
const node = nodes.find((node) => node.id === nodeId);
234+
235+
if (!node) {
236+
return false;
237+
}
238+
239+
if (node.type === "add") {
240+
return false;
241+
}
242+
243+
if (handleType === "target") {
244+
return node.type !== "trigger";
245+
}
246+
247+
return true;
248+
},
249+
[nodes]
250+
);
251+
230252
const isValidConnection = useCallback(
231253
(connection: XYFlowConnection | XYFlowEdge) => {
232254
// Ensure we have both source and target
@@ -272,6 +294,7 @@ export function WorkflowCanvas() {
272294
const onConnectStart = useCallback(
273295
(_event: MouseEvent | TouchEvent, params: OnConnectStartParams) => {
274296
connectingNodeId.current = params.nodeId;
297+
connectingHandleType.current = params.handleType;
275298
},
276299
[]
277300
);
@@ -306,6 +329,123 @@ export function WorkflowCanvas() {
306329
[]
307330
);
308331

332+
const handleConnectionToExistingNode = useCallback(
333+
(nodeElement: Element) => {
334+
const targetNodeId = nodeElement.getAttribute("data-id");
335+
const fromSource = connectingHandleType.current === "source";
336+
const requiredHandle = fromSource ? "target" : "source";
337+
const connectingId = connectingNodeId.current;
338+
339+
if (
340+
targetNodeId &&
341+
connectingId &&
342+
targetNodeId !== connectingId &&
343+
nodeHasHandle(targetNodeId, requiredHandle)
344+
) {
345+
const sourceId = fromSource ? connectingId : targetNodeId;
346+
const targetId = fromSource ? targetNodeId : connectingId;
347+
onConnect({
348+
source: sourceId,
349+
target: targetId,
350+
sourceHandle: null,
351+
targetHandle: null,
352+
});
353+
}
354+
},
355+
[nodeHasHandle, onConnect]
356+
);
357+
358+
const handleConnectionToNewNode = useCallback(
359+
(event: MouseEvent | TouchEvent, clientX: number, clientY: number) => {
360+
const sourceNodeId = connectingNodeId.current;
361+
if (!sourceNodeId) {
362+
return;
363+
}
364+
365+
const { adjustedX, adjustedY } = calculateMenuPosition(
366+
event,
367+
clientX,
368+
clientY
369+
);
370+
371+
// Get the action template
372+
const actionTemplate = nodeTemplates.find((t) => t.type === "action");
373+
if (!actionTemplate) {
374+
return;
375+
}
376+
377+
// Get the position in the flow coordinate system
378+
const position = screenToFlowPosition({
379+
x: adjustedX,
380+
y: adjustedY,
381+
});
382+
383+
// Center the node vertically at the cursor position
384+
// Node height is 192px (h-48 in Tailwind)
385+
const nodeHeight = 192;
386+
position.y -= nodeHeight / 2;
387+
388+
const newNode: WorkflowNode = {
389+
id: nanoid(),
390+
type: actionTemplate.type,
391+
position,
392+
data: {
393+
label: actionTemplate.label,
394+
description: actionTemplate.description,
395+
type: actionTemplate.type,
396+
config: actionTemplate.defaultConfig,
397+
status: "idle",
398+
},
399+
selected: true,
400+
};
401+
402+
addNode(newNode);
403+
setSelectedNode(newNode.id);
404+
setActiveTab("properties");
405+
406+
// Deselect all other nodes and select only the new node
407+
// Need to do this after a delay because panOnDrag will clear selection
408+
setTimeout(() => {
409+
setNodes((currentNodes) =>
410+
currentNodes.map((n) => ({
411+
...n,
412+
selected: n.id === newNode.id,
413+
}))
414+
);
415+
}, 50);
416+
417+
// Create connection from the source node to the new node
418+
const newEdge = {
419+
id: nanoid(),
420+
source: sourceNodeId,
421+
target: newNode.id,
422+
type: "animated",
423+
};
424+
setEdges([...edges, newEdge]);
425+
setHasUnsavedChanges(true);
426+
// Trigger immediate autosave for the new edge
427+
triggerAutosave({ immediate: true });
428+
429+
// Set flag to prevent immediate deselection
430+
justCreatedNodeFromConnection.current = true;
431+
setTimeout(() => {
432+
justCreatedNodeFromConnection.current = false;
433+
}, 100);
434+
},
435+
[
436+
calculateMenuPosition,
437+
screenToFlowPosition,
438+
addNode,
439+
edges,
440+
setEdges,
441+
setNodes,
442+
setSelectedNode,
443+
setActiveTab,
444+
setHasUnsavedChanges,
445+
triggerAutosave,
446+
]
447+
);
448+
309449
const onConnectEnd = useCallback(
310450
(event: MouseEvent | TouchEvent) => {
311451
if (!connectingNodeId.current) {
@@ -327,96 +467,28 @@ export function WorkflowCanvas() {
327467
return;
328468
}
329469

330-
const isNode = target.closest(".react-flow__node");
470+
const nodeElement = target.closest(".react-flow__node");
331471
const isHandle = target.closest(".react-flow__handle");
332472

333-
if (!(isNode || isHandle)) {
334-
const { adjustedX, adjustedY } = calculateMenuPosition(
335-
event,
336-
clientX,
337-
clientY
338-
);
339-
340-
// Get the action template
341-
const actionTemplate = nodeTemplates.find((t) => t.type === "action");
342-
if (!actionTemplate) {
343-
return;
344-
}
345-
346-
// Get the position in the flow coordinate system
347-
const position = screenToFlowPosition({
348-
x: adjustedX,
349-
y: adjustedY,
350-
});
351-
352-
// Center the node vertically at the cursor position
353-
// Node height is 192px (h-48 in Tailwind)
354-
const nodeHeight = 192;
355-
position.y -= nodeHeight / 2;
473+
// Create connection on edge dragged over node release
474+
if (nodeElement && !isHandle && connectingHandleType.current) {
475+
handleConnectionToExistingNode(nodeElement);
476+
connectingNodeId.current = null;
477+
connectingHandleType.current = null;
478+
return;
479+
}
356480

357-
// Create new action node
358-
const newNode: WorkflowNode = {
359-
id: nanoid(),
360-
type: actionTemplate.type,
361-
position,
362-
data: {
363-
label: actionTemplate.label,
364-
description: actionTemplate.description,
365-
type: actionTemplate.type,
366-
config: actionTemplate.defaultConfig,
367-
status: "idle",
368-
},
369-
selected: true,
370-
};
371-
372-
addNode(newNode);
373-
setSelectedNode(newNode.id);
374-
setActiveTab("properties");
375-
376-
// Deselect all other nodes and select only the new node
377-
// Need to do this after a delay because panOnDrag will clear selection
378-
setTimeout(() => {
379-
setNodes((currentNodes) =>
380-
currentNodes.map((n) => ({
381-
...n,
382-
selected: n.id === newNode.id,
383-
}))
384-
);
385-
}, 50);
386-
387-
// Create connection from the source node to the new node
388-
const newEdge = {
389-
id: nanoid(),
390-
source: connectingNodeId.current,
391-
target: newNode.id,
392-
type: "animated",
393-
};
394-
setEdges([...edges, newEdge]);
395-
setHasUnsavedChanges(true);
396-
// Trigger immediate autosave for the new edge
397-
triggerAutosave({ immediate: true });
398-
399-
// Set flag to prevent immediate deselection
400-
justCreatedNodeFromConnection.current = true;
401-
setTimeout(() => {
402-
justCreatedNodeFromConnection.current = false;
403-
}, 100);
481+
if (!(nodeElement || isHandle)) {
482+
handleConnectionToNewNode(event, clientX, clientY);
404483
}
405484

406485
connectingNodeId.current = null;
486+
connectingHandleType.current = null;
407487
},
408488
[
409489
getClientPosition,
410-
calculateMenuPosition,
411-
screenToFlowPosition,
412-
addNode,
413-
edges,
414-
setEdges,
415-
setNodes,
416-
setSelectedNode,
417-
setActiveTab,
418-
setHasUnsavedChanges,
419-
triggerAutosave,
490+
handleConnectionToExistingNode,
491+
handleConnectionToNewNode,
420492
]
421493
);
422494

0 commit comments

Comments
 (0)