From 0d0cce7bb1967ee1548cd2a2153644abaa10eaf3 Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Thu, 16 Oct 2025 18:32:47 +0200 Subject: [PATCH] Iterators design updates (#15146) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A few fixes : - add a counter of iteration for nodes that have been run Capture d’écran 2025-10-16 à 17 50
07 - if a node has fail, we should end the workflow even if a step is still running - fix position for add node button in iterator loop : Before https://github.com/user-attachments/assets/eafeda64-977f-415d-bab4-b7d0c44406b0 After https://github.com/user-attachments/assets/6ff221a3-b287-45f6-977f-b127f9998692 --- .../view-run/utils/getIsOutputTabDisabled.ts | 4 +- .../WorkflowDiagramDefaultEdgeEditable.tsx | 2 +- .../workflow-edges/utils/getEdgePath.ts | 17 +++- .../components/WorkflowRunDiagramStepNode.tsx | 86 ++++++++++++++----- .../utils/getNodeIterationCount.ts | 15 ++++ .../workflow-executor.workspace-service.ts | 8 +- 6 files changed, 99 insertions(+), 33 deletions(-) create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/utils/getNodeIterationCount.ts diff --git a/packages/twenty-front/src/modules/command-menu/pages/workflow/step/view-run/utils/getIsOutputTabDisabled.ts b/packages/twenty-front/src/modules/command-menu/pages/workflow/step/view-run/utils/getIsOutputTabDisabled.ts index 0d5067218e7..99ee5dc4a94 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/workflow/step/view-run/utils/getIsOutputTabDisabled.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/workflow/step/view-run/utils/getIsOutputTabDisabled.ts @@ -6,6 +6,8 @@ export const getIsOutputTabDisabled = ({ stepExecutionStatus: WorkflowRunStepStatus; }) => { return ( - stepExecutionStatus === 'RUNNING' || stepExecutionStatus === 'NOT_STARTED' + stepExecutionStatus === 'RUNNING' || + stepExecutionStatus === 'NOT_STARTED' || + stepExecutionStatus === 'SKIPPED' ); }; 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 0bbf18f1641..7facd18aae1 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 @@ -41,7 +41,7 @@ export const WorkflowDiagramDefaultEdgeEditable = ({ const { segments, - labelPosition: [labelX, labelY], + overlayPosition: [labelX, labelY], } = getEdgePath({ sourceX, sourceY, diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/utils/getEdgePath.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/utils/getEdgePath.ts index eedd7c15492..8d002d4a515 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/utils/getEdgePath.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/utils/getEdgePath.ts @@ -32,7 +32,7 @@ export const getEdgePath = ({ strategy, }: GetEdgePathParams) => { if (strategy === 'smooth-step-path-to-target') { - const [path, labelX, labelY] = getSmoothStepPath({ + const [path] = getSmoothStepPath({ sourceX, sourceY, sourcePosition, @@ -43,6 +43,12 @@ export const getEdgePath = ({ offset: EDGE_PADDING_X, }); + const overlayY = + targetY - sourceY > EDGE_PADDING_BOTTOM && + sourceX + EDGE_PADDING_X < targetX + ? targetY - (targetY - sourceY) / 2 + : targetY - EDGE_PADDING_BOTTOM / 2; + return { segments: [ { @@ -51,7 +57,7 @@ export const getEdgePath = ({ markerEnd, }, ], - labelPosition: [labelX, labelY], + overlayPosition: [targetX, overlayY], }; } @@ -73,7 +79,7 @@ export const getEdgePath = ({ markerEnd, }, ], - labelPosition: [labelX, labelY], + overlayPosition: [labelX, labelY], }; } @@ -117,6 +123,9 @@ export const getEdgePath = ({ markerEnd, }, ], - labelPosition: [firstSegmentTargetX, firstSegmentTargetY], + overlayPosition: + strategy === 'bypass-source-node-on-right-side' + ? [sourceX, sourceY + EDGE_PADDING_BOTTOM] + : [firstSegmentTargetX, firstSegmentTargetY], }; }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowRunDiagramStepNode.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowRunDiagramStepNode.tsx index 4204ae1a788..1d732ffbb8c 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowRunDiagramStepNode.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowRunDiagramStepNode.tsx @@ -3,11 +3,14 @@ import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandM import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; +import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun'; import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow'; import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; +import { type WorkflowRunStepStatus } from '@/workflow/types/Workflow'; import { WORKFLOW_DIAGRAM_STEP_NODE_BASE_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramStepNodeClickOutsideId'; import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState'; import { type WorkflowRunDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { getWorkflowDiagramColors } from '@/workflow/workflow-diagram/utils/getWorkflowDiagramColors'; import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey'; import { WorkflowDiagramHandleSource } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleSource'; import { WorkflowDiagramHandleTarget } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleTarget'; @@ -19,7 +22,8 @@ import { WorkflowNodeLabelWithCounterPart } from '@/workflow/workflow-diagram/wo 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 { useTheme } from '@emotion/react'; +import { getNodeIterationCount } from '@/workflow/workflow-diagram/workflow-nodes/utils/getNodeIterationCount'; +import { css, useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { Position } from '@xyflow/react'; import { useContext } from 'react'; @@ -33,7 +37,7 @@ const StyledNodeLabelWithCounterPart = styled(WorkflowNodeLabelWithCounterPart)` column-gap: ${({ theme }) => theme.spacing(2)}; `; -const StyledNodeCounter = styled.div` +const StyledStatusIconsContainer = styled.div` align-items: center; display: flex; gap: ${({ theme }) => theme.spacing(1)}; @@ -54,6 +58,25 @@ const StyledColorIcon = styled.div<{ background: ${({ color }) => color}; `; +const StyledIterationCounter = styled.div<{ + runStatus?: WorkflowRunStepStatus; +}>` + font-size: ${({ theme }) => theme.font.size.sm}; + font-weight: ${({ theme }) => theme.font.weight.medium}; + + ${({ theme, runStatus }) => { + const colors = getWorkflowDiagramColors({ theme, runStatus }); + return css` + color: ${colors.unselected.color}; + `; + }} +`; + +const StyledRightPartContainer = styled.div` + display: flex; + gap: ${({ theme }) => theme.spacing(1)}; +`; + export const WorkflowRunDiagramStepNode = ({ id, data, @@ -83,6 +106,15 @@ export const WorkflowRunDiagramStepNode = ({ commandMenuNavigationStackState, ); + const workflowRun = useWorkflowRun({ workflowRunId }); + + const stepInfo = workflowRun?.state?.stepInfos[data.stepId]; + + const iterationCount = + data.nodeType === 'action' && isDefined(stepInfo) + ? getNodeIterationCount({ stepInfo }) + : 0; + const handleClick = () => { if (!isDefined(workflowId)) { throw new Error('Workflow ID must be defined'); @@ -122,29 +154,37 @@ export const WorkflowRunDiagramStepNode = ({ {capitalize(data.nodeType)} - {(data.runStatus === StepStatus.SUCCESS || - data.runStatus === StepStatus.STOPPED) && ( - - - - - - )} + + {iterationCount > 0 && ( + + {iterationCount} + + )} - {data.runStatus === StepStatus.FAILED && ( - - - - - - )} + {(data.runStatus === StepStatus.SUCCESS || + data.runStatus === StepStatus.STOPPED) && ( + + + + + + )} - {(data.runStatus === StepStatus.RUNNING || - data.runStatus === StepStatus.PENDING) && ( - - - - )} + {data.runStatus === StepStatus.FAILED && ( + + + + + + )} + + {(data.runStatus === StepStatus.RUNNING || + data.runStatus === StepStatus.PENDING) && ( + + + + )} + diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/utils/getNodeIterationCount.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/utils/getNodeIterationCount.ts new file mode 100644 index 00000000000..3af1ee26dfa --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/utils/getNodeIterationCount.ts @@ -0,0 +1,15 @@ +import { getWorkflowRunAllStepInfoHistory } from '@/workflow/workflow-steps/utils/getWorkflowRunAllStepInfoHistory'; +import { StepStatus, type WorkflowRunStepInfo } from 'twenty-shared/workflow'; +export const getNodeIterationCount = ({ + stepInfo, +}: { + stepInfo: WorkflowRunStepInfo; +}) => { + const stepInfoHistory = getWorkflowRunAllStepInfoHistory({ stepInfo }); + + return stepInfoHistory.filter( + (stepInfo) => + stepInfo.status === StepStatus.SUCCESS || + stepInfo.status === StepStatus.STOPPED, + ).length; +}; diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service.ts index 086f1ca0432..bcb250d9b16 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service.ts @@ -224,10 +224,6 @@ export class WorkflowExecutorWorkspaceService { const steps = workflowRun.state.flow.steps; - if (workflowShouldKeepRunning({ stepInfos, steps })) { - return; - } - if (workflowShouldFail({ stepInfos, steps })) { await this.workflowRunWorkspaceService.endWorkflowRun({ workflowRunId, @@ -239,6 +235,10 @@ export class WorkflowExecutorWorkspaceService { return; } + if (workflowShouldKeepRunning({ stepInfos, steps })) { + return; + } + await this.workflowRunWorkspaceService.endWorkflowRun({ workflowRunId, workspaceId,