Visualize workflow run step input (#10677)
- Compute the context the selected step had access to during its execution and display it with the `<JsonNestedNode />` component - Ensure several steps with the same name can be displayed in order - Prevent access to the input tab in a few cases - Hide the input tab when the trigger node is selected as this node takes no input - Hide the input tab when the selected node has not been executed yet or is currently executed - Fallback to the Node tab when the Input tab can't be accessed ## Successful workflow execution https://github.com/user-attachments/assets/4a2bb5f5-450c-46ed-b2d7-a14d3b1e5c1f ## Failed workflow execution https://github.com/user-attachments/assets/3be2784e-e76c-48ab-aef5-17f63410898e Closes https://github.com/twentyhq/core-team-issues/issues/433
This commit is contained in:
committed by
GitHub
parent
9d78dc322d
commit
cb5f4820d7
@ -2,10 +2,16 @@ import { ShowPageSubContainerTabListContainer } from '@/ui/layout/show-page/comp
|
||||
import { SingleTabProps, TabList } from '@/ui/layout/tab/components/TabList';
|
||||
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
||||
import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow';
|
||||
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
|
||||
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
|
||||
import { useWorkflowSelectedNodeOrThrow } from '@/workflow/workflow-diagram/hooks/useWorkflowSelectedNodeOrThrow';
|
||||
import { WorkflowRunStepInputDetail } from '@/workflow/workflow-steps/components/WorkflowRunStepInputDetail';
|
||||
import { WorkflowStepDetail } from '@/workflow/workflow-steps/components/WorkflowStepDetail';
|
||||
import { WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/constants/WorkflowRunStepSidePanelTabListComponentId';
|
||||
import { getWorkflowRunStepExecutionStatus } from '@/workflow/workflow-steps/utils/getWorkflowRunStepExecutionStatus';
|
||||
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
||||
import styled from '@emotion/styled';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { IconLogin2, IconLogout, IconStepInto } from 'twenty-ui';
|
||||
|
||||
const StyledTabListContainer = styled(ShowPageSubContainerTabListContainer)`
|
||||
@ -17,17 +23,41 @@ type TabId = 'node' | 'input' | 'output';
|
||||
export const RightDrawerWorkflowRunViewStep = () => {
|
||||
const flow = useFlowOrThrow();
|
||||
const workflowSelectedNode = useWorkflowSelectedNodeOrThrow();
|
||||
const workflowRunId = useWorkflowRunIdOrThrow();
|
||||
|
||||
const workflowRun = useWorkflowRun({ workflowRunId });
|
||||
|
||||
const { activeTabId } = useTabList<TabId>(
|
||||
WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID,
|
||||
);
|
||||
|
||||
const stepExecutionStatus = isDefined(workflowRun)
|
||||
? getWorkflowRunStepExecutionStatus({
|
||||
workflowRunOutput: workflowRun.output,
|
||||
stepId: workflowSelectedNode,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const isInputTabDisabled =
|
||||
workflowSelectedNode === TRIGGER_STEP_ID ||
|
||||
stepExecutionStatus === 'running' ||
|
||||
stepExecutionStatus === 'not-executed';
|
||||
|
||||
const tabs: SingleTabProps<TabId>[] = [
|
||||
{ id: 'node', title: 'Node', Icon: IconStepInto },
|
||||
{ id: 'input', title: 'Input', Icon: IconLogin2 },
|
||||
{
|
||||
id: 'input',
|
||||
title: 'Input',
|
||||
Icon: IconLogin2,
|
||||
disabled: isInputTabDisabled,
|
||||
},
|
||||
{ id: 'output', title: 'Output', Icon: IconLogout },
|
||||
];
|
||||
|
||||
if (!isDefined(workflowRun)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledTabListContainer>
|
||||
@ -46,6 +76,10 @@ export const RightDrawerWorkflowRunViewStep = () => {
|
||||
steps={flow.steps}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{activeTabId === 'input' ? (
|
||||
<WorkflowRunStepInputDetail stepId={workflowSelectedNode} />
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,47 @@
|
||||
import { JsonNestedNode } from '@/workflow/components/json-visualizer/components/JsonNestedNode';
|
||||
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
|
||||
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
|
||||
import { getWorkflowRunStepContext } from '@/workflow/workflow-steps/utils/getWorkflowRunStepContext';
|
||||
import styled from '@emotion/styled';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { IconBrackets } from 'twenty-ui';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
padding-block: ${({ theme }) => theme.spacing(4)};
|
||||
padding-inline: ${({ theme }) => theme.spacing(3)};
|
||||
`;
|
||||
|
||||
export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
|
||||
const workflowRunId = useWorkflowRunIdOrThrow();
|
||||
const workflowRun = useWorkflowRun({ workflowRunId });
|
||||
|
||||
if (
|
||||
!(
|
||||
isDefined(workflowRun) &&
|
||||
isDefined(workflowRun.context) &&
|
||||
isDefined(workflowRun.output?.flow)
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stepContext = getWorkflowRunStepContext({
|
||||
context: workflowRun.context,
|
||||
flow: workflowRun.output.flow,
|
||||
stepId,
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<JsonNestedNode
|
||||
elements={stepContext.map(({ id, name, context }) => ({
|
||||
id,
|
||||
label: name,
|
||||
value: context,
|
||||
}))}
|
||||
Icon={IconBrackets}
|
||||
depth={0}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,142 @@
|
||||
import { flowState } from '@/workflow/states/flowState';
|
||||
import { workflowRunIdState } from '@/workflow/states/workflowRunIdState';
|
||||
import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState';
|
||||
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
||||
import styled from '@emotion/styled';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { expect, userEvent, waitFor, within } from '@storybook/test';
|
||||
import { graphql, HttpResponse } from 'msw';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { ComponentDecorator, RouterDecorator } from 'twenty-ui';
|
||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||
import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { oneFailedWorkflowRunQueryResult } from '~/testing/mock-data/workflow-run';
|
||||
import { RightDrawerWorkflowRunViewStep } from '../RightDrawerWorkflowRunViewStep';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 500px;
|
||||
`;
|
||||
|
||||
const meta: Meta<typeof RightDrawerWorkflowRunViewStep> = {
|
||||
title: 'Modules/Workflow/RightDrawerWorkflowRunViewStep',
|
||||
component: RightDrawerWorkflowRunViewStep,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<StyledWrapper>
|
||||
<Story />
|
||||
</StyledWrapper>
|
||||
),
|
||||
I18nFrontDecorator,
|
||||
ComponentDecorator,
|
||||
(Story) => {
|
||||
const setFlow = useSetRecoilState(flowState);
|
||||
const setWorkflowSelectedNode = useSetRecoilState(
|
||||
workflowSelectedNodeState,
|
||||
);
|
||||
const setWorkflowRunId = useSetRecoilState(workflowRunIdState);
|
||||
|
||||
setFlow(oneFailedWorkflowRunQueryResult.workflowRun.output.flow);
|
||||
setWorkflowSelectedNode(
|
||||
oneFailedWorkflowRunQueryResult.workflowRun.output.flow.steps[0].id,
|
||||
);
|
||||
setWorkflowRunId(oneFailedWorkflowRunQueryResult.workflowRun.id);
|
||||
|
||||
return <Story />;
|
||||
},
|
||||
RouterDecorator,
|
||||
ObjectMetadataItemsDecorator,
|
||||
WorkspaceDecorator,
|
||||
],
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [
|
||||
graphql.query('FindOneWorkflowRun', () => {
|
||||
return HttpResponse.json({
|
||||
data: oneFailedWorkflowRunQueryResult,
|
||||
});
|
||||
}),
|
||||
...graphqlMocks.handlers,
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof RightDrawerWorkflowRunViewStep>;
|
||||
|
||||
export const NodeTab: Story = {};
|
||||
|
||||
export const InputTab: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await userEvent.click(await canvas.findByRole('button', { name: 'Input' }));
|
||||
|
||||
expect(await canvas.findByText('Trigger')).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const InputTabDisabledForTrigger: Story = {
|
||||
decorators: [
|
||||
(Story) => {
|
||||
const setWorkflowSelectedNode = useSetRecoilState(
|
||||
workflowSelectedNodeState,
|
||||
);
|
||||
|
||||
setWorkflowSelectedNode(TRIGGER_STEP_ID);
|
||||
|
||||
return <Story />;
|
||||
},
|
||||
],
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const inputTab = await canvas.findByRole('button', { name: 'Input' });
|
||||
|
||||
expect(inputTab).toBeDisabled();
|
||||
},
|
||||
};
|
||||
|
||||
export const InputTabNotExecutedStep: Story = {
|
||||
decorators: [
|
||||
(Story) => {
|
||||
const setWorkflowSelectedNode = useSetRecoilState(
|
||||
workflowSelectedNodeState,
|
||||
);
|
||||
|
||||
setWorkflowSelectedNode(
|
||||
oneFailedWorkflowRunQueryResult.workflowRun.output.flow.steps.at(-1)!
|
||||
.id,
|
||||
);
|
||||
|
||||
return <Story />;
|
||||
},
|
||||
],
|
||||
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const inputTab = await canvas.findByRole('button', { name: 'Input' });
|
||||
|
||||
expect(inputTab).toBeDisabled();
|
||||
},
|
||||
};
|
||||
|
||||
export const OutputTab: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await userEvent.click(
|
||||
await canvas.findByRole('button', { name: 'Output' }),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(canvas.queryByText('Create Record')).not.toBeInTheDocument();
|
||||
});
|
||||
},
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export type WorkflowRunTabId = 'node' | 'input' | 'output';
|
||||
@ -0,0 +1,266 @@
|
||||
import { WorkflowRunFlow } from '@/workflow/types/Workflow';
|
||||
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
||||
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: {},
|
||||
},
|
||||
},
|
||||
steps: [],
|
||||
} satisfies WorkflowRunFlow;
|
||||
const context = {
|
||||
[TRIGGER_STEP_ID]: { company: { id: '123' } },
|
||||
};
|
||||
|
||||
const result = getWorkflowRunStepContext({
|
||||
stepId: TRIGGER_STEP_ID,
|
||||
flow,
|
||||
context,
|
||||
});
|
||||
|
||||
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,
|
||||
},
|
||||
{
|
||||
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 context = {
|
||||
[TRIGGER_STEP_ID]: { company: { id: '123' } },
|
||||
step1: { taskId: '456' },
|
||||
};
|
||||
|
||||
const result = getWorkflowRunStepContext({
|
||||
stepId: 'step2',
|
||||
flow,
|
||||
context,
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: TRIGGER_STEP_ID,
|
||||
name: 'Company Created',
|
||||
context: { company: { id: '123' } },
|
||||
},
|
||||
{
|
||||
id: 'step1',
|
||||
name: 'Create company',
|
||||
context: { taskId: '456' },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
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 context = {
|
||||
[TRIGGER_STEP_ID]: { company: { id: '123' } },
|
||||
step1: { taskId: '456' },
|
||||
step2: { emailId: '789' },
|
||||
};
|
||||
|
||||
const result = getWorkflowRunStepContext({
|
||||
stepId: 'step1',
|
||||
flow,
|
||||
context,
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: TRIGGER_STEP_ID,
|
||||
name: 'Company Created',
|
||||
context: { company: { id: '123' } },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle multiple steps with the same name', () => {
|
||||
const flow = {
|
||||
trigger: {
|
||||
name: 'Company Created',
|
||||
type: 'DATABASE_EVENT',
|
||||
settings: {
|
||||
eventName: 'company.created',
|
||||
outputSchema: {},
|
||||
},
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
id: 'step1',
|
||||
name: 'Create Note',
|
||||
type: 'CREATE_RECORD',
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
input: {
|
||||
objectName: 'Note',
|
||||
objectRecord: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
id: 'step2',
|
||||
name: 'Create Note',
|
||||
type: 'CREATE_RECORD',
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
input: {
|
||||
objectName: 'Note',
|
||||
objectRecord: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
id: 'step3',
|
||||
name: 'Create Note',
|
||||
type: 'CREATE_RECORD',
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
input: {
|
||||
objectName: 'Note',
|
||||
objectRecord: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
valid: true,
|
||||
},
|
||||
],
|
||||
} satisfies WorkflowRunFlow;
|
||||
const context = {
|
||||
[TRIGGER_STEP_ID]: { company: { id: '123' } },
|
||||
step1: { noteId: '456' },
|
||||
step2: { noteId: '789' },
|
||||
step3: { noteId: '101' },
|
||||
};
|
||||
|
||||
const result = getWorkflowRunStepContext({
|
||||
stepId: 'step3',
|
||||
flow,
|
||||
context,
|
||||
});
|
||||
|
||||
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' },
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,230 @@
|
||||
import { getWorkflowRunStepExecutionStatus } from '../getWorkflowRunStepExecutionStatus';
|
||||
|
||||
describe('getWorkflowRunStepExecutionStatus', () => {
|
||||
const stepId = '453e0084-aca2-45b9-8d1c-458a2b8ac70a';
|
||||
|
||||
it('should return not-executed when the output is null', () => {
|
||||
expect(
|
||||
getWorkflowRunStepExecutionStatus({
|
||||
workflowRunOutput: null,
|
||||
stepId,
|
||||
}),
|
||||
).toBe('not-executed');
|
||||
});
|
||||
|
||||
it('should return success when step has result', () => {
|
||||
expect(
|
||||
getWorkflowRunStepExecutionStatus({
|
||||
workflowRunOutput: {
|
||||
flow: {
|
||||
steps: [
|
||||
{
|
||||
id: stepId,
|
||||
name: 'Code - Serverless Function',
|
||||
type: 'CODE',
|
||||
valid: false,
|
||||
settings: {
|
||||
input: {
|
||||
serverlessFunctionId:
|
||||
'5f7b9b44-bb07-41ba-aef8-ec0eaa5eea2c',
|
||||
serverlessFunctionInput: {
|
||||
a: null,
|
||||
b: null,
|
||||
},
|
||||
serverlessFunctionVersion: 'draft',
|
||||
},
|
||||
outputSchema: {
|
||||
link: {
|
||||
tab: 'test',
|
||||
icon: 'IconVariable',
|
||||
label: 'Generate Function Output',
|
||||
isLeaf: true,
|
||||
},
|
||||
_outputSchemaType: 'LINK',
|
||||
},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
trigger: {
|
||||
type: 'MANUAL',
|
||||
settings: {
|
||||
outputSchema: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
stepsOutput: {
|
||||
[stepId]: {
|
||||
result: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
stepId,
|
||||
}),
|
||||
).toBe('success');
|
||||
});
|
||||
|
||||
it('should return failure when workflow has error', () => {
|
||||
const error = 'fn(...).then is not a function';
|
||||
|
||||
expect(
|
||||
getWorkflowRunStepExecutionStatus({
|
||||
workflowRunOutput: {
|
||||
flow: {
|
||||
steps: [
|
||||
{
|
||||
id: stepId,
|
||||
name: 'Code - Serverless Function',
|
||||
type: 'CODE',
|
||||
valid: false,
|
||||
settings: {
|
||||
input: {
|
||||
serverlessFunctionId:
|
||||
'5f7b9b44-bb07-41ba-aef8-ec0eaa5eea2c',
|
||||
serverlessFunctionInput: {
|
||||
a: null,
|
||||
b: null,
|
||||
},
|
||||
serverlessFunctionVersion: 'draft',
|
||||
},
|
||||
outputSchema: {
|
||||
link: {
|
||||
tab: 'test',
|
||||
icon: 'IconVariable',
|
||||
label: 'Generate Function Output',
|
||||
isLeaf: true,
|
||||
},
|
||||
_outputSchemaType: 'LINK',
|
||||
},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
trigger: {
|
||||
type: 'MANUAL',
|
||||
settings: {
|
||||
outputSchema: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
error,
|
||||
stepsOutput: {
|
||||
[stepId]: {
|
||||
error,
|
||||
},
|
||||
},
|
||||
},
|
||||
stepId,
|
||||
}),
|
||||
).toBe('failure');
|
||||
});
|
||||
|
||||
it('should return not-executed when step has no output', () => {
|
||||
const secondStepId = '5f7b9b44-bb07-41ba-aef8-ec0eaa5eea2c';
|
||||
|
||||
expect(
|
||||
getWorkflowRunStepExecutionStatus({
|
||||
workflowRunOutput: {
|
||||
flow: {
|
||||
steps: [
|
||||
{
|
||||
id: stepId,
|
||||
name: 'Code - Serverless Function',
|
||||
type: 'CODE',
|
||||
valid: false,
|
||||
settings: {
|
||||
input: {
|
||||
serverlessFunctionId:
|
||||
'5f7b9b44-bb07-41ba-aef8-ec0eaa5eea2c',
|
||||
serverlessFunctionInput: {
|
||||
a: null,
|
||||
b: null,
|
||||
},
|
||||
serverlessFunctionVersion: 'draft',
|
||||
},
|
||||
outputSchema: {
|
||||
link: {
|
||||
tab: 'test',
|
||||
icon: 'IconVariable',
|
||||
label: 'Generate Function Output',
|
||||
isLeaf: true,
|
||||
},
|
||||
_outputSchemaType: 'LINK',
|
||||
},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: secondStepId,
|
||||
name: 'Code - Serverless Function',
|
||||
type: 'CODE',
|
||||
valid: false,
|
||||
settings: {
|
||||
input: {
|
||||
serverlessFunctionId:
|
||||
'5f7b9b44-bb07-41ba-aef8-ec0eaa5eea2c',
|
||||
serverlessFunctionInput: {
|
||||
a: null,
|
||||
b: null,
|
||||
},
|
||||
serverlessFunctionVersion: 'draft',
|
||||
},
|
||||
outputSchema: {
|
||||
link: {
|
||||
tab: 'test',
|
||||
icon: 'IconVariable',
|
||||
label: 'Generate Function Output',
|
||||
isLeaf: true,
|
||||
},
|
||||
_outputSchemaType: 'LINK',
|
||||
},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
trigger: {
|
||||
type: 'MANUAL',
|
||||
settings: {
|
||||
outputSchema: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
stepsOutput: {
|
||||
[stepId]: {
|
||||
result: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
stepId: secondStepId,
|
||||
}),
|
||||
).toBe('not-executed');
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,38 @@
|
||||
import { WorkflowRunContext, WorkflowRunFlow } from '@/workflow/types/Workflow';
|
||||
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
||||
|
||||
export const getWorkflowRunStepContext = ({
|
||||
stepId,
|
||||
flow,
|
||||
context,
|
||||
}: {
|
||||
stepId: string;
|
||||
context: WorkflowRunContext;
|
||||
flow: WorkflowRunFlow;
|
||||
}) => {
|
||||
const stepContext: Array<{ id: string; name: string; context: any }> = [];
|
||||
|
||||
if (stepId === TRIGGER_STEP_ID) {
|
||||
return stepContext;
|
||||
}
|
||||
|
||||
stepContext.push({
|
||||
id: TRIGGER_STEP_ID,
|
||||
name: flow.trigger.name ?? 'Trigger',
|
||||
context: context[TRIGGER_STEP_ID],
|
||||
});
|
||||
|
||||
for (const step of flow.steps) {
|
||||
if (step.id === stepId) {
|
||||
break;
|
||||
}
|
||||
|
||||
stepContext.push({
|
||||
id: step.id,
|
||||
name: step.name,
|
||||
context: context[step.id],
|
||||
});
|
||||
}
|
||||
|
||||
return stepContext;
|
||||
};
|
||||
@ -0,0 +1,28 @@
|
||||
import { WorkflowRunOutput } from '@/workflow/types/Workflow';
|
||||
import { WorkflowDiagramRunStatus } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { isNull } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
export const getWorkflowRunStepExecutionStatus = ({
|
||||
workflowRunOutput,
|
||||
stepId,
|
||||
}: {
|
||||
workflowRunOutput: WorkflowRunOutput | null;
|
||||
stepId: string;
|
||||
}): WorkflowDiagramRunStatus => {
|
||||
if (isNull(workflowRunOutput)) {
|
||||
return 'not-executed';
|
||||
}
|
||||
|
||||
const stepOutput = workflowRunOutput.stepsOutput?.[stepId];
|
||||
|
||||
if (isDefined(stepOutput?.error)) {
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
if (isDefined(stepOutput?.result)) {
|
||||
return 'success';
|
||||
}
|
||||
|
||||
return 'not-executed';
|
||||
};
|
||||
Reference in New Issue
Block a user