Use variables within Iterators (#14604)
Variables were not working properly. Output schema was outdated and parent steps were not calculated properly. Before https://github.com/user-attachments/assets/b070c1ff-41dd-4ea0-a590-25bb39027456 After https://github.com/user-attachments/assets/baa43e69-0e7c-4697-8812-3ce523efba13
This commit is contained in:
+176
-99
@@ -1,111 +1,188 @@
|
||||
import { type WorkflowStep } from '@/workflow/types/Workflow';
|
||||
import { getPreviousSteps } from '../getWorkflowPreviousSteps';
|
||||
|
||||
const mockWorkflow: WorkflowStep[] = [
|
||||
{
|
||||
id: 'step1',
|
||||
name: 'First Step',
|
||||
type: 'CODE',
|
||||
valid: true,
|
||||
nextStepIds: ['step2', 'step3'],
|
||||
settings: {
|
||||
input: {
|
||||
serverlessFunctionId: 'func1',
|
||||
serverlessFunctionVersion: '1.0.0',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: true },
|
||||
continueOnFailure: { value: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'step2',
|
||||
name: 'Second Step',
|
||||
type: 'CODE',
|
||||
valid: true,
|
||||
nextStepIds: ['step4'],
|
||||
settings: {
|
||||
input: {
|
||||
serverlessFunctionId: 'func2',
|
||||
serverlessFunctionVersion: '1.0.0',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: true },
|
||||
continueOnFailure: { value: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'step3',
|
||||
name: 'Third Step',
|
||||
type: 'CODE',
|
||||
valid: true,
|
||||
nextStepIds: ['step4'],
|
||||
settings: {
|
||||
input: {
|
||||
serverlessFunctionId: 'func3',
|
||||
serverlessFunctionVersion: '1.0.0',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: true },
|
||||
continueOnFailure: { value: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'step4',
|
||||
name: 'Fourth Step',
|
||||
type: 'CODE',
|
||||
valid: true,
|
||||
nextStepIds: [],
|
||||
settings: {
|
||||
input: {
|
||||
serverlessFunctionId: 'func4',
|
||||
serverlessFunctionVersion: '1.0.0',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: true },
|
||||
continueOnFailure: { value: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('getWorkflowPreviousSteps', () => {
|
||||
it('should return empty array when there are no previous steps', () => {
|
||||
const result = getPreviousSteps(mockWorkflow, 'step1');
|
||||
expect(result).toEqual([]);
|
||||
describe('using a simple workflow', () => {
|
||||
const mockWorkflow: WorkflowStep[] = [
|
||||
{
|
||||
id: 'step1',
|
||||
name: 'First Step',
|
||||
type: 'CODE',
|
||||
valid: true,
|
||||
nextStepIds: ['step2', 'step3'],
|
||||
settings: {
|
||||
input: {
|
||||
serverlessFunctionId: 'func1',
|
||||
serverlessFunctionVersion: '1.0.0',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: true },
|
||||
continueOnFailure: { value: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'step2',
|
||||
name: 'Second Step',
|
||||
type: 'CODE',
|
||||
valid: true,
|
||||
nextStepIds: ['step4'],
|
||||
settings: {
|
||||
input: {
|
||||
serverlessFunctionId: 'func2',
|
||||
serverlessFunctionVersion: '1.0.0',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: true },
|
||||
continueOnFailure: { value: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'step3',
|
||||
name: 'Third Step',
|
||||
type: 'CODE',
|
||||
valid: true,
|
||||
nextStepIds: ['step4'],
|
||||
settings: {
|
||||
input: {
|
||||
serverlessFunctionId: 'func3',
|
||||
serverlessFunctionVersion: '1.0.0',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: true },
|
||||
continueOnFailure: { value: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'step4',
|
||||
name: 'Fourth Step',
|
||||
type: 'CODE',
|
||||
valid: true,
|
||||
nextStepIds: [],
|
||||
settings: {
|
||||
input: {
|
||||
serverlessFunctionId: 'func4',
|
||||
serverlessFunctionVersion: '1.0.0',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: true },
|
||||
continueOnFailure: { value: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
it('should return empty array when there are no previous steps', () => {
|
||||
const result = getPreviousSteps({
|
||||
steps: mockWorkflow,
|
||||
currentStep: mockWorkflow[0],
|
||||
});
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return direct previous steps', () => {
|
||||
const result = getPreviousSteps({
|
||||
steps: mockWorkflow,
|
||||
currentStep: mockWorkflow[1],
|
||||
});
|
||||
expect(result).toEqual([mockWorkflow[0]]);
|
||||
});
|
||||
|
||||
it('should return all previous steps including indirect ones', () => {
|
||||
const result = getPreviousSteps({
|
||||
steps: mockWorkflow,
|
||||
currentStep: mockWorkflow[3],
|
||||
});
|
||||
expect(result).toEqual([
|
||||
mockWorkflow[0],
|
||||
mockWorkflow[1],
|
||||
mockWorkflow[2],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle circular dependencies', () => {
|
||||
const circularWorkflow = [...mockWorkflow];
|
||||
circularWorkflow[3].nextStepIds = ['step1']; // Make step4 point back to step1
|
||||
|
||||
const result = getPreviousSteps({
|
||||
steps: circularWorkflow,
|
||||
currentStep: circularWorkflow[3],
|
||||
});
|
||||
expect(result).toEqual([
|
||||
mockWorkflow[0],
|
||||
mockWorkflow[1],
|
||||
mockWorkflow[2],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return direct previous steps', () => {
|
||||
const result = getPreviousSteps(mockWorkflow, 'step2');
|
||||
expect(result).toEqual([mockWorkflow[0]]);
|
||||
});
|
||||
describe('using a workflow with an iterator', () => {
|
||||
const mockWorkflowWithIterator = [
|
||||
{
|
||||
id: 'iterator1',
|
||||
name: 'Iterator Step',
|
||||
type: 'ITERATOR',
|
||||
valid: true,
|
||||
nextStepIds: ['step3'],
|
||||
settings: {
|
||||
input: {
|
||||
initialLoopStepIds: ['step2'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'step2',
|
||||
name: 'Second Step',
|
||||
type: 'CODE',
|
||||
valid: true,
|
||||
nextStepIds: ['iterator1'],
|
||||
settings: {},
|
||||
},
|
||||
{
|
||||
id: 'step3',
|
||||
name: 'Third Step',
|
||||
type: 'CODE',
|
||||
valid: true,
|
||||
nextStepIds: [],
|
||||
settings: {},
|
||||
},
|
||||
] as WorkflowStep[];
|
||||
|
||||
it('should return all previous steps including indirect ones', () => {
|
||||
const result = getPreviousSteps(mockWorkflow, 'step4');
|
||||
expect(result).toEqual([mockWorkflow[0], mockWorkflow[1], mockWorkflow[2]]);
|
||||
});
|
||||
it('should consider iterator step as parent of loop steps', () => {
|
||||
const result = getPreviousSteps({
|
||||
steps: mockWorkflowWithIterator,
|
||||
currentStep: mockWorkflowWithIterator[1],
|
||||
});
|
||||
|
||||
it('should handle circular dependencies', () => {
|
||||
const circularWorkflow = [...mockWorkflow];
|
||||
circularWorkflow[3].nextStepIds = ['step1']; // Make step4 point back to step1
|
||||
expect(result).toEqual([mockWorkflowWithIterator[0]]);
|
||||
});
|
||||
|
||||
const result = getPreviousSteps(circularWorkflow, 'step4');
|
||||
expect(result).toEqual([mockWorkflow[0], mockWorkflow[1], mockWorkflow[2]]);
|
||||
});
|
||||
it('should not consider loop step as parent of iterator', () => {
|
||||
const result = getPreviousSteps({
|
||||
steps: mockWorkflowWithIterator,
|
||||
currentStep: mockWorkflowWithIterator[0],
|
||||
});
|
||||
|
||||
it('should handle non-existent step ID', () => {
|
||||
const result = getPreviousSteps(mockWorkflow, 'non-existent-step');
|
||||
expect(result).toEqual([]);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should consider iterator step as parent of non-loop steps', () => {
|
||||
const result = getPreviousSteps({
|
||||
steps: mockWorkflowWithIterator,
|
||||
currentStep: mockWorkflowWithIterator[2],
|
||||
});
|
||||
|
||||
expect(result).toEqual([mockWorkflowWithIterator[0]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+49
-7
@@ -1,12 +1,54 @@
|
||||
import { type WorkflowStep } from '@/workflow/types/Workflow';
|
||||
import { isLastStepOfLoop } from '@/workflow/workflow-diagram/utils/isLastStepOfLoop';
|
||||
|
||||
export const getPreviousSteps = (
|
||||
steps: WorkflowStep[],
|
||||
currentStepId: string,
|
||||
visitedSteps: Set<string> = new Set([currentStepId]),
|
||||
): WorkflowStep[] => {
|
||||
const isParentStep = ({
|
||||
currentStep,
|
||||
potentialParentStep,
|
||||
steps,
|
||||
}: {
|
||||
currentStep: WorkflowStep;
|
||||
potentialParentStep: WorkflowStep;
|
||||
steps: WorkflowStep[];
|
||||
}) => {
|
||||
if (potentialParentStep.type === 'ITERATOR') {
|
||||
return (
|
||||
potentialParentStep.settings.input.initialLoopStepIds?.includes(
|
||||
currentStep.id,
|
||||
) || potentialParentStep.nextStepIds?.includes(currentStep.id)
|
||||
);
|
||||
}
|
||||
|
||||
if (currentStep.type === 'ITERATOR') {
|
||||
return (
|
||||
potentialParentStep.nextStepIds?.includes(currentStep.id) &&
|
||||
!isLastStepOfLoop({
|
||||
iterator: currentStep,
|
||||
stepId: potentialParentStep.id,
|
||||
steps,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return potentialParentStep.nextStepIds?.includes(currentStep.id);
|
||||
};
|
||||
|
||||
export const getPreviousSteps = ({
|
||||
steps,
|
||||
currentStep,
|
||||
visitedSteps = new Set([currentStep.id]),
|
||||
}: {
|
||||
steps: WorkflowStep[];
|
||||
currentStep: WorkflowStep;
|
||||
visitedSteps?: Set<string>;
|
||||
}): WorkflowStep[] => {
|
||||
const parentSteps = steps
|
||||
.filter((step) => step.nextStepIds?.includes(currentStepId))
|
||||
.filter((step) =>
|
||||
isParentStep({
|
||||
currentStep,
|
||||
potentialParentStep: step,
|
||||
steps,
|
||||
}),
|
||||
)
|
||||
.filter((step) => !visitedSteps.has(step.id));
|
||||
|
||||
const grandParentSteps = parentSteps
|
||||
@@ -15,7 +57,7 @@ export const getPreviousSteps = (
|
||||
return [];
|
||||
}
|
||||
visitedSteps.add(step.id);
|
||||
return getPreviousSteps(steps, step.id, visitedSteps);
|
||||
return getPreviousSteps({ steps, currentStep: step, visitedSteps });
|
||||
})
|
||||
.flat();
|
||||
|
||||
|
||||
+11
-1
@@ -1,5 +1,6 @@
|
||||
import { type WorkflowRunFlow } from '@/workflow/types/Workflow';
|
||||
import { getPreviousSteps } from '@/workflow/workflow-steps/utils/getWorkflowPreviousSteps';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
getWorkflowRunContext,
|
||||
TRIGGER_STEP_ID,
|
||||
@@ -19,7 +20,16 @@ export const getWorkflowRunStepContext = ({
|
||||
return [];
|
||||
}
|
||||
|
||||
const previousSteps = getPreviousSteps(flow.steps, stepId);
|
||||
const currentStep = flow.steps.find((step) => step.id === stepId);
|
||||
|
||||
if (!isDefined(currentStep)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const previousSteps = getPreviousSteps({
|
||||
steps: flow.steps,
|
||||
currentStep,
|
||||
});
|
||||
|
||||
const context = getWorkflowRunContext(stepInfos);
|
||||
|
||||
|
||||
+3
-2
@@ -25,9 +25,10 @@ export const useAvailableVariablesInWorkflowStep = ({
|
||||
);
|
||||
const flow = useFlowOrThrow();
|
||||
const steps = flow.steps ?? [];
|
||||
const currentStep = steps.find((step) => step.id === workflowSelectedNode);
|
||||
|
||||
const previousStepIds: string[] = isDefined(workflowSelectedNode)
|
||||
? getPreviousSteps(steps, workflowSelectedNode).map((step) => step.id)
|
||||
const previousStepIds: string[] = isDefined(currentStep)
|
||||
? getPreviousSteps({ steps, currentStep }).map((step) => step.id)
|
||||
: [];
|
||||
|
||||
const availableStepsOutputSchema: StepOutputSchemaV2[] = useRecoilValue(
|
||||
|
||||
+14
-1
@@ -78,11 +78,24 @@ export class WorkflowSchemaWorkspaceService {
|
||||
});
|
||||
case WorkflowActionType.ITERATOR: {
|
||||
return {
|
||||
nextItemToProcess: {
|
||||
currentItem: {
|
||||
label: 'Current Item',
|
||||
isLeaf: true,
|
||||
type: 'unknown',
|
||||
value: generateFakeValue('unknown'),
|
||||
},
|
||||
currentItemIndex: {
|
||||
label: 'Current Item Index',
|
||||
isLeaf: true,
|
||||
type: 'number',
|
||||
value: generateFakeValue('number'),
|
||||
},
|
||||
hasProcessedAllItems: {
|
||||
label: 'Has Processed All Items',
|
||||
isLeaf: true,
|
||||
type: 'boolean',
|
||||
value: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
case WorkflowActionType.CODE: // StepOutput schema is computed on serverlessFunction draft execution
|
||||
|
||||
Reference in New Issue
Block a user