feat: Add ability to drag arrow tip to another node (#14657)

Allow users to drag the target of edges when editing workflows.

---------

Co-authored-by: Devessier <baptiste@devessier.fr>
This commit is contained in:
Harshit Singh
2025-09-29 19:11:41 +05:30
committed by GitHub
parent 9b88cee1ce
commit 59be3aab19
6 changed files with 69 additions and 10 deletions
@@ -41,6 +41,7 @@ import {
type OnBeforeDelete,
type OnDelete,
type OnNodeDrag,
type OnReconnect,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import React, {
@@ -102,6 +103,9 @@ export const WorkflowDiagramCanvasBase = ({
onConnect,
onDeleteEdge,
onNodeDragStop,
onReconnect,
onReconnectStart,
onReconnectEnd,
handlePaneContextMenu,
nodesConnectable = false,
nodesDraggable = false,
@@ -136,6 +140,9 @@ export const WorkflowDiagramCanvasBase = ({
onConnect?: (params: WorkflowConnection) => void;
onDeleteEdge?: (edge: WorkflowDiagramEdge) => void;
onNodeDragStop?: OnNodeDrag<WorkflowDiagramNode>;
onReconnect?: OnReconnect;
onReconnectStart?: () => void;
onReconnectEnd?: () => void;
nodesConnectable?: boolean;
nodesDraggable?: boolean;
handlePaneContextMenu?: ({
@@ -476,6 +483,9 @@ export const WorkflowDiagramCanvasBase = ({
onNodesChange={handleNodesChanges}
onEdgesChange={handleEdgesChange}
onConnect={handleConnect}
onReconnect={onReconnect}
onReconnectStart={onReconnectStart}
onReconnectEnd={onReconnectEnd}
onNodeDragStop={onNodeDragStop}
onBeforeDelete={onBeforeDelete}
onDelete={onDelete}
@@ -11,6 +11,8 @@ import {
type WorkflowDiagramEdge,
type WorkflowDiagramNode,
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { assertEdgeHasDefinedHandlesOrThrow } from '@/workflow/workflow-diagram/utils/assertEdgeHasDefinedHandlesOrThrow';
import { assertWorkflowConnectionOrThrow } from '@/workflow/workflow-diagram/utils/assertWorkflowConnectionOrThrow';
import { getWorkflowVersionStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowVersionStatusTagProps';
import { WorkflowDiagramBlankEdge } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramBlankEdge';
import { WorkflowDiagramDefaultEdgeEditable } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramDefaultEdgeEditable';
@@ -21,8 +23,13 @@ import { useCreateEdge } from '@/workflow/workflow-steps/hooks/useCreateEdge';
import { useDeleteEdge } from '@/workflow/workflow-steps/hooks/useDeleteEdge';
import { useUpdateStep } from '@/workflow/workflow-steps/hooks/useUpdateStep';
import { useUpdateWorkflowVersionTrigger } from '@/workflow/workflow-trigger/hooks/useUpdateWorkflowVersionTrigger';
import { addEdge, ReactFlowProvider } from '@xyflow/react';
import React from 'react';
import {
addEdge,
ReactFlowProvider,
type Connection,
type Edge,
} from '@xyflow/react';
import React, { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
export const WorkflowDiagramCanvasEditable = () => {
@@ -73,6 +80,30 @@ export const WorkflowDiagramCanvasEditable = () => {
});
};
const handleReconnect = useCallback(
async (oldEdge: Edge, connection: Connection) => {
assertEdgeHasDefinedHandlesOrThrow(oldEdge);
assertWorkflowConnectionOrThrow(connection);
await deleteEdge({
source: oldEdge.source,
target: oldEdge.target,
sourceConnectionOptions: getConnectionOptionsForSourceHandle({
sourceHandleId: oldEdge.sourceHandle,
}),
});
await createEdge({
source: connection.source,
target: connection.target,
connectionOptions: getConnectionOptionsForSourceHandle({
sourceHandleId: connection.sourceHandle,
}),
});
},
[deleteEdge, createEdge],
);
const onDeleteEdge = async (edge: WorkflowDiagramEdge) => {
await deleteEdge({
source: edge.source,
@@ -140,6 +171,7 @@ export const WorkflowDiagramCanvasEditable = () => {
tagColor={tagProps.color}
tagText={tagProps.text}
onConnect={onConnect}
onReconnect={handleReconnect}
onNodeDragStop={onNodeDragStop}
handlePaneContextMenu={handlePaneContextMenu}
nodesConnectable
@@ -159,6 +159,7 @@ describe('getWorkflowVersionDiagram', () => {
"id": "8f3b2121-f194-4ba4-9fbf-0",
"markerEnd": "edge-branch-arrow-default",
"markerStart": undefined,
"reconnectable": "target",
"selectable": true,
"source": "trigger",
"sourceHandle": "default",
@@ -79,8 +79,12 @@ export const generateNodesAndEdgesForDefaultNode = ({
nextStepId,
}),
},
...(edgeTypeBetweenTwoNodes.includes('editable')
? { deletable: true, selectable: true }
...(workflowContext === 'workflow'
? {
deletable: true,
selectable: true,
reconnectable: 'target',
}
: {}),
});
}
@@ -91,8 +91,12 @@ export const generateNodesAndEdgesForIteratorNode = ({
source: step.id,
sourceHandle: WORKFLOW_DIAGRAM_ITERATOR_NODE_LOOP_HANDLE_ID,
target: initialLoopStepId,
...(edgeTypeBetweenTwoNodes.includes('editable')
? { deletable: true, selectable: true }
...(workflowContext === 'workflow'
? {
deletable: true,
selectable: true,
reconnectable: 'target',
}
: {}),
targetHandle: WORKFLOW_DIAGRAM_NODE_DEFAULT_TARGET_HANDLE_ID,
data: {
@@ -115,8 +119,12 @@ export const generateNodesAndEdgesForIteratorNode = ({
source: step.id,
sourceHandle: WORKFLOW_DIAGRAM_NODE_DEFAULT_SOURCE_HANDLE_ID,
target: nextStepId,
...(edgeTypeBetweenTwoNodes.includes('editable')
? { deletable: true, selectable: true }
...(workflowContext === 'workflow'
? {
deletable: true,
selectable: true,
reconnectable: 'target',
}
: {}),
targetHandle: WORKFLOW_DIAGRAM_NODE_DEFAULT_TARGET_HANDLE_ID,
data: {
@@ -52,8 +52,12 @@ export const generateWorkflowDiagram = ({
source: TRIGGER_STEP_ID,
sourceHandle: WORKFLOW_DIAGRAM_NODE_DEFAULT_SOURCE_HANDLE_ID,
target: stepLinkToTriggerId,
...(edgeTypeBetweenTwoNodes.includes('editable')
? { deletable: true, selectable: true }
...(workflowContext === 'workflow'
? {
deletable: true,
selectable: true,
reconnectable: 'target',
}
: {}),
targetHandle: WORKFLOW_DIAGRAM_NODE_DEFAULT_TARGET_HANDLE_ID,
});