From fffff751569c5a018539547b6c980fae426370d6 Mon Sep 17 00:00:00 2001 From: Baptiste Devessier Date: Fri, 12 Sep 2025 13:30:31 +0200 Subject: [PATCH] Fix edge hovering and selection (#14416) ## Before https://github.com/user-attachments/assets/416160bb-8289-43cb-bf0d-d2da053b2056 ## After https://github.com/user-attachments/assets/d1e3bcfc-e1cb-46d1-a7c8-9d0a32993f6d --- .../components/WorkflowDiagramCanvasBase.tsx | 19 ++++- .../WorkflowDiagramCanvasEditable.tsx | 14 +++- .../WorkflowDiagramCanvasEditableEffect.tsx | 5 ++ .../workflow-diagram/types/WorkflowDiagram.ts | 17 +++- .../generateWorkflowRunDiagram.test.ts | 28 +++++++ .../getWorkflowVersionDiagram.test.ts | 2 + .../transformFilterNodesAsEdges.test.ts | 28 +++++++ .../assertEdgeHasDefinedHandlesOrThrow.ts | 18 ++++ .../utils/assertWorkflowConnectionOrThrow.ts | 19 +++++ .../generateNodesAndEdgesForDefaultNode.ts | 4 + .../generateNodesAndEdgesForIteratorNode.ts | 8 ++ .../utils/generateWorkflowDiagram.ts | 4 + .../components/WorkflowDiagramBaseEdge.tsx | 31 +++++-- .../WorkflowDiagramDefaultEdgeEditable.tsx | 30 +++++-- .../WorkflowDiagramDefaultEdgeReadonly.tsx | 7 +- .../WorkflowDiagramDefaultEdgeRun.tsx | 6 +- .../WorkflowDiagramFilterEdgeEditable.tsx | 32 ++++--- .../WorkflowDiagramFilterEdgeReadonly.tsx | 15 +--- .../WorkflowDiagramFilterEdgeRun.tsx | 14 +--- .../components/WorkflowRunDiagramBaseEdge.tsx | 6 +- .../workflow-edges/hooks/useEdgeState.ts | 84 +++++++++++++++---- .../workflowHoveredEdgeComponentState.ts | 4 +- .../workflowSelectedEdgeComponentState.ts | 4 +- .../types/WorkflowDiagramEdge.ts | 4 - .../WorkflowDiagramEdgeComponentProps.ts | 8 ++ .../types/WorkflowDiagramEdgeDescriptor.ts | 6 ++ .../WorkflowDiagramHandleTarget.tsx | 2 + ...flowDiagramIteratorEmptyActionEditable.tsx | 14 +++- ...WorkflowDiagramStepNodeEditableContent.tsx | 27 ++++-- .../WorkflowDiagramStepNodeReadonly.tsx | 2 + .../components/WorkflowRunDiagramStepNode.tsx | 2 + ...orkflowDiagramNodeDefaultSourceHandleId.ts | 1 + ...orkflowDiagramNodeDefaultTargetHandleId.ts | 1 + .../workflow-steps/hooks/useCreateEdge.ts | 7 +- .../workflow-steps/hooks/useDeleteEdge.ts | 7 +- 35 files changed, 375 insertions(+), 105 deletions(-) create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/utils/assertEdgeHasDefinedHandlesOrThrow.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/utils/assertWorkflowConnectionOrThrow.ts delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdge.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdgeComponentProps.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdgeDescriptor.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/constants/WorkflowDiagramNodeDefaultSourceHandleId.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/constants/WorkflowDiagramNodeDefaultTargetHandleId.ts diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase.tsx index 49cde021fc..6e32b7aee2 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase.tsx @@ -12,15 +12,18 @@ import { workflowDiagramPanOnDragComponentState } from '@/workflow/workflow-diag import { workflowDiagramWaitingNodesDimensionsComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramWaitingNodesDimensionsComponentState'; import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState'; import { + type WorkflowConnection, type WorkflowDiagram, type WorkflowDiagramEdge, type WorkflowDiagramEdgeType, type WorkflowDiagramNode, type WorkflowDiagramNodeType, } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { assertWorkflowConnectionOrThrow } from '@/workflow/workflow-diagram/utils/assertWorkflowConnectionOrThrow'; import { WorkflowDiagramConnection } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramConnection'; import { WorkflowDiagramCustomMarkers } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramCustomMarkers'; import { useEdgeState } from '@/workflow/workflow-diagram/workflow-edges/hooks/useEdgeState'; +import { type WorkflowDiagramEdgeComponentProps } from '@/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdgeComponentProps'; import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; @@ -32,7 +35,6 @@ import { useReactFlow, type Connection, type EdgeChange, - type EdgeProps, type FitViewOptions, type NodeChange, type NodeProps, @@ -119,7 +121,7 @@ export const WorkflowDiagramCanvasBase = ({ Record< WorkflowDiagramEdgeType, React.ComponentType< - EdgeProps & { + WorkflowDiagramEdgeComponentProps & { data: any; type: any; } @@ -131,7 +133,7 @@ export const WorkflowDiagramCanvasBase = ({ tagColor: TagColor; tagText: string; onInit?: () => void; - onConnect?: (params: Connection) => void; + onConnect?: (params: WorkflowConnection) => void; onDeleteEdge?: (edge: WorkflowDiagramEdge) => void; onNodeDragStop?: OnNodeDrag; nodesConnectable?: boolean; @@ -438,6 +440,8 @@ export const WorkflowDiagramCanvasBase = ({ setEdgeHovered({ source: hoveredEdge.source, target: hoveredEdge.target, + sourceHandle: hoveredEdge.sourceHandle, + targetHandle: hoveredEdge.targetHandle, }); }, [setEdgeHovered], @@ -447,6 +451,12 @@ export const WorkflowDiagramCanvasBase = ({ clearEdgeHover(); }, [clearEdgeHover]); + const handleConnect = (connection: Connection) => { + assertWorkflowConnectionOrThrow(connection); + + onConnect?.(connection); + }; + return ( @@ -457,6 +467,7 @@ export const WorkflowDiagramCanvasBase = ({ maxZoom={defaultFitViewOptions.maxZoom} defaultViewport={{ x: 0, y: 150, zoom: defaultFitViewOptions.maxZoom }} nodeTypes={nodeTypes} + // @ts-expect-error We override Reactflow types for sourceHandle and targetHandle to be required edgeTypes={edgeTypes} nodes={nodes} edges={edges} @@ -464,7 +475,7 @@ export const WorkflowDiagramCanvasBase = ({ onEdgeMouseLeave={onEdgeMouseLeave} onNodesChange={handleNodesChanges} onEdgesChange={handleEdgesChange} - onConnect={onConnect} + onConnect={handleConnect} onNodeDragStop={onNodeDragStop} onBeforeDelete={onBeforeDelete} onDelete={onDelete} diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditable.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditable.tsx index d2d7ba3625..166bb0e675 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditable.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditable.tsx @@ -7,6 +7,7 @@ import { WorkflowDiagramCanvasEditableEffect } from '@/workflow/workflow-diagram import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState'; import { workflowDiagramRightClickMenuPositionState } from '@/workflow/workflow-diagram/states/workflowDiagramRightClickMenuPositionState'; import { + type WorkflowConnection, type WorkflowDiagramEdge, type WorkflowDiagramNode, } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; @@ -21,7 +22,7 @@ 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, type Connection, ReactFlowProvider } from '@xyflow/react'; +import { addEdge, ReactFlowProvider } from '@xyflow/react'; import React from 'react'; import { isDefined } from 'twenty-shared/utils'; @@ -50,7 +51,7 @@ export const WorkflowDiagramCanvasEditable = () => { const { updateTrigger } = useUpdateWorkflowVersionTrigger(); - const onConnect = (edgeConnect: Connection) => { + const onConnect = (edgeConnect: WorkflowConnection) => { setWorkflowDiagram((diagram) => { if (isDefined(diagram) === false) { throw new Error( @@ -60,7 +61,7 @@ export const WorkflowDiagramCanvasEditable = () => { return { ...diagram, - edges: addEdge(edgeConnect, diagram.edges), + edges: addEdge(edgeConnect, diagram.edges), }; }); @@ -68,7 +69,12 @@ export const WorkflowDiagramCanvasEditable = () => { }; const onDeleteEdge = async (edge: WorkflowDiagramEdge) => { - await deleteEdge(edge); + await deleteEdge({ + source: edge.source, + target: edge.target, + sourceHandle: edge.sourceHandle, + targetHandle: edge.targetHandle, + }); }; const onNodeDragStop = async ( diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditableEffect.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditableEffect.tsx index 229e79d61b..cc7be9c682 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditableEffect.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditableEffect.tsx @@ -1,3 +1,4 @@ +import { assertEdgeHasDefinedHandlesOrThrow } from '@/workflow/workflow-diagram/utils/assertEdgeHasDefinedHandlesOrThrow'; import { useEdgeState } from '@/workflow/workflow-diagram/workflow-edges/hooks/useEdgeState'; import { type OnSelectionChangeParams, @@ -19,9 +20,13 @@ export const WorkflowDiagramCanvasEditableEffect = () => { return; } + assertEdgeHasDefinedHandlesOrThrow(selectedEdge); + setEdgeSelected({ source: selectedEdge.source, target: selectedEdge.target, + sourceHandle: selectedEdge.sourceHandle, + targetHandle: selectedEdge.targetHandle, }); }, [setEdgeSelected, clearEdgeSelected], diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/types/WorkflowDiagram.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/types/WorkflowDiagram.ts index ab09f3db3a..f94282dd21 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/types/WorkflowDiagram.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/types/WorkflowDiagram.ts @@ -6,12 +6,25 @@ import { import { type WorkflowDiagramIteratorEmptyActionNodeData } from '@/workflow/workflow-diagram/workflow-iterator/types/WorkflowDiagramIteratorEmptyActionNodeData'; import { type FilterSettings } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter'; import { type MessageDescriptor } from '@lingui/core'; -import { type Edge, type Node, type Position } from '@xyflow/react'; +import { + type Connection, + type Edge, + type Node, + type Position, +} from '@xyflow/react'; import { type StepStatus } from 'twenty-shared/workflow'; export type WorkflowDiagramStepNode = Node; export type WorkflowDiagramNode = Node; -export type WorkflowDiagramEdge = Edge; +export type WorkflowDiagramEdge = Edge & { + sourceHandle: string; + targetHandle: string; +}; + +export type WorkflowConnection = Connection & { + sourceHandle: string; + targetHandle: string; +}; export type WorkflowRunDiagramNode = Node; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/generateWorkflowRunDiagram.test.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/generateWorkflowRunDiagram.test.ts index 971a9569e6..a2ea74ad89 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/generateWorkflowRunDiagram.test.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/generateWorkflowRunDiagram.test.ts @@ -121,7 +121,9 @@ describe('generateWorkflowRunDiagram', () => { "markerStart": undefined, "selectable": false, "source": "trigger", + "sourceHandle": "default", "target": "step1", + "targetHandle": "default", "type": "empty-filter--run", "zIndex": -2, }, @@ -136,7 +138,9 @@ describe('generateWorkflowRunDiagram', () => { "markerStart": undefined, "selectable": false, "source": "step1", + "sourceHandle": "default", "target": "step2", + "targetHandle": "default", "type": "empty-filter--run", "zIndex": -2, }, @@ -151,7 +155,9 @@ describe('generateWorkflowRunDiagram', () => { "markerStart": undefined, "selectable": false, "source": "step2", + "sourceHandle": "default", "target": "step3", + "targetHandle": "default", "type": "empty-filter--run", "zIndex": -2, }, @@ -352,7 +358,9 @@ describe('generateWorkflowRunDiagram', () => { "markerStart": undefined, "selectable": false, "source": "trigger", + "sourceHandle": "default", "target": "step1", + "targetHandle": "default", "type": "empty-filter--run", "zIndex": -2, }, @@ -367,7 +375,9 @@ describe('generateWorkflowRunDiagram', () => { "markerStart": undefined, "selectable": false, "source": "step1", + "sourceHandle": "default", "target": "step2", + "targetHandle": "default", "type": "empty-filter--run", "zIndex": -2, }, @@ -382,7 +392,9 @@ describe('generateWorkflowRunDiagram', () => { "markerStart": undefined, "selectable": false, "source": "step2", + "sourceHandle": "default", "target": "step3", + "targetHandle": "default", "type": "empty-filter--run", "zIndex": -2, }, @@ -583,7 +595,9 @@ describe('generateWorkflowRunDiagram', () => { "markerStart": undefined, "selectable": false, "source": "trigger", + "sourceHandle": "default", "target": "step1", + "targetHandle": "default", "type": "empty-filter--run", "zIndex": -2, }, @@ -598,7 +612,9 @@ describe('generateWorkflowRunDiagram', () => { "markerStart": undefined, "selectable": false, "source": "step1", + "sourceHandle": "default", "target": "step2", + "targetHandle": "default", "type": "empty-filter--run", "zIndex": -2, }, @@ -613,7 +629,9 @@ describe('generateWorkflowRunDiagram', () => { "markerStart": undefined, "selectable": false, "source": "step2", + "sourceHandle": "default", "target": "step3", + "targetHandle": "default", "type": "empty-filter--run", "zIndex": -2, }, @@ -833,7 +851,9 @@ describe('generateWorkflowRunDiagram', () => { "markerStart": undefined, "selectable": false, "source": "trigger", + "sourceHandle": "default", "target": "step1", + "targetHandle": "default", "type": "empty-filter--run", "zIndex": -2, }, @@ -848,7 +868,9 @@ describe('generateWorkflowRunDiagram', () => { "markerStart": undefined, "selectable": false, "source": "step1", + "sourceHandle": "default", "target": "step2", + "targetHandle": "default", "type": "empty-filter--run", "zIndex": -2, }, @@ -863,7 +885,9 @@ describe('generateWorkflowRunDiagram', () => { "markerStart": undefined, "selectable": false, "source": "step2", + "sourceHandle": "default", "target": "step3", + "targetHandle": "default", "type": "empty-filter--run", "zIndex": -2, }, @@ -878,7 +902,9 @@ describe('generateWorkflowRunDiagram', () => { "markerStart": undefined, "selectable": false, "source": "step3", + "sourceHandle": "default", "target": "step4", + "targetHandle": "default", "type": "empty-filter--run", "zIndex": -2, }, @@ -1064,7 +1090,9 @@ describe('generateWorkflowRunDiagram', () => { "markerStart": undefined, "selectable": false, "source": "trigger", + "sourceHandle": "default", "target": "step1", + "targetHandle": "default", "type": "empty-filter--run", "zIndex": -2, }, diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/getWorkflowVersionDiagram.test.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/getWorkflowVersionDiagram.test.ts index f590001f25..61b7175090 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/getWorkflowVersionDiagram.test.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/getWorkflowVersionDiagram.test.ts @@ -161,7 +161,9 @@ describe('getWorkflowVersionDiagram', () => { "markerStart": undefined, "selectable": true, "source": "trigger", + "sourceHandle": "default", "target": "step-1", + "targetHandle": "default", "type": "empty-filter--editable", "zIndex": -2, }, diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/transformFilterNodesAsEdges.test.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/transformFilterNodesAsEdges.test.ts index d31a4b12f1..112bc58533 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/transformFilterNodesAsEdges.test.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/transformFilterNodesAsEdges.test.ts @@ -34,7 +34,9 @@ describe('transformFilterNodesAsEdges', () => { { id: 'A-C', source: 'A', + sourceHandle: 'default', target: 'C', + targetHandle: 'default', data: { edgeType: 'default', }, @@ -96,13 +98,17 @@ describe('transformFilterNodesAsEdges', () => { { id: 'A-B', source: 'A', + sourceHandle: 'default', target: 'B', + targetHandle: 'default', data: { edgeType: 'default' }, }, { id: 'B-C', source: 'B', + sourceHandle: 'default', target: 'C', + targetHandle: 'default', data: { edgeType: 'default' }, }, ], @@ -148,7 +154,9 @@ describe('transformFilterNodesAsEdges', () => { id: 'A-C-filter-B', type: 'filter--editable', source: 'A', + sourceHandle: 'default', target: 'C', + targetHandle: 'default', selectable: true, deletable: true, data: { @@ -229,25 +237,33 @@ describe('transformFilterNodesAsEdges', () => { { id: 'A-B1', source: 'A', + sourceHandle: 'default', target: 'B1', + targetHandle: 'default', data: { edgeType: 'default' }, }, { id: 'B1-C', source: 'B1', + sourceHandle: 'default', target: 'C', + targetHandle: 'default', data: { edgeType: 'default' }, }, { id: 'C-B2', source: 'C', + sourceHandle: 'default', target: 'B2', + targetHandle: 'default', data: { edgeType: 'default' }, }, { id: 'B2-D', source: 'B2', + sourceHandle: 'default', target: 'D', + targetHandle: 'default', data: { edgeType: 'default' }, }, ], @@ -275,7 +291,9 @@ describe('transformFilterNodesAsEdges', () => { id: 'A-C-filter-B1', type: 'filter--editable', source: 'A', + sourceHandle: 'default', target: 'C', + targetHandle: 'default', selectable: true, deletable: true, data: { @@ -294,7 +312,9 @@ describe('transformFilterNodesAsEdges', () => { id: 'C-D-filter-B2', type: 'filter--editable', source: 'C', + sourceHandle: 'default', target: 'D', + targetHandle: 'default', selectable: true, deletable: true, data: { @@ -339,7 +359,9 @@ describe('transformFilterNodesAsEdges', () => { { id: 'A-B', source: 'A', + sourceHandle: 'default', target: 'B', + targetHandle: 'default', data: { edgeType: 'default' }, }, ], @@ -415,13 +437,17 @@ describe('transformFilterNodesAsEdges', () => { { id: 'trigger-B', source: 'trigger', + sourceHandle: 'default', target: 'B', + targetHandle: 'default', data: { edgeType: 'default' }, }, { id: 'B-C', source: 'B', + sourceHandle: 'default', target: 'C', + targetHandle: 'default', data: { edgeType: 'default' }, }, ], @@ -467,7 +493,9 @@ describe('transformFilterNodesAsEdges', () => { id: 'trigger-C-filter-B', type: 'filter--editable', source: 'trigger', + sourceHandle: 'default', target: 'C', + targetHandle: 'default', selectable: true, deletable: true, data: { diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/assertEdgeHasDefinedHandlesOrThrow.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/assertEdgeHasDefinedHandlesOrThrow.ts new file mode 100644 index 0000000000..b711a554a0 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/assertEdgeHasDefinedHandlesOrThrow.ts @@ -0,0 +1,18 @@ +import { type Edge } from '@xyflow/react'; +import { isDefined } from 'twenty-shared/utils'; + +type AssertEdgeHasDefinedHandlesOrThrow = ( + edge: Edge, +) => asserts edge is Edge & { + sourceHandle: string; + targetHandle: string; +}; + +export const assertEdgeHasDefinedHandlesOrThrow: AssertEdgeHasDefinedHandlesOrThrow = + (edge) => { + if (!isDefined(edge.sourceHandle) || !isDefined(edge.targetHandle)) { + throw new Error( + 'sourceHandle and targetHandle must be defined on the edge.', + ); + } + }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/assertWorkflowConnectionOrThrow.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/assertWorkflowConnectionOrThrow.ts new file mode 100644 index 0000000000..f9382abf66 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/assertWorkflowConnectionOrThrow.ts @@ -0,0 +1,19 @@ +import { type WorkflowConnection } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { type Connection } from '@xyflow/react'; +import { isDefined } from 'twenty-shared/utils'; + +type AssertWorkflowConnectionOrThrow = ( + connection: Connection, +) => asserts connection is WorkflowConnection; + +export const assertWorkflowConnectionOrThrow: AssertWorkflowConnectionOrThrow = + (connection) => { + if ( + !isDefined(connection.sourceHandle) || + !isDefined(connection.targetHandle) + ) { + throw new Error( + 'WorkflowConnection must have defined sourceHandle and targetHandle.', + ); + } + }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateNodesAndEdgesForDefaultNode.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateNodesAndEdgesForDefaultNode.ts index 1febeee5c2..a5398f0871 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateNodesAndEdgesForDefaultNode.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateNodesAndEdgesForDefaultNode.ts @@ -7,6 +7,8 @@ import { } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; import { getEdgeTypeBetweenTwoNodes } from '@/workflow/workflow-diagram/utils/getEdgeTypeBetweenTwoNodes'; import { WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowVisualizerEdgeDefaultConfiguration'; +import { WORKFLOW_DIAGRAM_NODE_DEFAULT_SOURCE_HANDLE_ID } from '@/workflow/workflow-diagram/workflow-nodes/constants/WorkflowDiagramNodeDefaultSourceHandleId'; +import { WORKFLOW_DIAGRAM_NODE_DEFAULT_TARGET_HANDLE_ID } from '@/workflow/workflow-diagram/workflow-nodes/constants/WorkflowDiagramNodeDefaultTargetHandleId'; import { isDefined } from 'twenty-shared/utils'; import { v4 } from 'uuid'; @@ -61,7 +63,9 @@ export const generateNodesAndEdgesForDefaultNode = ({ type: edgeTypeBetweenTwoNodes, id: v4(), source: step.id, + sourceHandle: WORKFLOW_DIAGRAM_NODE_DEFAULT_SOURCE_HANDLE_ID, target: child, + targetHandle: WORKFLOW_DIAGRAM_NODE_DEFAULT_TARGET_HANDLE_ID, ...(edgeTypeBetweenTwoNodes.includes('editable') ? { deletable: true, selectable: true } : {}), diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateNodesAndEdgesForIteratorNode.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateNodesAndEdgesForIteratorNode.ts index 1445b59e5a..0486a56753 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateNodesAndEdgesForIteratorNode.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateNodesAndEdgesForIteratorNode.ts @@ -9,6 +9,8 @@ import { getEdgeTypeBetweenTwoNodes } from '@/workflow/workflow-diagram/utils/ge import { WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowVisualizerEdgeDefaultConfiguration'; import { type WorkflowDiagramIteratorEmptyActionNodeData } from '@/workflow/workflow-diagram/workflow-iterator/types/WorkflowDiagramIteratorEmptyActionNodeData'; import { WORKFLOW_DIAGRAM_ITERATOR_NODE_LOOP_HANDLE_ID } from '@/workflow/workflow-diagram/workflow-nodes/constants/WorkflowDiagramIteratorNodeLoopHandleId'; +import { WORKFLOW_DIAGRAM_NODE_DEFAULT_SOURCE_HANDLE_ID } from '@/workflow/workflow-diagram/workflow-nodes/constants/WorkflowDiagramNodeDefaultSourceHandleId'; +import { WORKFLOW_DIAGRAM_NODE_DEFAULT_TARGET_HANDLE_ID } from '@/workflow/workflow-diagram/workflow-nodes/constants/WorkflowDiagramNodeDefaultTargetHandleId'; import { msg } from '@lingui/core/macro'; import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards'; import { Position } from '@xyflow/react'; @@ -88,6 +90,7 @@ export const generateNodesAndEdgesForIteratorNode = ({ ...(edgeTypeBetweenTwoNodes.includes('editable') ? { deletable: true, selectable: true } : {}), + targetHandle: WORKFLOW_DIAGRAM_NODE_DEFAULT_TARGET_HANDLE_ID, data: { ...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION.data, labelOptions: { @@ -128,6 +131,7 @@ export const generateNodesAndEdgesForIteratorNode = ({ ...(edgeTypeBetweenTwoNodes.includes('editable') ? { deletable: true, selectable: true } : {}), + targetHandle: WORKFLOW_DIAGRAM_NODE_DEFAULT_TARGET_HANDLE_ID, data: { ...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION.data, labelOptions: { @@ -142,10 +146,12 @@ export const generateNodesAndEdgesForIteratorNode = ({ type: edgeTypeBetweenTwoNodes, id: v4(), source: emptyNodeId, + sourceHandle: WORKFLOW_DIAGRAM_NODE_DEFAULT_SOURCE_HANDLE_ID, target: step.id, ...(edgeTypeBetweenTwoNodes.includes('editable') ? { deletable: true, selectable: true } : {}), + targetHandle: WORKFLOW_DIAGRAM_NODE_DEFAULT_TARGET_HANDLE_ID, }); } @@ -155,10 +161,12 @@ export const generateNodesAndEdgesForIteratorNode = ({ type: edgeTypeBetweenTwoNodes, id: v4(), source: step.id, + sourceHandle: WORKFLOW_DIAGRAM_NODE_DEFAULT_SOURCE_HANDLE_ID, target: child, ...(edgeTypeBetweenTwoNodes.includes('editable') ? { deletable: true, selectable: true } : {}), + targetHandle: WORKFLOW_DIAGRAM_NODE_DEFAULT_TARGET_HANDLE_ID, data: { ...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION.data, labelOptions: { diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateWorkflowDiagram.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateWorkflowDiagram.ts index d6154ea890..0623bde424 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateWorkflowDiagram.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateWorkflowDiagram.ts @@ -16,6 +16,8 @@ import { generateNodesAndEdgesForIteratorNode } from '@/workflow/workflow-diagra import { getEdgeTypeBetweenTwoNodes } from '@/workflow/workflow-diagram/utils/getEdgeTypeBetweenTwoNodes'; import { getWorkflowDiagramTriggerNode } from '@/workflow/workflow-diagram/utils/getWorkflowDiagramTriggerNode'; import { WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowVisualizerEdgeDefaultConfiguration'; +import { WORKFLOW_DIAGRAM_NODE_DEFAULT_SOURCE_HANDLE_ID } from '@/workflow/workflow-diagram/workflow-nodes/constants/WorkflowDiagramNodeDefaultSourceHandleId'; +import { WORKFLOW_DIAGRAM_NODE_DEFAULT_TARGET_HANDLE_ID } from '@/workflow/workflow-diagram/workflow-nodes/constants/WorkflowDiagramNodeDefaultTargetHandleId'; import { isDefined } from 'twenty-shared/utils'; import { TRIGGER_STEP_ID } from 'twenty-shared/workflow'; import { v4 } from 'uuid'; @@ -48,10 +50,12 @@ export const generateWorkflowDiagram = ({ type: edgeTypeBetweenTwoNodes, id: v4(), source: TRIGGER_STEP_ID, + sourceHandle: WORKFLOW_DIAGRAM_NODE_DEFAULT_SOURCE_HANDLE_ID, target: stepLinkToTriggerId, ...(edgeTypeBetweenTwoNodes.includes('editable') ? { deletable: true, selectable: true } : {}), + targetHandle: WORKFLOW_DIAGRAM_NODE_DEFAULT_TARGET_HANDLE_ID, }); } diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramBaseEdge.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramBaseEdge.tsx index a88404e7bc..c1d357322e 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramBaseEdge.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramBaseEdge.tsx @@ -1,11 +1,16 @@ -import { useTheme } from '@emotion/react'; -import { BaseEdge, type EdgeProps } from '@xyflow/react'; -import { type WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; import { useEdgeState } from '@/workflow/workflow-diagram/workflow-edges/hooks/useEdgeState'; +import { type WorkflowDiagramEdgeComponentProps } from '@/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdgeComponentProps'; +import { useTheme } from '@emotion/react'; +import { BaseEdge } from '@xyflow/react'; type WorkflowDiagramBaseEdgeProps = Pick< - EdgeProps, - 'source' | 'target' | 'markerStart' | 'markerEnd' + WorkflowDiagramEdgeComponentProps, + | 'source' + | 'sourceHandleId' + | 'target' + | 'targetHandleId' + | 'markerStart' + | 'markerEnd' > & { path: string; }; @@ -13,6 +18,8 @@ type WorkflowDiagramBaseEdgeProps = Pick< export const WorkflowDiagramBaseEdge = ({ source, target, + sourceHandleId, + targetHandleId, markerStart, markerEnd, path, @@ -21,9 +28,19 @@ export const WorkflowDiagramBaseEdge = ({ const { isEdgeSelected, isEdgeHovered } = useEdgeState(); - const selected = isEdgeSelected({ source, target }); + const selected = isEdgeSelected({ + source, + target, + sourceHandle: sourceHandleId, + targetHandle: targetHandleId, + }); - const isHovered = isEdgeHovered({ source, target }); + const isHovered = isEdgeHovered({ + source, + target, + sourceHandle: sourceHandleId, + targetHandle: targetHandleId, + }); const stroke = selected ? theme.color.blue diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramDefaultEdgeEditable.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramDefaultEdgeEditable.tsx index 6d0dbc39a6..c2a23404f3 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramDefaultEdgeEditable.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramDefaultEdgeEditable.tsx @@ -2,7 +2,6 @@ import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; import { useOpenWorkflowEditFilterInCommandMenu } from '@/workflow/workflow-diagram/hooks/useOpenWorkflowEditFilterInCommandMenu'; import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation'; -import { type WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; import { WorkflowDiagramBaseEdge } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramBaseEdge'; import { WorkflowDiagramEdgeButtonGroup } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeButtonGroup'; import { WorkflowDiagramEdgeLabel } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeLabel'; @@ -11,24 +10,24 @@ import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/work import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2VisibilityContainer'; import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowDiagramEdgeOptionsClickOutsideId'; import { useEdgeState } from '@/workflow/workflow-diagram/workflow-edges/hooks/useEdgeState'; +import { type WorkflowDiagramEdgeComponentProps } from '@/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdgeComponentProps'; import { useCreateStep } from '@/workflow/workflow-steps/hooks/useCreateStep'; import { useDeleteEdge } from '@/workflow/workflow-steps/hooks/useDeleteEdge'; import { useLingui } from '@lingui/react/macro'; -import { - EdgeLabelRenderer, - type EdgeProps, - getBezierPath, -} from '@xyflow/react'; +import { EdgeLabelRenderer, getBezierPath } from '@xyflow/react'; import { type MouseEvent, useContext } from 'react'; import { useSetRecoilState } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; import { IconFilter, IconPlus, IconTrash } from 'twenty-ui/display'; -type WorkflowDiagramDefaultEdgeEditableProps = EdgeProps; +type WorkflowDiagramDefaultEdgeEditableProps = + WorkflowDiagramEdgeComponentProps; export const WorkflowDiagramDefaultEdgeEditable = ({ source, + sourceHandleId, target, + targetHandleId, sourceX, sourceY, targetX, @@ -100,14 +99,21 @@ export const WorkflowDiagramDefaultEdgeEditable = ({ const handleDeleteBranch = async (event: MouseEvent) => { event.stopPropagation(); - await deleteEdge({ source, target }); + await deleteEdge({ + source, + sourceHandle: sourceHandleId, + target, + targetHandle: targetHandleId, + }); }; return ( <> ; +type WorkflowDiagramDefaultEdgeReadonlyProps = + WorkflowDiagramEdgeComponentProps; export const WorkflowDiagramDefaultEdgeReadonly = ({ sourceX, diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramDefaultEdgeRun.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramDefaultEdgeRun.tsx index 0a65a8b56c..ba03fa9f6c 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramDefaultEdgeRun.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramDefaultEdgeRun.tsx @@ -1,8 +1,8 @@ import { WorkflowRunDiagramBaseEdge } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowRunDiagramBaseEdge'; -import { type WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; -import { type EdgeProps, getBezierPath } from '@xyflow/react'; +import { type WorkflowDiagramEdgeComponentProps } from '@/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdgeComponentProps'; +import { getBezierPath } from '@xyflow/react'; -type WorkflowDiagramDefaultEdgeRunProps = EdgeProps; +type WorkflowDiagramDefaultEdgeRunProps = WorkflowDiagramEdgeComponentProps; export const WorkflowDiagramDefaultEdgeRun = ({ sourceX, diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeEditable.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeEditable.tsx index d4a754d4cd..d0897cd424 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeEditable.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeEditable.tsx @@ -13,10 +13,7 @@ import { useOpenWorkflowEditFilterInCommandMenu } from '@/workflow/workflow-diag import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation'; import { workflowDiagramPanOnDragComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramPanOnDragComponentState'; import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState'; -import { - type WorkflowDiagramEdge, - type WorkflowDiagramEdgeData, -} from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { type WorkflowDiagramEdgeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; import { getWorkflowDiagramColors } from '@/workflow/workflow-diagram/utils/getWorkflowDiagramColors'; import { WorkflowDiagramBaseEdge } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramBaseEdge'; import { WorkflowDiagramEdgeButtonGroup } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeButtonGroup'; @@ -26,6 +23,7 @@ import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/work import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2VisibilityContainer'; import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowDiagramEdgeOptionsClickOutsideId'; import { useEdgeState } from '@/workflow/workflow-diagram/workflow-edges/hooks/useEdgeState'; +import { type WorkflowDiagramEdgeComponentProps } from '@/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdgeComponentProps'; import { useDeleteEdge } from '@/workflow/workflow-steps/hooks/useDeleteEdge'; import { useDeleteStep } from '@/workflow/workflow-steps/hooks/useDeleteStep'; import { WorkflowStepFilterCounter } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterCounter'; @@ -34,11 +32,7 @@ import { css, useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { useLingui } from '@lingui/react/macro'; import { isNonEmptyString } from '@sniptt/guards'; -import { - EdgeLabelRenderer, - type EdgeProps, - getBezierPath, -} from '@xyflow/react'; +import { EdgeLabelRenderer, getBezierPath } from '@xyflow/react'; import { useContext } from 'react'; import { useSetRecoilState } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; @@ -53,7 +47,7 @@ import { import { IconButtonGroup } from 'twenty-ui/input'; import { MenuItem } from 'twenty-ui/navigation'; -type WorkflowDiagramFilterEdgeEditableProps = EdgeProps; +type WorkflowDiagramFilterEdgeEditableProps = WorkflowDiagramEdgeComponentProps; const assertFilterEdgeDataOrThrow: ( data: WorkflowDiagramEdgeData | undefined, @@ -86,7 +80,9 @@ const StyledIconButtonGroup = styled(IconButtonGroup)<{ selected?: boolean }>` export const WorkflowDiagramFilterEdgeEditable = ({ source, + sourceHandleId, target, + targetHandleId, sourceY, sourceX, targetY, @@ -179,7 +175,12 @@ export const WorkflowDiagramFilterEdgeEditable = ({ const handleDeleteBranchClick = async () => { closeDropdown(dropdownId); - await deleteEdge({ source, target }); + await deleteEdge({ + source, + sourceHandle: sourceHandleId, + target, + targetHandle: targetHandleId, + }); }; const { filterCounter } = useFilterCounter({ stepId: data.stepId }); @@ -189,7 +190,9 @@ export const WorkflowDiagramFilterEdgeEditable = ({ <> - {isEdgeHovered({ source, target }) || + {isEdgeHovered({ + source, + target, + sourceHandle: sourceHandleId, + targetHandle: targetHandleId, + }) || isDropdownOpen || nodeCreationStarted ? ( ; +type WorkflowDiagramFilterEdgeReadonlyProps = WorkflowDiagramEdgeComponentProps; const assertFilterEdgeDataOrThrow: ( data: WorkflowDiagramEdgeData | undefined, diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeRun.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeRun.tsx index 61f07e1895..452c847a94 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeRun.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeRun.tsx @@ -2,30 +2,24 @@ import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/ho import type { WorkflowRunStepStatus } from '@/workflow/types/Workflow'; import { useOpenWorkflowRunFilterInCommandMenu } from '@/workflow/workflow-diagram/hooks/useOpenWorkflowRunFilterInCommandMenu'; import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState'; -import { - type WorkflowDiagramEdge, - type WorkflowDiagramEdgeData, -} from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { type WorkflowDiagramEdgeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; import { getWorkflowDiagramColors } from '@/workflow/workflow-diagram/utils/getWorkflowDiagramColors'; import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2Container'; import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2VisibilityContainer'; import { WorkflowRunDiagramBaseEdge } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowRunDiagramBaseEdge'; import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowDiagramEdgeOptionsClickOutsideId'; +import { type WorkflowDiagramEdgeComponentProps } from '@/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdgeComponentProps'; import { WorkflowStepFilterCounter } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterCounter'; import { useFilterCounter } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useFilterCounter'; import { css, useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { isNonEmptyString } from '@sniptt/guards'; -import { - EdgeLabelRenderer, - type EdgeProps, - getBezierPath, -} from '@xyflow/react'; +import { EdgeLabelRenderer, getBezierPath } from '@xyflow/react'; import { isDefined } from 'twenty-shared/utils'; import { IconFilter } from 'twenty-ui/display'; import { IconButtonGroup } from 'twenty-ui/input'; -type WorkflowDiagramFilterEdgeRunProps = EdgeProps; +type WorkflowDiagramFilterEdgeRunProps = WorkflowDiagramEdgeComponentProps; const assertFilterEdgeDataOrThrow: ( data: WorkflowDiagramEdgeData | undefined, diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowRunDiagramBaseEdge.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowRunDiagramBaseEdge.tsx index 69dd23c8a5..f80382c334 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowRunDiagramBaseEdge.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowRunDiagramBaseEdge.tsx @@ -1,9 +1,9 @@ -import { type WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { type WorkflowDiagramEdgeComponentProps } from '@/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdgeComponentProps'; import { useTheme } from '@emotion/react'; -import { BaseEdge, type EdgeProps } from '@xyflow/react'; +import { BaseEdge } from '@xyflow/react'; type WorkflowRunDiagramBaseEdgeProps = Pick< - EdgeProps, + WorkflowDiagramEdgeComponentProps, 'markerStart' | 'markerEnd' > & { path: string; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/hooks/useEdgeState.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/hooks/useEdgeState.ts index 15c61489d8..1de33ebe69 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/hooks/useEdgeState.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/hooks/useEdgeState.ts @@ -1,9 +1,9 @@ -import { workflowSelectedEdgeComponentState } from '@/workflow/workflow-diagram/workflow-edges/states/workflowSelectedEdgeComponentState'; import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState'; -import { type WorkflowDiagramEdge } from '@/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdge'; -import { workflowHoveredEdgeComponentState } from '@/workflow/workflow-diagram/workflow-edges/states/workflowHoveredEdgeComponentState'; -import { useReactFlow } from '@xyflow/react'; import { EDGE_BRANCH_ARROW_MARKER } from '@/workflow/workflow-diagram/workflow-edges/constants/EdgeBranchArrowMarker'; +import { workflowHoveredEdgeComponentState } from '@/workflow/workflow-diagram/workflow-edges/states/workflowHoveredEdgeComponentState'; +import { workflowSelectedEdgeComponentState } from '@/workflow/workflow-diagram/workflow-edges/states/workflowSelectedEdgeComponentState'; +import { type WorkflowDiagramEdgeDescriptor } from '@/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdgeDescriptor'; +import { useReactFlow } from '@xyflow/react'; export const useEdgeState = () => { const reactflow = useReactFlow(); @@ -15,32 +15,73 @@ export const useEdgeState = () => { workflowHoveredEdgeComponentState, ); - const isSourceSelected = (nodeId: string) => { - return workflowSelectedEdge?.source === nodeId; + const isSourceSelected = ({ + nodeId, + sourceHandle, + }: { + nodeId: string; + sourceHandle: string | null | undefined; + }) => { + return ( + workflowSelectedEdge?.source === nodeId && + workflowSelectedEdge.sourceHandle === sourceHandle + ); }; - const isSourceHovered = (nodeId: string) => { - return workflowHoveredEdge?.source === nodeId; + const isSourceHovered = ({ + nodeId, + sourceHandle, + }: { + nodeId: string; + sourceHandle: string | null | undefined; + }) => { + return ( + workflowHoveredEdge?.source === nodeId && + workflowHoveredEdge.sourceHandle === sourceHandle + ); }; - const isEdgeSelected = ({ source, target }: WorkflowDiagramEdge) => { - return isSourceSelected(source) && workflowSelectedEdge?.target === target; + const isEdgeSelected = ({ + source, + target, + sourceHandle, + targetHandle, + }: WorkflowDiagramEdgeDescriptor) => { + return ( + isSourceSelected({ nodeId: source, sourceHandle }) && + workflowSelectedEdge?.target === target && + workflowSelectedEdge.targetHandle === targetHandle + ); }; - const isEdgeHovered = ({ source, target }: WorkflowDiagramEdge) => { - return isSourceHovered(source) && workflowHoveredEdge?.target === target; + const isEdgeHovered = ({ + source, + target, + sourceHandle, + targetHandle, + }: WorkflowDiagramEdgeDescriptor) => { + return ( + isSourceHovered({ nodeId: source, sourceHandle }) && + workflowHoveredEdge?.target === target && + workflowHoveredEdge.targetHandle === targetHandle + ); }; - const setEdgeSelected = ({ source, target }: WorkflowDiagramEdge) => { - if (isEdgeSelected({ source, target })) { + const setEdgeSelected = (edgeDescriptor: WorkflowDiagramEdgeDescriptor) => { + if (isEdgeSelected(edgeDescriptor)) { return; } - setWorkflowSelectedEdge({ source, target }); + setWorkflowSelectedEdge(edgeDescriptor); reactflow.setEdges((edges) => edges.map((edge) => { - if (edge.source === source && edge.target === target) { + if ( + edge.source === edgeDescriptor.source && + edge.sourceHandle === edgeDescriptor.sourceHandle && + edge.target === edgeDescriptor.target && + edge.targetHandle === edgeDescriptor.targetHandle + ) { return { ...edge, ...EDGE_BRANCH_ARROW_MARKER.Selected, @@ -55,14 +96,21 @@ export const useEdgeState = () => { ); }; - const setEdgeHovered = ({ source, target }: WorkflowDiagramEdge) => { - setWorkflowHoveredEdge({ source, target }); + const setEdgeHovered = ({ + source, + target, + sourceHandle, + targetHandle, + }: WorkflowDiagramEdgeDescriptor) => { + setWorkflowHoveredEdge({ source, target, sourceHandle, targetHandle }); reactflow.setEdges((edges) => edges.map((edge) => { if ( edge.source === source && + edge.sourceHandle === sourceHandle && edge.target === target && + edge.targetHandle === targetHandle && edge.markerEnd !== EDGE_BRANCH_ARROW_MARKER.Selected.markerEnd ) { return { diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/states/workflowHoveredEdgeComponentState.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/states/workflowHoveredEdgeComponentState.ts index 36774acbe6..acb6648bc3 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/states/workflowHoveredEdgeComponentState.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/states/workflowHoveredEdgeComponentState.ts @@ -1,9 +1,9 @@ import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext'; -import { type WorkflowDiagramEdge } from '@/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdge'; +import { type WorkflowDiagramEdgeDescriptor } from '@/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdgeDescriptor'; export const workflowHoveredEdgeComponentState = createComponentState< - WorkflowDiagramEdge | undefined + WorkflowDiagramEdgeDescriptor | undefined >({ key: 'workflowHoveredEdgeComponentState', defaultValue: undefined, diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/states/workflowSelectedEdgeComponentState.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/states/workflowSelectedEdgeComponentState.ts index a5940c5a65..01d59f0bac 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/states/workflowSelectedEdgeComponentState.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/states/workflowSelectedEdgeComponentState.ts @@ -1,9 +1,9 @@ import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext'; -import { type WorkflowDiagramEdge } from '@/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdge'; +import { type WorkflowDiagramEdgeDescriptor } from '@/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdgeDescriptor'; export const workflowSelectedEdgeComponentState = createComponentState< - WorkflowDiagramEdge | undefined + WorkflowDiagramEdgeDescriptor | undefined >({ key: 'workflowSelectedEdgeComponentState', defaultValue: undefined, diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdge.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdge.ts deleted file mode 100644 index dcd2518a4f..0000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdge.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type WorkflowDiagramEdge = { - source: string; - target: string; -}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdgeComponentProps.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdgeComponentProps.ts new file mode 100644 index 0000000000..b400c60790 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdgeComponentProps.ts @@ -0,0 +1,8 @@ +import { type WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { type EdgeProps } from '@xyflow/react'; + +export type WorkflowDiagramEdgeComponentProps = + EdgeProps & { + sourceHandleId: string; + targetHandleId: string; + }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdgeDescriptor.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdgeDescriptor.ts new file mode 100644 index 0000000000..4cd67e7001 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdgeDescriptor.ts @@ -0,0 +1,6 @@ +export type WorkflowDiagramEdgeDescriptor = { + source: string; + target: string; + sourceHandle: string; + targetHandle: string; +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleTarget.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleTarget.tsx index 1dd0fec5fd..8e6b64c219 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleTarget.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleTarget.tsx @@ -1,3 +1,4 @@ +import { WORKFLOW_DIAGRAM_NODE_DEFAULT_TARGET_HANDLE_ID } from '@/workflow/workflow-diagram/workflow-nodes/constants/WorkflowDiagramNodeDefaultTargetHandleId'; import styled from '@emotion/styled'; import { Handle, Position } from '@xyflow/react'; @@ -27,6 +28,7 @@ export const WorkflowDiagramHandleTarget = ({ }: WorkflowDiagramHandleTargetProps) => { return ( ); diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeEditableContent.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeEditableContent.tsx index cf6bf063bb..387a70f4a0 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeEditableContent.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeEditableContent.tsx @@ -13,6 +13,7 @@ import { WorkflowNodeLabel } from '@/workflow/workflow-diagram/workflow-nodes/co import { WorkflowNodeLabelWithCounterPart } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeLabelWithCounterPart'; import { WorkflowNodeRightPart } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeRightPart'; import { WorkflowNodeTitle } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeTitle'; +import { WORKFLOW_DIAGRAM_NODE_DEFAULT_SOURCE_HANDLE_ID } from '@/workflow/workflow-diagram/workflow-nodes/constants/WorkflowDiagramNodeDefaultSourceHandleId'; import { useConnectionState } from '@/workflow/workflow-diagram/workflow-nodes/hooks/useConnectionState'; import styled from '@emotion/styled'; import { useLingui } from '@lingui/react/macro'; @@ -143,15 +144,24 @@ export const WorkflowDiagramStepNodeEditableContent = ({ )} {isDefined(data.rightHandleOptions) && ( @@ -160,13 +170,20 @@ export const WorkflowDiagramStepNodeEditableContent = ({ type="source" position={Position.Right} selected={ - isSourceSelected(id) || + isSourceSelected({ + nodeId: id, + sourceHandle: data.rightHandleOptions.id, + }) || selected || isSourceConnected(id) || (isConnectable(id) && isHovered) } - // TODO: fix hovered state when multiple source handles - hovered={isSourceHovered(id) || isHovered} + hovered={ + isSourceHovered({ + nodeId: id, + sourceHandle: data.rightHandleOptions.id, + }) || isHovered + } /> )} diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeReadonly.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeReadonly.tsx index 9d8e59b2de..9cac927826 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeReadonly.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeReadonly.tsx @@ -18,6 +18,7 @@ import { WorkflowNodeLabel } from '@/workflow/workflow-diagram/workflow-nodes/co import { WorkflowNodeLabelWithCounterPart } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeLabelWithCounterPart'; import { WorkflowNodeRightPart } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeRightPart'; import { WorkflowNodeTitle } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeTitle'; +import { WORKFLOW_DIAGRAM_NODE_DEFAULT_SOURCE_HANDLE_ID } from '@/workflow/workflow-diagram/workflow-nodes/constants/WorkflowDiagramNodeDefaultSourceHandleId'; import { Position } from '@xyflow/react'; import { useContext } from 'react'; import { useSetRecoilState } from 'recoil'; @@ -97,6 +98,7 @@ export const WorkflowDiagramStepNodeReadonly = ({ { const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersionOrThrow(); - const createEdge = async ({ source, target }: WorkflowDiagramEdge) => { + const createEdge = async ({ + source, + target, + }: WorkflowDiagramEdgeDescriptor) => { if (isLoading) { return; } diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useDeleteEdge.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useDeleteEdge.ts index 6f4a87cc99..2ee3b9a660 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useDeleteEdge.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useDeleteEdge.ts @@ -1,5 +1,5 @@ import { useGetUpdatableWorkflowVersionOrThrow } from '@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow'; -import { type WorkflowDiagramEdge } from '@/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdge'; +import { type WorkflowDiagramEdgeDescriptor } from '@/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdgeDescriptor'; import { useDeleteWorkflowVersionEdge } from '@/workflow/workflow-steps/hooks/useDeleteWorkflowVersionEdge'; import { useState } from 'react'; @@ -11,7 +11,10 @@ export const useDeleteEdge = () => { const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersionOrThrow(); - const deleteEdge = async ({ source, target }: WorkflowDiagramEdge) => { + const deleteEdge = async ({ + source, + target, + }: WorkflowDiagramEdgeDescriptor) => { if (isLoading) { return; }