Iterators design updates (#15146)
A few fixes : - add a counter of iteration for nodes that have been run <img width="601" height="538" alt="Capture d’écran 2025-10-16 à 17 50 07" src="https://github.com/user-attachments/assets/de9d56ed-3057-4a67-b1d7-b421f644d11d" /> - 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
This commit is contained in:
+3
-1
@@ -6,6 +6,8 @@ export const getIsOutputTabDisabled = ({
|
||||
stepExecutionStatus: WorkflowRunStepStatus;
|
||||
}) => {
|
||||
return (
|
||||
stepExecutionStatus === 'RUNNING' || stepExecutionStatus === 'NOT_STARTED'
|
||||
stepExecutionStatus === 'RUNNING' ||
|
||||
stepExecutionStatus === 'NOT_STARTED' ||
|
||||
stepExecutionStatus === 'SKIPPED'
|
||||
);
|
||||
};
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ export const WorkflowDiagramDefaultEdgeEditable = ({
|
||||
|
||||
const {
|
||||
segments,
|
||||
labelPosition: [labelX, labelY],
|
||||
overlayPosition: [labelX, labelY],
|
||||
} = getEdgePath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
|
||||
+13
-4
@@ -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],
|
||||
};
|
||||
};
|
||||
|
||||
+63
-23
@@ -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)}
|
||||
</WorkflowNodeLabel>
|
||||
|
||||
{(data.runStatus === StepStatus.SUCCESS ||
|
||||
data.runStatus === StepStatus.STOPPED) && (
|
||||
<StyledNodeCounter>
|
||||
<StyledColorIcon color={theme.tag.background.turquoise}>
|
||||
<IconCheck color={theme.tag.text.turquoise} size={14} />
|
||||
</StyledColorIcon>
|
||||
</StyledNodeCounter>
|
||||
)}
|
||||
<StyledRightPartContainer>
|
||||
{iterationCount > 0 && (
|
||||
<StyledIterationCounter runStatus={data.runStatus}>
|
||||
{iterationCount}
|
||||
</StyledIterationCounter>
|
||||
)}
|
||||
|
||||
{data.runStatus === StepStatus.FAILED && (
|
||||
<StyledNodeCounter>
|
||||
<StyledColorIcon color={theme.tag.background.red}>
|
||||
<IconX color={theme.tag.text.red} size={14} />
|
||||
</StyledColorIcon>
|
||||
</StyledNodeCounter>
|
||||
)}
|
||||
{(data.runStatus === StepStatus.SUCCESS ||
|
||||
data.runStatus === StepStatus.STOPPED) && (
|
||||
<StyledStatusIconsContainer>
|
||||
<StyledColorIcon color={theme.tag.background.turquoise}>
|
||||
<IconCheck color={theme.tag.text.turquoise} size={14} />
|
||||
</StyledColorIcon>
|
||||
</StyledStatusIconsContainer>
|
||||
)}
|
||||
|
||||
{(data.runStatus === StepStatus.RUNNING ||
|
||||
data.runStatus === StepStatus.PENDING) && (
|
||||
<StyledNodeCounter>
|
||||
<Loader color="yellow" />
|
||||
</StyledNodeCounter>
|
||||
)}
|
||||
{data.runStatus === StepStatus.FAILED && (
|
||||
<StyledStatusIconsContainer>
|
||||
<StyledColorIcon color={theme.tag.background.red}>
|
||||
<IconX color={theme.tag.text.red} size={14} />
|
||||
</StyledColorIcon>
|
||||
</StyledStatusIconsContainer>
|
||||
)}
|
||||
|
||||
{(data.runStatus === StepStatus.RUNNING ||
|
||||
data.runStatus === StepStatus.PENDING) && (
|
||||
<StyledStatusIconsContainer>
|
||||
<Loader color="yellow" />
|
||||
</StyledStatusIconsContainer>
|
||||
)}
|
||||
</StyledRightPartContainer>
|
||||
</StyledNodeLabelWithCounterPart>
|
||||
|
||||
<WorkflowNodeTitle runStatus={data.runStatus}>
|
||||
|
||||
+15
@@ -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;
|
||||
};
|
||||
+4
-4
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user