Visualize workflow run step input (#14784)
https://github.com/user-attachments/assets/8358a252-0407-4925-a923-7b1d6c7404b0
This commit is contained in:
+13
@@ -1,8 +1,11 @@
|
||||
import { workflowRunIteratorSubStepIterationIndexComponentState } from '@/command-menu/pages/workflow/step/view-run/states/workflowRunIteratorSubStepIterationIndexComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
|
||||
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
|
||||
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
|
||||
import { WorkflowRunStepJsonContainer } from '@/workflow/workflow-steps/components/WorkflowRunStepJsonContainer';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { getIsDescendantOfIterator } from '@/workflow/workflow-steps/utils/getIsDescendantOfIterator';
|
||||
import { getWorkflowRunStepContext } from '@/workflow/workflow-steps/utils/getWorkflowRunStepContext';
|
||||
import { getWorkflowVariablesUsedInStep } from '@/workflow/workflow-steps/utils/getWorkflowVariablesUsedInStep';
|
||||
import { getActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionHeaderTypeOrThrow';
|
||||
@@ -33,6 +36,10 @@ export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
|
||||
(step) => step.id === stepId,
|
||||
);
|
||||
|
||||
const workflowRunIteratorSubStepIterationIndex = useRecoilComponentValue(
|
||||
workflowRunIteratorSubStepIterationIndexComponentState,
|
||||
);
|
||||
|
||||
if (
|
||||
!(
|
||||
isDefined(workflowRun) &&
|
||||
@@ -71,6 +78,12 @@ export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
|
||||
stepInfos: workflowRun.state.stepInfos,
|
||||
flow: workflowRun.state.flow,
|
||||
stepId,
|
||||
currentLoopIterationIndex: getIsDescendantOfIterator({
|
||||
stepId,
|
||||
steps: workflowRun.state.flow.steps,
|
||||
})
|
||||
? workflowRunIteratorSubStepIterationIndex
|
||||
: undefined,
|
||||
});
|
||||
|
||||
if (stepContext.length === 0) {
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
export type WorkflowRunStepContext = {
|
||||
id: string;
|
||||
name: string;
|
||||
context: unknown;
|
||||
};
|
||||
+245
-231
@@ -2,284 +2,298 @@ import { type WorkflowRunFlow } from '@/workflow/types/Workflow';
|
||||
import { StepStatus, TRIGGER_STEP_ID } from 'twenty-shared/workflow';
|
||||
import { getWorkflowRunStepContext } from '../getWorkflowRunStepContext';
|
||||
|
||||
describe('getWorkflowRunStepContext', () => {
|
||||
it('should return an empty array for trigger step', () => {
|
||||
const flow = {
|
||||
trigger: {
|
||||
name: 'Company Created',
|
||||
type: 'DATABASE_EVENT',
|
||||
settings: {
|
||||
eventName: 'company.created',
|
||||
outputSchema: {},
|
||||
const mockFlow = {
|
||||
trigger: {
|
||||
type: 'MANUAL',
|
||||
name: 'Trigger',
|
||||
settings: { outputSchema: {} },
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
id: 'step1',
|
||||
name: 'Step 1',
|
||||
type: 'CODE',
|
||||
nextStepIds: ['step2'],
|
||||
valid: true,
|
||||
settings: {
|
||||
input: {
|
||||
serverlessFunctionId: '',
|
||||
serverlessFunctionInput: {},
|
||||
serverlessFunctionVersion: '',
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: false },
|
||||
continueOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
steps: [],
|
||||
} satisfies WorkflowRunFlow;
|
||||
const stepInfos = {
|
||||
[TRIGGER_STEP_ID]: {
|
||||
result: { company: { id: '123' } },
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
{
|
||||
id: 'step2',
|
||||
name: 'Step 2',
|
||||
type: 'CODE',
|
||||
nextStepIds: [],
|
||||
valid: true,
|
||||
settings: {
|
||||
input: {
|
||||
serverlessFunctionId: '',
|
||||
serverlessFunctionInput: {},
|
||||
serverlessFunctionVersion: '',
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: false },
|
||||
continueOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
],
|
||||
} satisfies WorkflowRunFlow;
|
||||
|
||||
const mockStepInfos = {
|
||||
[TRIGGER_STEP_ID]: { result: 'trigger-result', status: StepStatus.SUCCESS },
|
||||
step1: { result: 'step1-result', status: StepStatus.SUCCESS },
|
||||
step2: { result: 'step2-result', status: StepStatus.SUCCESS },
|
||||
};
|
||||
|
||||
const mockStepInfosWithHistory = {
|
||||
[TRIGGER_STEP_ID]: { result: 'trigger-result', status: StepStatus.SUCCESS },
|
||||
step1: {
|
||||
result: 'step1-result',
|
||||
status: StepStatus.SUCCESS,
|
||||
history: [
|
||||
{ result: 'step1-result', status: StepStatus.SUCCESS },
|
||||
{ result: 'step1-result-2', status: StepStatus.SUCCESS },
|
||||
],
|
||||
},
|
||||
step2: {
|
||||
result: 'step2-result',
|
||||
status: StepStatus.SUCCESS,
|
||||
history: [
|
||||
{ result: 'step2-result', status: StepStatus.SUCCESS },
|
||||
{ result: 'step2-result-2', status: StepStatus.SUCCESS },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
describe('getWorkflowRunStepContext', () => {
|
||||
it('returns empty array for trigger step', () => {
|
||||
const result = getWorkflowRunStepContext({
|
||||
stepId: TRIGGER_STEP_ID,
|
||||
flow,
|
||||
stepInfos,
|
||||
flow: mockFlow,
|
||||
stepInfos: mockStepInfos,
|
||||
currentLoopIterationIndex: undefined,
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should include previous steps context', () => {
|
||||
const flow = {
|
||||
trigger: {
|
||||
name: 'Company Created',
|
||||
type: 'DATABASE_EVENT',
|
||||
settings: {
|
||||
eventName: 'company.created',
|
||||
outputSchema: {},
|
||||
},
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
id: 'step1',
|
||||
name: 'Create company',
|
||||
type: 'CREATE_RECORD',
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
input: {
|
||||
objectName: 'Company',
|
||||
objectRecord: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
valid: true,
|
||||
nextStepIds: ['step2'],
|
||||
},
|
||||
{
|
||||
id: 'step2',
|
||||
name: 'Send Email',
|
||||
type: 'SEND_EMAIL',
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
input: {
|
||||
connectedAccountId: '123',
|
||||
email: '',
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
valid: true,
|
||||
nextStepIds: [],
|
||||
},
|
||||
],
|
||||
} satisfies WorkflowRunFlow;
|
||||
|
||||
const stepInfos = {
|
||||
[TRIGGER_STEP_ID]: {
|
||||
result: { company: { id: '123' } },
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
step1: { result: { taskId: '456' }, status: StepStatus.SUCCESS },
|
||||
step2: { result: { taskId: '456' }, status: StepStatus.SUCCESS },
|
||||
};
|
||||
|
||||
it('returns context for previous steps and trigger', () => {
|
||||
const result = getWorkflowRunStepContext({
|
||||
stepId: 'step2',
|
||||
flow,
|
||||
stepInfos,
|
||||
flow: mockFlow,
|
||||
stepInfos: mockStepInfos,
|
||||
currentLoopIterationIndex: undefined,
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: TRIGGER_STEP_ID,
|
||||
name: 'Company Created',
|
||||
context: { company: { id: '123' } },
|
||||
},
|
||||
{
|
||||
id: 'step1',
|
||||
name: 'Create company',
|
||||
context: { taskId: '456' },
|
||||
},
|
||||
{ id: TRIGGER_STEP_ID, name: 'Trigger', context: 'trigger-result' },
|
||||
{ id: 'step1', name: 'Step 1', context: 'step1-result' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not include subsequent steps context', () => {
|
||||
const flow = {
|
||||
trigger: {
|
||||
name: 'Company Created',
|
||||
type: 'DATABASE_EVENT',
|
||||
settings: {
|
||||
eventName: 'company.created',
|
||||
outputSchema: {},
|
||||
},
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
id: 'step1',
|
||||
name: 'Create company',
|
||||
type: 'CREATE_RECORD',
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
input: {
|
||||
objectName: 'Company',
|
||||
objectRecord: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
id: 'step2',
|
||||
name: 'Send Email',
|
||||
type: 'SEND_EMAIL',
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
input: {
|
||||
connectedAccountId: '123',
|
||||
email: '',
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
valid: true,
|
||||
},
|
||||
],
|
||||
} satisfies WorkflowRunFlow;
|
||||
const stepInfos = {
|
||||
[TRIGGER_STEP_ID]: {
|
||||
result: { company: { id: '123' } },
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
step1: { result: { taskId: '456' }, status: StepStatus.SUCCESS },
|
||||
step2: { result: { emailId: '789' }, status: StepStatus.SUCCESS },
|
||||
};
|
||||
|
||||
it('returns context for previous steps with loop index', () => {
|
||||
const result = getWorkflowRunStepContext({
|
||||
stepId: 'step1',
|
||||
flow,
|
||||
stepInfos,
|
||||
stepId: 'step2',
|
||||
flow: mockFlow,
|
||||
stepInfos: mockStepInfosWithHistory,
|
||||
currentLoopIterationIndex: 1,
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: TRIGGER_STEP_ID,
|
||||
name: 'Company Created',
|
||||
context: { company: { id: '123' } },
|
||||
},
|
||||
{ id: TRIGGER_STEP_ID, name: 'Trigger', context: 'trigger-result' },
|
||||
{ id: 'step1', name: 'Step 1', context: 'step1-result-2' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle multiple steps with the same name', () => {
|
||||
const flow = {
|
||||
it('returns empty array if step not found', () => {
|
||||
const result = getWorkflowRunStepContext({
|
||||
stepId: 'unknown',
|
||||
flow: mockFlow,
|
||||
stepInfos: mockStepInfos,
|
||||
currentLoopIterationIndex: undefined,
|
||||
});
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('handles iterator step type and loop context', () => {
|
||||
const flowWithIterator = {
|
||||
trigger: {
|
||||
name: 'Company Created',
|
||||
type: 'DATABASE_EVENT',
|
||||
settings: {
|
||||
eventName: 'company.created',
|
||||
outputSchema: {},
|
||||
},
|
||||
type: 'MANUAL',
|
||||
name: 'Trigger',
|
||||
settings: { outputSchema: {} },
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
id: 'step1',
|
||||
name: 'Create Note',
|
||||
type: 'CREATE_RECORD',
|
||||
name: 'Step 1',
|
||||
type: 'CODE',
|
||||
nextStepIds: ['iterator1'],
|
||||
valid: true,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
input: {
|
||||
objectName: 'Note',
|
||||
objectRecord: {},
|
||||
serverlessFunctionId: '',
|
||||
serverlessFunctionInput: {},
|
||||
serverlessFunctionVersion: '',
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: false },
|
||||
continueOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
valid: true,
|
||||
nextStepIds: ['step2'],
|
||||
},
|
||||
{
|
||||
id: 'step2',
|
||||
name: 'Create Note',
|
||||
type: 'CREATE_RECORD',
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
input: {
|
||||
objectName: 'Note',
|
||||
objectRecord: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
valid: true,
|
||||
nextStepIds: ['step3'],
|
||||
},
|
||||
{
|
||||
id: 'step3',
|
||||
name: 'Create Note',
|
||||
type: 'CREATE_RECORD',
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
input: {
|
||||
objectName: 'Note',
|
||||
objectRecord: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
valid: true,
|
||||
id: 'iterator1',
|
||||
name: 'Iterator',
|
||||
type: 'ITERATOR',
|
||||
nextStepIds: [],
|
||||
valid: true,
|
||||
settings: {
|
||||
input: {
|
||||
initialLoopStepIds: ['step2'],
|
||||
items: [1, 2, 3],
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: false },
|
||||
continueOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'step2',
|
||||
name: 'Step 2',
|
||||
type: 'CODE',
|
||||
nextStepIds: ['iterator1'],
|
||||
valid: true,
|
||||
settings: {
|
||||
input: {
|
||||
serverlessFunctionId: '',
|
||||
serverlessFunctionInput: {},
|
||||
serverlessFunctionVersion: '',
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: false },
|
||||
continueOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
} satisfies WorkflowRunFlow;
|
||||
|
||||
const stepInfos = {
|
||||
[TRIGGER_STEP_ID]: {
|
||||
result: { company: { id: '123' } },
|
||||
result: 'trigger-result',
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
step1: { result: { noteId: '456' }, status: StepStatus.SUCCESS },
|
||||
step2: { result: { noteId: '789' }, status: StepStatus.SUCCESS },
|
||||
step3: { result: { noteId: '101' }, status: StepStatus.SUCCESS },
|
||||
step1: {
|
||||
result: 'step1-result',
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
iterator1: {
|
||||
result: {
|
||||
currentItemIndex: 2,
|
||||
hasProcessedAllItems: true,
|
||||
},
|
||||
status: StepStatus.SUCCESS,
|
||||
history: [
|
||||
{
|
||||
result: {
|
||||
currentItem: 1,
|
||||
currentItemIndex: 0,
|
||||
hasProcessedAllItems: false,
|
||||
},
|
||||
status: StepStatus.RUNNING,
|
||||
},
|
||||
{
|
||||
result: {
|
||||
currentItem: 2,
|
||||
currentItemIndex: 1,
|
||||
hasProcessedAllItems: false,
|
||||
},
|
||||
status: StepStatus.RUNNING,
|
||||
},
|
||||
],
|
||||
},
|
||||
step2: {
|
||||
result: 'step2-result-3',
|
||||
status: StepStatus.SUCCESS,
|
||||
history: [
|
||||
{ result: 'step2-result-1', status: StepStatus.SUCCESS },
|
||||
{ result: 'step2-result-2', status: StepStatus.SUCCESS },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = getWorkflowRunStepContext({
|
||||
stepId: 'step3',
|
||||
flow,
|
||||
const step2Iteration0Result = getWorkflowRunStepContext({
|
||||
stepId: 'step2',
|
||||
flow: flowWithIterator,
|
||||
stepInfos,
|
||||
currentLoopIterationIndex: 0,
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: TRIGGER_STEP_ID,
|
||||
name: 'Company Created',
|
||||
context: { company: { id: '123' } },
|
||||
},
|
||||
{
|
||||
id: 'step1',
|
||||
name: 'Create Note',
|
||||
context: { noteId: '456' },
|
||||
},
|
||||
{
|
||||
id: 'step2',
|
||||
name: 'Create Note',
|
||||
context: { noteId: '789' },
|
||||
},
|
||||
]);
|
||||
expect(step2Iteration0Result).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"context": "trigger-result",
|
||||
"id": "trigger",
|
||||
"name": "Trigger",
|
||||
},
|
||||
{
|
||||
"context": "step1-result",
|
||||
"id": "step1",
|
||||
"name": "Step 1",
|
||||
},
|
||||
{
|
||||
"context": {
|
||||
"currentItem": 1,
|
||||
"currentItemIndex": 0,
|
||||
"hasProcessedAllItems": false,
|
||||
},
|
||||
"id": "iterator1",
|
||||
"name": "Iterator",
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
||||
const step2Iteration1Result = getWorkflowRunStepContext({
|
||||
stepId: 'step2',
|
||||
flow: flowWithIterator,
|
||||
stepInfos,
|
||||
currentLoopIterationIndex: 1,
|
||||
});
|
||||
|
||||
expect(step2Iteration1Result).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"context": "trigger-result",
|
||||
"id": "trigger",
|
||||
"name": "Trigger",
|
||||
},
|
||||
{
|
||||
"context": "step1-result",
|
||||
"id": "step1",
|
||||
"name": "Step 1",
|
||||
},
|
||||
{
|
||||
"context": {
|
||||
"currentItem": 2,
|
||||
"currentItemIndex": 1,
|
||||
"hasProcessedAllItems": false,
|
||||
},
|
||||
"id": "iterator1",
|
||||
"name": "Iterator",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
+29
-9
@@ -1,8 +1,9 @@
|
||||
import { type WorkflowRunFlow } from '@/workflow/types/Workflow';
|
||||
import { type WorkflowRunStepContext } from '@/workflow/workflow-steps/types/WorkflowRunStepContext';
|
||||
import { getPreviousSteps } from '@/workflow/workflow-steps/utils/getWorkflowPreviousSteps';
|
||||
import { getWorkflowRunAllStepInfoHistory } from '@/workflow/workflow-steps/utils/getWorkflowRunAllStepInfoHistory';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
getWorkflowRunContext,
|
||||
TRIGGER_STEP_ID,
|
||||
type WorkflowRunStepInfos,
|
||||
} from 'twenty-shared/workflow';
|
||||
@@ -11,10 +12,12 @@ export const getWorkflowRunStepContext = ({
|
||||
stepId,
|
||||
flow,
|
||||
stepInfos,
|
||||
currentLoopIterationIndex,
|
||||
}: {
|
||||
stepId: string;
|
||||
stepInfos: WorkflowRunStepInfos;
|
||||
flow: WorkflowRunFlow;
|
||||
currentLoopIterationIndex: number | undefined;
|
||||
}) => {
|
||||
if (stepId === TRIGGER_STEP_ID) {
|
||||
return [];
|
||||
@@ -31,22 +34,39 @@ export const getWorkflowRunStepContext = ({
|
||||
currentStep,
|
||||
});
|
||||
|
||||
const context = getWorkflowRunContext(stepInfos);
|
||||
const reversedPreviousSteps = previousSteps.toReversed();
|
||||
|
||||
const previousStepsContext = previousSteps.map((step) => {
|
||||
return {
|
||||
const reversedPreviousStepsContext: WorkflowRunStepContext[] = [];
|
||||
|
||||
let isInLoop = isDefined(currentLoopIterationIndex);
|
||||
|
||||
for (const step of reversedPreviousSteps) {
|
||||
const stepInfoHistory = getWorkflowRunAllStepInfoHistory({
|
||||
stepInfo: stepInfos[step.id],
|
||||
});
|
||||
|
||||
const historyItemIndex =
|
||||
isDefined(currentLoopIterationIndex) && isInLoop
|
||||
? currentLoopIterationIndex
|
||||
: 0;
|
||||
|
||||
reversedPreviousStepsContext.push({
|
||||
id: step.id,
|
||||
name: step.name,
|
||||
context: context[step.id],
|
||||
};
|
||||
});
|
||||
context: stepInfoHistory[historyItemIndex].result,
|
||||
});
|
||||
|
||||
if (step.type === 'ITERATOR') {
|
||||
isInLoop = false;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
id: TRIGGER_STEP_ID,
|
||||
name: flow.trigger.name ?? 'Trigger',
|
||||
context: context[TRIGGER_STEP_ID],
|
||||
context: stepInfos[TRIGGER_STEP_ID].result,
|
||||
},
|
||||
...previousStepsContext,
|
||||
...reversedPreviousStepsContext.toReversed(),
|
||||
];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user