@@ -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 ( ( n ) => n . 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