From b3153535a6036936728522b3aa8798f1c0ae1e45 Mon Sep 17 00:00:00 2001 From: Baptiste Devessier Date: Wed, 1 Oct 2025 14:15:07 +0200 Subject: [PATCH] Fix step output for iterators (#14796) It also prevents the app from crashing when opening the output of the trigger node. ## Before CleanShot 2025-09-30 at 19 44
39@2x ## After https://github.com/user-attachments/assets/b7585690-dd3b-4875-9c3e-18b60b3acc3d --- .../hooks/useWorkflowRunStepInfo.ts | 13 +- .../__tests__/getStepInfoHistoryItem.test.ts | 132 ++++++++++++++++++ .../utils/getIsDescendantOfIterator.ts | 5 + .../utils/getStepInfoHistoryItem.ts | 26 ++++ 4 files changed, 171 insertions(+), 5 deletions(-) create mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/utils/__tests__/getStepInfoHistoryItem.test.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/utils/getStepInfoHistoryItem.ts diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useWorkflowRunStepInfo.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useWorkflowRunStepInfo.ts index 1f2761a592..424c0b3801 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useWorkflowRunStepInfo.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useWorkflowRunStepInfo.ts @@ -2,8 +2,8 @@ import { workflowRunIteratorSubStepIterationIndexComponentState } from '@/comman import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun'; import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow'; -import { getWorkflowRunAllStepInfoHistory } from '@/workflow/workflow-steps/utils/getWorkflowRunAllStepInfoHistory'; import { isDefined } from 'twenty-shared/utils'; +import { getStepInfoHistoryItem } from '../utils/getStepInfoHistoryItem'; export const useWorkflowRunStepInfo = ({ stepId }: { stepId: string }) => { const workflowRunId = useWorkflowRunIdOrThrow(); @@ -15,11 +15,14 @@ export const useWorkflowRunStepInfo = ({ stepId }: { stepId: string }) => { const stepInfo = workflowRun?.state?.stepInfos[stepId]; - if (!isDefined(stepInfo)) { + if (!isDefined(stepInfo) || !isDefined(workflowRun?.state?.flow)) { return undefined; } - const allStepInfoHistory = getWorkflowRunAllStepInfoHistory({ stepInfo }); - - return allStepInfoHistory[workflowRunIteratorSubStepIterationIndex]; + return getStepInfoHistoryItem({ + stepInfo, + steps: workflowRun.state.flow.steps, + stepId, + iterationIndex: workflowRunIteratorSubStepIterationIndex, + }); }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/utils/__tests__/getStepInfoHistoryItem.test.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/utils/__tests__/getStepInfoHistoryItem.test.ts new file mode 100644 index 0000000000..6ff169950d --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/utils/__tests__/getStepInfoHistoryItem.test.ts @@ -0,0 +1,132 @@ +import { type WorkflowStep } from '@/workflow/types/Workflow'; +import { type WorkflowRunStepInfo, StepStatus } from 'twenty-shared/workflow'; +import { getStepInfoHistoryItem } from '../getStepInfoHistoryItem'; + +describe('getStepInfoHistoryItem', () => { + const iteratorStep: WorkflowStep = { + id: 'iterator1', + name: 'Iterator', + type: 'ITERATOR', + valid: true, + nextStepIds: ['step2'], + settings: { + input: { initialLoopStepIds: ['step2'] }, + outputSchema: {}, + errorHandlingOptions: { + retryOnFailure: { value: false }, + continueOnFailure: { value: false }, + }, + }, + }; + + const codeStep: WorkflowStep = { + id: 'step2', + name: 'Step 2', + type: 'CODE', + valid: true, + nextStepIds: [], + settings: { + input: { + serverlessFunctionId: 'func', + serverlessFunctionVersion: '1.0.0', + serverlessFunctionInput: {}, + }, + outputSchema: {}, + errorHandlingOptions: { + retryOnFailure: { value: false }, + continueOnFailure: { value: false }, + }, + }, + }; + + const steps: WorkflowStep[] = [iteratorStep, codeStep]; + + it('returns the correct history item for a descendant of iterator', () => { + const stepInfo: WorkflowRunStepInfo = { + result: 'final', + status: StepStatus.SUCCESS, + history: [ + { result: 'first', status: StepStatus.SUCCESS }, + { result: 'second', status: StepStatus.SUCCESS }, + ], + }; + + expect( + getStepInfoHistoryItem({ + stepInfo, + steps, + stepId: 'step2', + iterationIndex: 0, + })?.result, + ).toBe('first'); + expect( + getStepInfoHistoryItem({ + stepInfo, + steps, + stepId: 'step2', + iterationIndex: 1, + })?.result, + ).toBe('second'); + expect( + getStepInfoHistoryItem({ + stepInfo, + steps, + stepId: 'step2', + iterationIndex: 2, + })?.result, + ).toBe('final'); + }); + + it('returns the last history item for a non-descendant', () => { + const stepInfo: WorkflowRunStepInfo = { + result: 'final', + status: StepStatus.SUCCESS, + history: [ + { result: 'first', status: StepStatus.SUCCESS }, + { result: 'second', status: StepStatus.SUCCESS }, + ], + }; + + expect( + getStepInfoHistoryItem({ + stepInfo, + steps, + stepId: 'iterator1', + iterationIndex: 0, + })?.result, + ).toBe('final'); + }); + + it('returns undefined if iterationIndex is out of bounds', () => { + const stepInfo: WorkflowRunStepInfo = { + result: 'final', + status: StepStatus.SUCCESS, + history: [{ result: 'first', status: StepStatus.SUCCESS }], + }; + + expect( + getStepInfoHistoryItem({ + stepInfo, + steps, + stepId: 'step2', + iterationIndex: 5, + }), + ).toBeUndefined(); + }); + + it('returns the only item if no history', () => { + const stepInfo: WorkflowRunStepInfo = { + result: 'only', + status: StepStatus.SUCCESS, + }; + + expect( + getStepInfoHistoryItem({ + stepInfo, + steps, + stepId: 'step2', + iterationIndex: 0, + })?.result, + ).toBe('only'); + }); +}); diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/utils/getIsDescendantOfIterator.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/utils/getIsDescendantOfIterator.ts index 32c4ce10f2..580b7efc25 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/utils/getIsDescendantOfIterator.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/utils/getIsDescendantOfIterator.ts @@ -1,5 +1,6 @@ import { type WorkflowStep } from '@/workflow/types/Workflow'; import { isParentStep } from '@/workflow/workflow-diagram/utils/isParentStep'; +import { TRIGGER_STEP_ID } from 'twenty-shared/workflow'; export const getIsDescendantOfIterator = ({ stepId, @@ -12,6 +13,10 @@ export const getIsDescendantOfIterator = ({ currentStepId: string, visited = new Set(), ): boolean => { + if (currentStepId === TRIGGER_STEP_ID) { + return false; + } + const currentStep = steps.find((step) => step.id === currentStepId); if (!currentStep) { throw new Error(`Step with ID ${currentStepId} not found`); diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/utils/getStepInfoHistoryItem.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/utils/getStepInfoHistoryItem.ts new file mode 100644 index 0000000000..61bca23dac --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/utils/getStepInfoHistoryItem.ts @@ -0,0 +1,26 @@ +import { type WorkflowStep } from '@/workflow/types/Workflow'; +import { type WorkflowRunStepInfo } from 'twenty-shared/workflow'; +import { getIsDescendantOfIterator } from './getIsDescendantOfIterator'; +import { getWorkflowRunAllStepInfoHistory } from './getWorkflowRunAllStepInfoHistory'; + +export const getStepInfoHistoryItem = ({ + stepInfo, + steps, + stepId, + iterationIndex, +}: { + stepInfo: WorkflowRunStepInfo; + steps: WorkflowStep[]; + stepId: string; + iterationIndex: number; +}) => { + const allStepInfoHistory = getWorkflowRunAllStepInfoHistory({ stepInfo }); + + const isDescendantOfIterator = getIsDescendantOfIterator({ steps, stepId }); + + if (isDescendantOfIterator) { + return allStepInfoHistory[iterationIndex]; + } + + return allStepInfoHistory.at(-1); +};