Fix step output for iterators (#14796)

It also prevents the app from crashing when opening the output of the
trigger node.

## Before

<img width="3738" height="2442" alt="CleanShot 2025-09-30 at 19 44
39@2x"
src="https://github.com/user-attachments/assets/39fa81b1-2c25-402e-9030-770054870f70"
/>

## After


https://github.com/user-attachments/assets/b7585690-dd3b-4875-9c3e-18b60b3acc3d
This commit is contained in:
Baptiste Devessier
2025-10-01 14:15:07 +02:00
committed by GitHub
parent bc37a3a9bb
commit b3153535a6
4 changed files with 171 additions and 5 deletions
@@ -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,
});
};
@@ -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');
});
});
@@ -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<string>(),
): 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`);
@@ -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);
};