22 branches 3 (#13181)
This PR does not produce any functional changes for our users. It prepares the branches for workflows by: - decommissioning `output` and `context` fields or `workflowRun` records and use newly created `state` field from front-end and back-end - use `stepStatus` computed by `back-end` in `front-end` - add utils and types in `twenty-shared/workflow` (not completed, a follow-up is scheduled https://github.com/twentyhq/core-team-issues/issues/1211) - add concurrency to `workflowQueue` message queue to avoid weird branch execution when using forms in workflow branches - add a WithLock decorator for better dev experience of `CacheLockService.withLock` usage Here is an example of such a workflow running (front branch display is not yet done that's why it looks ugly) -> https://discord.com/channels/1130383047699738754/1258024460238192691/1392897615171158098
This commit is contained in:
@ -4,7 +4,6 @@ import { commandMenuWorkflowRunIdComponentState } from '@/command-menu/pages/wor
|
||||
import { commandMenuWorkflowVersionIdComponentState } from '@/command-menu/pages/workflow/states/commandMenuWorkflowVersionIdComponentState';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { useSetInitialWorkflowRunRightDrawerTab } from '@/workflow/workflow-diagram/hooks/useSetInitialWorkflowRunRightDrawerTab';
|
||||
import { WorkflowDiagramRunStatus } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import {
|
||||
@ -13,6 +12,7 @@ import {
|
||||
IconSettingsAutomation,
|
||||
} from 'twenty-ui/display';
|
||||
import { v4 } from 'uuid';
|
||||
import { WorkflowRunStepStatus } from '@/workflow/types/Workflow';
|
||||
|
||||
export const useWorkflowCommandMenu = () => {
|
||||
const { navigateCommandMenu } = useNavigateCommandMenu();
|
||||
@ -142,7 +142,7 @@ export const useWorkflowCommandMenu = () => {
|
||||
title: string;
|
||||
icon: IconComponent;
|
||||
workflowSelectedNode: string;
|
||||
stepExecutionStatus: WorkflowDiagramRunStatus;
|
||||
stepExecutionStatus: WorkflowRunStepStatus;
|
||||
}) => {
|
||||
const pageId = v4();
|
||||
|
||||
|
||||
@ -20,11 +20,11 @@ import {
|
||||
WorkflowRunTabId,
|
||||
WorkflowRunTabIdType,
|
||||
} from '@/workflow/workflow-steps/types/WorkflowRunTabId';
|
||||
import { getWorkflowRunStepExecutionStatus } from '@/workflow/workflow-steps/utils/getWorkflowRunStepExecutionStatus';
|
||||
import styled from '@emotion/styled';
|
||||
import { isNull } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconLogin2, IconLogout, IconStepInto } from 'twenty-ui/display';
|
||||
import { getWorkflowRunStepExecutionStatus } from '@/workflow/workflow-steps/utils/getWorkflowRunStepExecutionStatus';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
@ -65,9 +65,10 @@ export const CommandMenuWorkflowRunViewStepContent = () => {
|
||||
}
|
||||
|
||||
const stepExecutionStatus = getWorkflowRunStepExecutionStatus({
|
||||
workflowRunOutput: workflowRun.output,
|
||||
workflowRunState: workflowRun.state,
|
||||
stepId: workflowSelectedNode,
|
||||
});
|
||||
|
||||
const stepDefinition = getStepDefinitionOrThrow({
|
||||
stepId: workflowSelectedNode,
|
||||
trigger: flow.trigger,
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
import { WorkflowDiagramRunStatus } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
||||
import { WorkflowRunStepStatus } from '@/workflow/types/Workflow';
|
||||
|
||||
export const getIsInputTabDisabled = ({
|
||||
stepExecutionStatus,
|
||||
workflowSelectedNode,
|
||||
}: {
|
||||
workflowSelectedNode: string;
|
||||
stepExecutionStatus: WorkflowDiagramRunStatus;
|
||||
stepExecutionStatus: WorkflowRunStepStatus;
|
||||
}) => {
|
||||
return (
|
||||
workflowSelectedNode === TRIGGER_STEP_ID ||
|
||||
stepExecutionStatus === 'not-executed'
|
||||
stepExecutionStatus === 'NOT_STARTED'
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { WorkflowDiagramRunStatus } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { WorkflowRunStepStatus } from '@/workflow/types/Workflow';
|
||||
|
||||
export const getIsOutputTabDisabled = ({
|
||||
stepExecutionStatus,
|
||||
}: {
|
||||
stepExecutionStatus: WorkflowDiagramRunStatus;
|
||||
stepExecutionStatus: WorkflowRunStepStatus;
|
||||
}) => {
|
||||
return (
|
||||
stepExecutionStatus === 'running' || stepExecutionStatus === 'not-executed'
|
||||
stepExecutionStatus === 'RUNNING' || stepExecutionStatus === 'NOT_STARTED'
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
import { WorkflowActionType } from '@/workflow/types/Workflow';
|
||||
import { WorkflowDiagramRunStatus } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import {
|
||||
WorkflowActionType,
|
||||
WorkflowRunStepStatus,
|
||||
} from '@/workflow/types/Workflow';
|
||||
|
||||
export const getShouldFocusNodeTab = ({
|
||||
stepExecutionStatus,
|
||||
actionType,
|
||||
}: {
|
||||
stepExecutionStatus: WorkflowDiagramRunStatus;
|
||||
stepExecutionStatus: WorkflowRunStepStatus;
|
||||
actionType: WorkflowActionType | undefined;
|
||||
}) => {
|
||||
return actionType === 'FORM' && stepExecutionStatus === 'running';
|
||||
return actionType === 'FORM' && stepExecutionStatus === 'PENDING';
|
||||
};
|
||||
|
||||
@ -8,7 +8,7 @@ import { TimelineActivities } from '@/activities/timeline-activities/components/
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { FieldsCard } from '@/object-record/record-show/components/FieldsCard';
|
||||
import { CardType } from '@/object-record/record-show/types/CardType';
|
||||
import { ListenRecordUpdatesEffect } from '@/subscription/components/ListenUpdatesEffect';
|
||||
import { ListenRecordUpdatesEffect } from '@/subscription/components/ListenRecordUpdatesEffect';
|
||||
import { ShowPageActivityContainer } from '@/ui/layout/show-page/components/ShowPageActivityContainer';
|
||||
import { getWorkflowVisualizerComponentInstanceId } from '@/workflow/utils/getWorkflowVisualizerComponentInstanceId';
|
||||
import { WorkflowRunVisualizerEffect } from '@/workflow/workflow-diagram/components/WorkflowRunVisualizerEffect';
|
||||
@ -192,7 +192,7 @@ export const CardComponents: Record<CardType, CardComponentType> = {
|
||||
<ListenRecordUpdatesEffect
|
||||
objectNameSingular={targetableObject.targetObjectNameSingular}
|
||||
recordId={targetableObject.id}
|
||||
listenedFields={['status', 'output']}
|
||||
listenedFields={['status', 'state']}
|
||||
/>
|
||||
<Suspense fallback={<LoadingSkeleton />}>
|
||||
<WorkflowRunVisualizer workflowRunId={targetableObject.id} />
|
||||
|
||||
@ -8,7 +8,7 @@ import { RecordTableRowArrowKeysEffect } from '@/object-record/record-table/reco
|
||||
import { RecordTableRowHotkeyEffect } from '@/object-record/record-table/record-table-row/components/RecordTableRowHotkeyEffect';
|
||||
import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState';
|
||||
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
|
||||
import { ListenRecordUpdatesEffect } from '@/subscription/components/ListenUpdatesEffect';
|
||||
import { ListenRecordUpdatesEffect } from '@/subscription/components/ListenRecordUpdatesEffect';
|
||||
import { getDefaultRecordFieldsToListen } from '@/subscription/utils/getDefaultRecordFieldsToListen.util';
|
||||
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
|
||||
@ -47,15 +47,15 @@ export const useRunWorkflowRunOpeningInCommandMenuSideEffects = () => {
|
||||
objectPermissionsByObjectMetadataId,
|
||||
});
|
||||
if (
|
||||
!(isDefined(workflowRunRecord) && isDefined(workflowRunRecord.output))
|
||||
!(isDefined(workflowRunRecord) && isDefined(workflowRunRecord.state))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { stepToOpenByDefault } = generateWorkflowRunDiagram({
|
||||
steps: workflowRunRecord.output.flow.steps,
|
||||
stepsOutput: workflowRunRecord.output.stepsOutput,
|
||||
trigger: workflowRunRecord.output.flow.trigger,
|
||||
steps: workflowRunRecord.state.flow.steps,
|
||||
stepInfos: workflowRunRecord.state.stepInfos,
|
||||
trigger: workflowRunRecord.state.flow.trigger,
|
||||
});
|
||||
|
||||
if (!isDefined(stepToOpenByDefault)) {
|
||||
@ -86,8 +86,8 @@ export const useRunWorkflowRunOpeningInCommandMenuSideEffects = () => {
|
||||
}),
|
||||
{
|
||||
workflowVersionId: workflowRunRecord.workflowVersionId,
|
||||
trigger: workflowRunRecord.output.flow.trigger,
|
||||
steps: workflowRunRecord.output.flow.steps,
|
||||
trigger: workflowRunRecord.state.flow.trigger,
|
||||
steps: workflowRunRecord.state.flow.steps,
|
||||
},
|
||||
);
|
||||
set(
|
||||
|
||||
@ -18,11 +18,12 @@ import {
|
||||
workflowFormActionSettingsSchema,
|
||||
workflowHttpRequestActionSchema,
|
||||
workflowManualTriggerSchema,
|
||||
workflowRunContextSchema,
|
||||
workflowRunOutputSchema,
|
||||
workflowRunOutputStepsOutputSchema,
|
||||
workflowRunSchema,
|
||||
workflowRunStateSchema,
|
||||
workflowRunStatusSchema,
|
||||
workflowRunStepStatusSchema,
|
||||
workflowSendEmailActionSchema,
|
||||
workflowSendEmailActionSettingsSchema,
|
||||
workflowTriggerSchema,
|
||||
@ -150,14 +151,16 @@ export type WorkflowRunOutputStepsOutput = z.infer<
|
||||
typeof workflowRunOutputStepsOutputSchema
|
||||
>;
|
||||
|
||||
export type WorkflowRunContext = z.infer<typeof workflowRunContextSchema>;
|
||||
|
||||
export type WorkflowRunFlow = WorkflowRunOutput['flow'];
|
||||
|
||||
export type WorkflowRunStatus = z.infer<typeof workflowRunStatusSchema>;
|
||||
|
||||
export type WorkflowRun = z.infer<typeof workflowRunSchema>;
|
||||
|
||||
export type WorkflowRunState = z.infer<typeof workflowRunStateSchema>;
|
||||
|
||||
export type WorkflowRunStepStatus = z.infer<typeof workflowRunStepStatusSchema>;
|
||||
|
||||
export type WorkflowRunFlow = WorkflowRunState['flow'];
|
||||
|
||||
export type Workflow = {
|
||||
__typename: 'Workflow';
|
||||
id: string;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { z } from 'zod';
|
||||
import { StepStatus } from 'twenty-shared/workflow';
|
||||
|
||||
// Base schemas
|
||||
export const objectRecordSchema = z.record(z.any());
|
||||
@ -317,6 +318,27 @@ export const workflowRunOutputSchema = z.object({
|
||||
error: z.any().optional(),
|
||||
});
|
||||
|
||||
export const workflowRunStepStatusSchema = z.nativeEnum(StepStatus);
|
||||
|
||||
export const workflowRunStateStepInfoSchema = z.object({
|
||||
result: z.any().optional(),
|
||||
error: z.any().optional(),
|
||||
status: workflowRunStepStatusSchema,
|
||||
});
|
||||
|
||||
export const workflowRunStateStepInfosSchema = z.record(
|
||||
workflowRunStateStepInfoSchema,
|
||||
);
|
||||
|
||||
export const workflowRunStateSchema = z.object({
|
||||
flow: z.object({
|
||||
trigger: workflowTriggerSchema,
|
||||
steps: z.array(workflowActionSchema),
|
||||
}),
|
||||
stepInfos: workflowRunStateStepInfosSchema,
|
||||
workflowRunError: z.any().optional(),
|
||||
});
|
||||
|
||||
export const workflowRunContextSchema = z.record(z.any());
|
||||
|
||||
export const workflowRunStatusSchema = z.enum([
|
||||
@ -335,6 +357,7 @@ export const workflowRunSchema = z
|
||||
workflowId: z.string(),
|
||||
output: workflowRunOutputSchema.nullable(),
|
||||
context: workflowRunContextSchema.nullable(),
|
||||
state: workflowRunStateSchema.nullable(),
|
||||
status: workflowRunStatusSchema,
|
||||
createdAt: z.string(),
|
||||
deletedAt: z.string().nullable(),
|
||||
|
||||
@ -1,27 +1,7 @@
|
||||
import { WorkflowDiagramStepNodeBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase';
|
||||
import { WorkflowDiagramStepNodeIcon } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeIcon';
|
||||
import {
|
||||
WorkflowDiagramRunStatus,
|
||||
WorkflowDiagramStepNodeData,
|
||||
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { WorkflowDiagramNodeVariant } from '@/workflow/workflow-diagram/types/WorkflowDiagramNodeVariant';
|
||||
|
||||
const getNodeVariantFromRunStatus = (
|
||||
runStatus: WorkflowDiagramRunStatus | undefined,
|
||||
): WorkflowDiagramNodeVariant => {
|
||||
switch (runStatus) {
|
||||
case 'success':
|
||||
return 'success';
|
||||
case 'failure':
|
||||
return 'failure';
|
||||
case 'running':
|
||||
return 'running';
|
||||
case 'not-executed':
|
||||
return 'not-executed';
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
};
|
||||
import { WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { getNodeVariantFromStepRunStatus } from '@/workflow/workflow-diagram/utils/getNodeVariantFromStepRunStatus';
|
||||
|
||||
export const WorkflowDiagramStepNodeReadonly = ({
|
||||
data,
|
||||
@ -31,7 +11,7 @@ export const WorkflowDiagramStepNodeReadonly = ({
|
||||
return (
|
||||
<WorkflowDiagramStepNodeBase
|
||||
name={data.name}
|
||||
variant={getNodeVariantFromRunStatus(data.runStatus)}
|
||||
variant={getNodeVariantFromStepRunStatus(data.runStatus)}
|
||||
nodeType={data.nodeType}
|
||||
Icon={<WorkflowDiagramStepNodeIcon data={data} />}
|
||||
/>
|
||||
|
||||
@ -9,7 +9,7 @@ import { useWorkflowVersion } from '@/workflow/hooks/useWorkflowVersion';
|
||||
import { flowComponentState } from '@/workflow/states/flowComponentState';
|
||||
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
|
||||
import { workflowVisualizerWorkflowRunIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowRunIdComponentState';
|
||||
import { WorkflowRunOutput } from '@/workflow/types/Workflow';
|
||||
import { WorkflowRunState } from '@/workflow/types/Workflow';
|
||||
import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState';
|
||||
import { workflowDiagramStatusComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramStatusComponentState';
|
||||
import { workflowRunDiagramAutomaticallyOpenedStepsComponentState } from '@/workflow/workflow-diagram/states/workflowRunDiagramAutomaticallyOpenedStepsComponentState';
|
||||
@ -82,15 +82,15 @@ export const WorkflowRunVisualizerEffect = ({
|
||||
const handleWorkflowRunDiagramGeneration = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
({
|
||||
workflowRunOutput,
|
||||
workflowRunState,
|
||||
workflowVersionId,
|
||||
isInRightDrawer,
|
||||
}: {
|
||||
workflowRunOutput: WorkflowRunOutput | undefined;
|
||||
workflowRunState: WorkflowRunState | undefined;
|
||||
workflowVersionId: string | undefined;
|
||||
isInRightDrawer: boolean;
|
||||
}) => {
|
||||
if (!(isDefined(workflowRunOutput) && isDefined(workflowVersionId))) {
|
||||
if (!(isDefined(workflowRunState) && isDefined(workflowVersionId))) {
|
||||
set(flowState, undefined);
|
||||
set(workflowDiagramState, undefined);
|
||||
|
||||
@ -108,15 +108,15 @@ export const WorkflowRunVisualizerEffect = ({
|
||||
|
||||
set(flowState, {
|
||||
workflowVersionId,
|
||||
trigger: workflowRunOutput.flow.trigger,
|
||||
steps: workflowRunOutput.flow.steps,
|
||||
trigger: workflowRunState.flow.trigger,
|
||||
steps: workflowRunState.flow.steps,
|
||||
});
|
||||
|
||||
const { diagram: baseWorkflowRunDiagram, stepToOpenByDefault } =
|
||||
generateWorkflowRunDiagram({
|
||||
trigger: workflowRunOutput.flow.trigger,
|
||||
steps: workflowRunOutput.flow.steps,
|
||||
stepsOutput: workflowRunOutput.stepsOutput,
|
||||
trigger: workflowRunState.flow.trigger,
|
||||
steps: workflowRunState.flow.steps,
|
||||
stepInfos: workflowRunState.stepInfos,
|
||||
});
|
||||
|
||||
if (isDefined(stepToOpenByDefault)) {
|
||||
@ -197,14 +197,14 @@ export const WorkflowRunVisualizerEffect = ({
|
||||
|
||||
useEffect(() => {
|
||||
handleWorkflowRunDiagramGeneration({
|
||||
workflowRunOutput: workflowRun?.output ?? undefined,
|
||||
workflowRunState: workflowRun?.state ?? undefined,
|
||||
workflowVersionId: workflowRun?.workflowVersionId,
|
||||
isInRightDrawer,
|
||||
});
|
||||
}, [
|
||||
handleWorkflowRunDiagramGeneration,
|
||||
isInRightDrawer,
|
||||
workflowRun?.output,
|
||||
workflowRun?.state,
|
||||
workflowRun?.workflowVersionId,
|
||||
]);
|
||||
|
||||
|
||||
@ -3,10 +3,10 @@ import { getIsOutputTabDisabled } from '@/command-menu/pages/workflow/step/view-
|
||||
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
import { WorkflowDiagramRunStatus } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { WorkflowRunTabId } from '@/workflow/workflow-steps/types/WorkflowRunTabId';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { WorkflowRunStepStatus } from '@/workflow/types/Workflow';
|
||||
|
||||
export const useSetInitialWorkflowRunRightDrawerTab = () => {
|
||||
const setInitialWorkflowRunRightDrawerTab = useRecoilCallback(
|
||||
@ -16,7 +16,7 @@ export const useSetInitialWorkflowRunRightDrawerTab = () => {
|
||||
stepExecutionStatus,
|
||||
}: {
|
||||
workflowSelectedNode: string;
|
||||
stepExecutionStatus: WorkflowDiagramRunStatus;
|
||||
stepExecutionStatus: WorkflowRunStepStatus;
|
||||
}) => {
|
||||
const commandMenuPageInfo = getSnapshotValue(
|
||||
snapshot,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import {
|
||||
WorkflowActionType,
|
||||
WorkflowRunStepStatus,
|
||||
WorkflowTriggerType,
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { Edge, Node } from '@xyflow/react';
|
||||
@ -21,32 +22,26 @@ export type WorkflowDiagram = {
|
||||
edges: Array<WorkflowDiagramEdge>;
|
||||
};
|
||||
|
||||
export type WorkflowDiagramRunStatus =
|
||||
| 'running'
|
||||
| 'success'
|
||||
| 'failure'
|
||||
| 'not-executed';
|
||||
|
||||
export type WorkflowDiagramStepNodeData =
|
||||
| {
|
||||
nodeType: 'trigger';
|
||||
triggerType: WorkflowTriggerType;
|
||||
name: string;
|
||||
icon?: string;
|
||||
runStatus?: WorkflowDiagramRunStatus;
|
||||
runStatus?: WorkflowRunStepStatus;
|
||||
}
|
||||
| {
|
||||
nodeType: 'action';
|
||||
actionType: WorkflowActionType;
|
||||
name: string;
|
||||
runStatus?: WorkflowDiagramRunStatus;
|
||||
runStatus?: WorkflowRunStepStatus;
|
||||
};
|
||||
|
||||
export type WorkflowRunDiagramStepNodeData = Exclude<
|
||||
WorkflowDiagramStepNodeData,
|
||||
'runStatus'
|
||||
> & {
|
||||
runStatus: WorkflowDiagramRunStatus;
|
||||
runStatus: WorkflowRunStepStatus;
|
||||
};
|
||||
|
||||
export type WorkflowDiagramCreateStepNodeData = {
|
||||
@ -66,7 +61,7 @@ export type WorkflowDiagramNodeData =
|
||||
export type WorkflowRunDiagramNodeData = Exclude<
|
||||
WorkflowDiagramStepNodeData,
|
||||
'runStatus'
|
||||
> & { runStatus: WorkflowDiagramRunStatus };
|
||||
> & { runStatus: WorkflowRunStepStatus };
|
||||
|
||||
export type EdgeData = {
|
||||
stepId?: string;
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
import {
|
||||
WorkflowRunOutputStepsOutput,
|
||||
WorkflowStep,
|
||||
WorkflowTrigger,
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { WorkflowStep, WorkflowTrigger } from '@/workflow/types/Workflow';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { getUuidV4Mock } from '~/testing/utils/getUuidV4Mock';
|
||||
import { generateWorkflowRunDiagram } from '../generateWorkflowRunDiagram';
|
||||
import { StepStatus, WorkflowRunStepInfos } from 'twenty-shared/workflow';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: getUuidV4Mock(),
|
||||
@ -82,14 +79,28 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const stepsOutput: WorkflowRunOutputStepsOutput = {
|
||||
const stepInfos: WorkflowRunStepInfos = {
|
||||
trigger: {
|
||||
result: {},
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
step1: {
|
||||
result: undefined,
|
||||
error: '',
|
||||
status: StepStatus.FAILED,
|
||||
},
|
||||
step2: {
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
step3: {
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
};
|
||||
|
||||
const result = generateWorkflowRunDiagram({ trigger, steps, stepsOutput });
|
||||
const result = generateWorkflowRunDiagram({
|
||||
trigger,
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
{
|
||||
@ -130,7 +141,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"icon": "IconPlaylistAdd",
|
||||
"name": "Company created",
|
||||
"nodeType": "trigger",
|
||||
"runStatus": "success",
|
||||
"runStatus": "SUCCESS",
|
||||
"triggerType": "DATABASE_EVENT",
|
||||
},
|
||||
"id": "trigger",
|
||||
@ -144,7 +155,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"actionType": "CODE",
|
||||
"name": "Step 1",
|
||||
"nodeType": "action",
|
||||
"runStatus": "failure",
|
||||
"runStatus": "FAILED",
|
||||
},
|
||||
"id": "step1",
|
||||
"position": {
|
||||
@ -157,7 +168,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"actionType": "CODE",
|
||||
"name": "Step 2",
|
||||
"nodeType": "action",
|
||||
"runStatus": "not-executed",
|
||||
"runStatus": "NOT_STARTED",
|
||||
},
|
||||
"id": "step2",
|
||||
"position": {
|
||||
@ -170,7 +181,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"actionType": "CODE",
|
||||
"name": "Step 3",
|
||||
"nodeType": "action",
|
||||
"runStatus": "not-executed",
|
||||
"runStatus": "NOT_STARTED",
|
||||
},
|
||||
"id": "step3",
|
||||
"position": {
|
||||
@ -255,22 +266,30 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const stepsOutput: WorkflowRunOutputStepsOutput = {
|
||||
const stepInfos: WorkflowRunStepInfos = {
|
||||
trigger: {
|
||||
result: {},
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
step1: {
|
||||
result: {},
|
||||
error: undefined,
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
step2: {
|
||||
result: {},
|
||||
error: undefined,
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
step3: {
|
||||
result: {},
|
||||
error: undefined,
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
};
|
||||
|
||||
const result = generateWorkflowRunDiagram({ trigger, steps, stepsOutput });
|
||||
const result = generateWorkflowRunDiagram({
|
||||
trigger,
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
{
|
||||
@ -313,7 +332,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"icon": "IconPlaylistAdd",
|
||||
"name": "Company created",
|
||||
"nodeType": "trigger",
|
||||
"runStatus": "success",
|
||||
"runStatus": "SUCCESS",
|
||||
"triggerType": "DATABASE_EVENT",
|
||||
},
|
||||
"id": "trigger",
|
||||
@ -327,7 +346,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"actionType": "CODE",
|
||||
"name": "Step 1",
|
||||
"nodeType": "action",
|
||||
"runStatus": "success",
|
||||
"runStatus": "SUCCESS",
|
||||
},
|
||||
"id": "step1",
|
||||
"position": {
|
||||
@ -340,7 +359,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"actionType": "CODE",
|
||||
"name": "Step 2",
|
||||
"nodeType": "action",
|
||||
"runStatus": "success",
|
||||
"runStatus": "SUCCESS",
|
||||
},
|
||||
"id": "step2",
|
||||
"position": {
|
||||
@ -353,7 +372,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"actionType": "CODE",
|
||||
"name": "Step 3",
|
||||
"nodeType": "action",
|
||||
"runStatus": "success",
|
||||
"runStatus": "SUCCESS",
|
||||
},
|
||||
"id": "step3",
|
||||
"position": {
|
||||
@ -438,9 +457,30 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const stepsOutput = undefined;
|
||||
const stepInfos: WorkflowRunStepInfos = {
|
||||
trigger: {
|
||||
result: {},
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
step1: {
|
||||
error: '',
|
||||
status: StepStatus.RUNNING,
|
||||
},
|
||||
step2: {
|
||||
error: '',
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
step3: {
|
||||
error: '',
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
};
|
||||
|
||||
const result = generateWorkflowRunDiagram({ trigger, steps, stepsOutput });
|
||||
const result = generateWorkflowRunDiagram({
|
||||
trigger,
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
{
|
||||
@ -481,7 +521,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"icon": "IconPlaylistAdd",
|
||||
"name": "Company created",
|
||||
"nodeType": "trigger",
|
||||
"runStatus": "success",
|
||||
"runStatus": "SUCCESS",
|
||||
"triggerType": "DATABASE_EVENT",
|
||||
},
|
||||
"id": "trigger",
|
||||
@ -495,7 +535,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"actionType": "CODE",
|
||||
"name": "Step 1",
|
||||
"nodeType": "action",
|
||||
"runStatus": "running",
|
||||
"runStatus": "RUNNING",
|
||||
},
|
||||
"id": "step1",
|
||||
"position": {
|
||||
@ -508,7 +548,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"actionType": "CODE",
|
||||
"name": "Step 2",
|
||||
"nodeType": "action",
|
||||
"runStatus": "not-executed",
|
||||
"runStatus": "NOT_STARTED",
|
||||
},
|
||||
"id": "step2",
|
||||
"position": {
|
||||
@ -521,7 +561,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"actionType": "CODE",
|
||||
"name": "Step 3",
|
||||
"nodeType": "action",
|
||||
"runStatus": "not-executed",
|
||||
"runStatus": "NOT_STARTED",
|
||||
},
|
||||
"id": "step3",
|
||||
"position": {
|
||||
@ -625,14 +665,30 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const stepsOutput: WorkflowRunOutputStepsOutput = {
|
||||
const stepInfos: WorkflowRunStepInfos = {
|
||||
trigger: {
|
||||
result: {},
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
step1: {
|
||||
result: {},
|
||||
error: undefined,
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
step2: {
|
||||
result: {},
|
||||
status: StepStatus.RUNNING,
|
||||
},
|
||||
step3: {
|
||||
result: {},
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
};
|
||||
|
||||
const result = generateWorkflowRunDiagram({ trigger, steps, stepsOutput });
|
||||
const result = generateWorkflowRunDiagram({
|
||||
trigger,
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
{
|
||||
@ -683,7 +739,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"icon": "IconPlaylistAdd",
|
||||
"name": "Company created",
|
||||
"nodeType": "trigger",
|
||||
"runStatus": "success",
|
||||
"runStatus": "SUCCESS",
|
||||
"triggerType": "DATABASE_EVENT",
|
||||
},
|
||||
"id": "trigger",
|
||||
@ -697,7 +753,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"actionType": "CODE",
|
||||
"name": "Step 1",
|
||||
"nodeType": "action",
|
||||
"runStatus": "success",
|
||||
"runStatus": "SUCCESS",
|
||||
},
|
||||
"id": "step1",
|
||||
"position": {
|
||||
@ -710,7 +766,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"actionType": "CODE",
|
||||
"name": "Step 2",
|
||||
"nodeType": "action",
|
||||
"runStatus": "running",
|
||||
"runStatus": "RUNNING",
|
||||
},
|
||||
"id": "step2",
|
||||
"position": {
|
||||
@ -723,7 +779,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"actionType": "CODE",
|
||||
"name": "Step 3",
|
||||
"nodeType": "action",
|
||||
"runStatus": "not-executed",
|
||||
"runStatus": "NOT_STARTED",
|
||||
},
|
||||
"id": "step3",
|
||||
"position": {
|
||||
@ -736,7 +792,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"actionType": "CODE",
|
||||
"name": "Step 4",
|
||||
"nodeType": "action",
|
||||
"runStatus": "not-executed",
|
||||
"runStatus": "NOT_STARTED",
|
||||
},
|
||||
"id": "step4",
|
||||
"position": {
|
||||
@ -786,15 +842,31 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
nextStepIds: undefined,
|
||||
},
|
||||
];
|
||||
const stepsOutput = {
|
||||
|
||||
const stepInfos: WorkflowRunStepInfos = {
|
||||
trigger: {
|
||||
result: {},
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
step1: {
|
||||
result: undefined,
|
||||
error: undefined,
|
||||
pendingEvent: true,
|
||||
result: {},
|
||||
status: StepStatus.PENDING,
|
||||
},
|
||||
step2: {
|
||||
result: {},
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
step3: {
|
||||
result: {},
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
};
|
||||
|
||||
const result = generateWorkflowRunDiagram({ trigger, steps, stepsOutput });
|
||||
const result = generateWorkflowRunDiagram({
|
||||
trigger,
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
{
|
||||
@ -817,7 +889,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"icon": "IconPlaylistAdd",
|
||||
"name": "Company created",
|
||||
"nodeType": "trigger",
|
||||
"runStatus": "success",
|
||||
"runStatus": "SUCCESS",
|
||||
"triggerType": "DATABASE_EVENT",
|
||||
},
|
||||
"id": "trigger",
|
||||
@ -831,7 +903,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"actionType": "FORM",
|
||||
"name": "Step 1",
|
||||
"nodeType": "action",
|
||||
"runStatus": "running",
|
||||
"runStatus": "PENDING",
|
||||
},
|
||||
"id": "step1",
|
||||
"position": {
|
||||
@ -846,7 +918,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"actionType": "FORM",
|
||||
"name": "Step 1",
|
||||
"nodeType": "action",
|
||||
"runStatus": "running",
|
||||
"runStatus": "PENDING",
|
||||
},
|
||||
"id": "step1",
|
||||
},
|
||||
|
||||
@ -1,11 +1,6 @@
|
||||
import {
|
||||
WorkflowRunOutputStepsOutput,
|
||||
WorkflowStep,
|
||||
WorkflowTrigger,
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { WorkflowStep, WorkflowTrigger } from '@/workflow/types/Workflow';
|
||||
import { WORKFLOW_VISUALIZER_EDGE_SUCCESS_CONFIGURATION } from '@/workflow/workflow-diagram/constants/WorkflowVisualizerEdgeSuccessConfiguration';
|
||||
import {
|
||||
WorkflowDiagramRunStatus,
|
||||
WorkflowRunDiagram,
|
||||
WorkflowRunDiagramNode,
|
||||
WorkflowRunDiagramStepNodeData,
|
||||
@ -14,15 +9,16 @@ import { generateWorkflowDiagram } from '@/workflow/workflow-diagram/utils/gener
|
||||
import { isStepNode } from '@/workflow/workflow-diagram/utils/isStepNode';
|
||||
import { transformFilterNodesAsEdges } from '@/workflow/workflow-diagram/utils/transformFilterNodesAsEdges';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { WorkflowRunStepInfos, StepStatus } from 'twenty-shared/workflow';
|
||||
|
||||
export const generateWorkflowRunDiagram = ({
|
||||
trigger,
|
||||
steps,
|
||||
stepsOutput,
|
||||
stepInfos,
|
||||
}: {
|
||||
trigger: WorkflowTrigger;
|
||||
steps: Array<WorkflowStep>;
|
||||
stepsOutput: WorkflowRunOutputStepsOutput | undefined;
|
||||
stepInfos: WorkflowRunStepInfos | undefined;
|
||||
}): {
|
||||
diagram: WorkflowRunDiagram;
|
||||
stepToOpenByDefault:
|
||||
@ -43,50 +39,29 @@ export const generateWorkflowRunDiagram = ({
|
||||
generateWorkflowDiagram({ trigger, steps }),
|
||||
);
|
||||
|
||||
let skippedExecution = false;
|
||||
|
||||
const workflowRunDiagramNodes: WorkflowRunDiagramNode[] =
|
||||
workflowDiagram.nodes.filter(isStepNode).map((node) => {
|
||||
if (node.data.nodeType === 'trigger') {
|
||||
const nodeId = node.id;
|
||||
|
||||
const stepInfo = stepInfos?.[nodeId];
|
||||
|
||||
if (!isDefined(stepInfo)) {
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
runStatus: 'success',
|
||||
runStatus: StepStatus.NOT_STARTED,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const nodeId = node.id;
|
||||
const nodeData = {
|
||||
...node.data,
|
||||
runStatus: stepInfo.status,
|
||||
};
|
||||
|
||||
const runResult = stepsOutput?.[nodeId];
|
||||
|
||||
const isPendingFormAction =
|
||||
node.data.nodeType === 'action' &&
|
||||
node.data.actionType === 'FORM' &&
|
||||
isDefined(runResult?.pendingEvent) &&
|
||||
runResult.pendingEvent;
|
||||
|
||||
let runStatus: WorkflowDiagramRunStatus = 'success';
|
||||
|
||||
if (skippedExecution) {
|
||||
runStatus = 'not-executed';
|
||||
} else if (!isDefined(runResult) || isPendingFormAction) {
|
||||
runStatus = 'running';
|
||||
} else if (isDefined(runResult.error)) {
|
||||
runStatus = 'failure';
|
||||
}
|
||||
|
||||
skippedExecution =
|
||||
skippedExecution || runStatus === 'failure' || runStatus === 'running';
|
||||
|
||||
const nodeData = { ...node.data, runStatus };
|
||||
|
||||
if (isPendingFormAction) {
|
||||
stepToOpenByDefault = {
|
||||
id: nodeId,
|
||||
data: nodeData,
|
||||
};
|
||||
if (!isDefined(stepToOpenByDefault) && stepInfo.status === 'PENDING') {
|
||||
stepToOpenByDefault = { id: nodeId, data: nodeData };
|
||||
}
|
||||
|
||||
return {
|
||||
@ -100,7 +75,17 @@ export const generateWorkflowRunDiagram = ({
|
||||
(node) => node.id === edge.source,
|
||||
);
|
||||
|
||||
if (isDefined(parentNode) && parentNode.data.runStatus === 'success') {
|
||||
if (!isDefined(parentNode)) {
|
||||
return edge;
|
||||
}
|
||||
|
||||
const stepInfo = stepInfos?.[parentNode.id];
|
||||
|
||||
if (!isDefined(stepInfo)) {
|
||||
return edge;
|
||||
}
|
||||
|
||||
if (stepInfo.status === 'SUCCESS') {
|
||||
return {
|
||||
...edge,
|
||||
...WORKFLOW_VISUALIZER_EDGE_SUCCESS_CONFIGURATION,
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
import { WorkflowRunStepStatus } from '@/workflow/types/Workflow';
|
||||
import { WorkflowDiagramNodeVariant } from '@/workflow/workflow-diagram/types/WorkflowDiagramNodeVariant';
|
||||
|
||||
export const getNodeVariantFromStepRunStatus = (
|
||||
runStatus: WorkflowRunStepStatus | undefined,
|
||||
): WorkflowDiagramNodeVariant => {
|
||||
switch (runStatus) {
|
||||
case 'SUCCESS':
|
||||
return 'success';
|
||||
case 'FAILED':
|
||||
return 'failure';
|
||||
case 'RUNNING':
|
||||
case 'PENDING':
|
||||
return 'running';
|
||||
case 'NOT_STARTED':
|
||||
return 'not-executed';
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
};
|
||||
@ -20,6 +20,7 @@ import {
|
||||
ShouldExpandNodeInitiallyProps,
|
||||
} from 'twenty-ui/json-visualizer';
|
||||
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
|
||||
import { JsonValue } from 'type-fest';
|
||||
|
||||
export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
|
||||
const { t, i18n } = useLingui();
|
||||
@ -29,15 +30,15 @@ export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
|
||||
|
||||
const workflowRunId = useWorkflowRunIdOrThrow();
|
||||
const workflowRun = useWorkflowRun({ workflowRunId });
|
||||
const step = workflowRun?.output?.flow.steps.find(
|
||||
const step = workflowRun?.state?.flow.steps.find(
|
||||
(step) => step.id === stepId,
|
||||
);
|
||||
|
||||
if (
|
||||
!(
|
||||
isDefined(workflowRun) &&
|
||||
isDefined(workflowRun.context) &&
|
||||
isDefined(workflowRun.output?.flow) &&
|
||||
isDefined(workflowRun.state?.stepInfos) &&
|
||||
isDefined(workflowRun.state?.flow) &&
|
||||
isDefined(step)
|
||||
)
|
||||
) {
|
||||
@ -46,7 +47,7 @@ export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
|
||||
|
||||
const previousStepId = getWorkflowPreviousStepId({
|
||||
stepId,
|
||||
steps: workflowRun.output.flow.steps,
|
||||
steps: workflowRun.state.flow.steps,
|
||||
});
|
||||
|
||||
if (previousStepId === undefined) {
|
||||
@ -55,8 +56,8 @@ export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
|
||||
|
||||
const stepDefinition = getStepDefinitionOrThrow({
|
||||
stepId,
|
||||
trigger: workflowRun.output.flow.trigger,
|
||||
steps: workflowRun.output.flow.steps,
|
||||
trigger: workflowRun.state.flow.trigger,
|
||||
steps: workflowRun.state.flow.steps,
|
||||
});
|
||||
if (stepDefinition?.type !== 'action') {
|
||||
throw new Error('The input tab must be rendered with an action step.');
|
||||
@ -76,10 +77,11 @@ export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
|
||||
const allVariablesUsedInStep = Array.from(variablesUsedInStep);
|
||||
|
||||
const stepContext = getWorkflowRunStepContext({
|
||||
context: workflowRun.context,
|
||||
flow: workflowRun.output.flow,
|
||||
stepInfos: workflowRun.state.stepInfos,
|
||||
flow: workflowRun.state.flow,
|
||||
stepId,
|
||||
});
|
||||
|
||||
if (stepContext.length === 0) {
|
||||
throw new Error('The input tab must be rendered with a non-empty context.');
|
||||
}
|
||||
@ -132,7 +134,7 @@ export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
|
||||
elements={stepContext.map(({ id, name, context }) => ({
|
||||
id,
|
||||
label: name,
|
||||
value: context,
|
||||
value: context as JsonValue,
|
||||
}))}
|
||||
Icon={IconBrackets}
|
||||
depth={0}
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import { WorkflowAction, WorkflowTrigger } from '@/workflow/types/Workflow';
|
||||
import {
|
||||
WorkflowAction,
|
||||
WorkflowRunStepStatus,
|
||||
WorkflowTrigger,
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
|
||||
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
|
||||
import { WorkflowDiagramRunStatus } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { WorkflowEditActionAiAgent } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowEditActionAiAgent';
|
||||
import { WorkflowActionServerlessFunction } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowActionServerlessFunction';
|
||||
import { WorkflowEditActionCreateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord';
|
||||
@ -21,7 +24,7 @@ type WorkflowRunStepNodeDetailProps = {
|
||||
stepId: string;
|
||||
trigger: WorkflowTrigger | null;
|
||||
steps: Array<WorkflowAction> | null;
|
||||
stepExecutionStatus?: WorkflowDiagramRunStatus;
|
||||
stepExecutionStatus?: WorkflowRunStepStatus;
|
||||
};
|
||||
|
||||
export const WorkflowRunStepNodeDetail = ({
|
||||
@ -172,7 +175,7 @@ export const WorkflowRunStepNodeDetail = ({
|
||||
key={stepId}
|
||||
action={stepDefinition.definition}
|
||||
actionOptions={{
|
||||
readonly: stepExecutionStatus !== 'running',
|
||||
readonly: stepExecutionStatus !== 'PENDING',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
|
||||
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
|
||||
import { WorkflowExecutorOutput } from '@/workflow/types/Workflow';
|
||||
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
|
||||
import { WorkflowRunStepJsonContainer } from '@/workflow/workflow-steps/components/WorkflowRunStepJsonContainer';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
@ -30,18 +29,16 @@ export const WorkflowRunStepOutputDetail = ({ stepId }: { stepId: string }) => {
|
||||
const workflowRunId = useWorkflowRunIdOrThrow();
|
||||
const workflowRun = useWorkflowRun({ workflowRunId });
|
||||
|
||||
if (!isDefined(workflowRun?.output?.stepsOutput)) {
|
||||
if (!isDefined(workflowRun?.state?.stepInfos)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stepOutput = workflowRun.output.stepsOutput[stepId] as
|
||||
| WorkflowExecutorOutput
|
||||
| undefined;
|
||||
const stepInfo = workflowRun.state.stepInfos[stepId];
|
||||
|
||||
const stepDefinition = getStepDefinitionOrThrow({
|
||||
stepId,
|
||||
trigger: workflowRun.output.flow.trigger,
|
||||
steps: workflowRun.output.flow.steps,
|
||||
trigger: workflowRun.state.flow.trigger,
|
||||
steps: workflowRun.state.flow.steps,
|
||||
});
|
||||
if (
|
||||
!isDefined(stepDefinition?.definition) ||
|
||||
@ -87,7 +84,7 @@ export const WorkflowRunStepOutputDetail = ({ stepId }: { stepId: string }) => {
|
||||
|
||||
<WorkflowRunStepJsonContainer>
|
||||
<JsonTree
|
||||
value={stepOutput ?? t`No output available`}
|
||||
value={stepInfo ?? t`No output available`}
|
||||
shouldExpandNodeInitially={isTwoFirstDepths}
|
||||
emptyArrayLabel={t`Empty Array`}
|
||||
emptyObjectLabel={t`Empty Object`}
|
||||
@ -95,7 +92,7 @@ export const WorkflowRunStepOutputDetail = ({ stepId }: { stepId: string }) => {
|
||||
arrowButtonCollapsedLabel={t`Expand`}
|
||||
arrowButtonExpandedLabel={t`Collapse`}
|
||||
getNodeHighlighting={
|
||||
isDefined(stepOutput?.error)
|
||||
isDefined(stepInfo?.error)
|
||||
? setRedHighlightingForEveryNode
|
||||
: undefined
|
||||
}
|
||||
|
||||
@ -49,18 +49,18 @@ export const useUpdateWorkflowRunStep = () => {
|
||||
|
||||
if (
|
||||
!isDefined(cachedRecord) ||
|
||||
!isDefined(cachedRecord?.output?.flow?.steps)
|
||||
!isDefined(cachedRecord?.state?.flow?.steps)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newCachedRecord = {
|
||||
...cachedRecord,
|
||||
output: {
|
||||
...cachedRecord.output,
|
||||
state: {
|
||||
...cachedRecord.state,
|
||||
flow: {
|
||||
...cachedRecord.output.flow,
|
||||
steps: cachedRecord.output.flow.steps.map((step: WorkflowAction) => {
|
||||
...cachedRecord.state.flow,
|
||||
steps: cachedRecord.state.flow.steps.map((step: WorkflowAction) => {
|
||||
if (step.id === updatedStep.id) {
|
||||
return updatedStep;
|
||||
}
|
||||
@ -71,7 +71,7 @@ export const useUpdateWorkflowRunStep = () => {
|
||||
};
|
||||
|
||||
const recordGqlFields = {
|
||||
output: true,
|
||||
state: true,
|
||||
};
|
||||
updateRecordFromCache({
|
||||
objectMetadataItems,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { WorkflowRunFlow } from '@/workflow/types/Workflow';
|
||||
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
||||
import { getWorkflowRunStepContext } from '../getWorkflowRunStepContext';
|
||||
import { StepStatus } from 'twenty-shared/workflow';
|
||||
|
||||
describe('getWorkflowRunStepContext', () => {
|
||||
it('should return an empty array for trigger step', () => {
|
||||
@ -15,14 +16,17 @@ describe('getWorkflowRunStepContext', () => {
|
||||
},
|
||||
steps: [],
|
||||
} satisfies WorkflowRunFlow;
|
||||
const context = {
|
||||
[TRIGGER_STEP_ID]: { company: { id: '123' } },
|
||||
const stepInfos = {
|
||||
[TRIGGER_STEP_ID]: {
|
||||
result: { company: { id: '123' } },
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
};
|
||||
|
||||
const result = getWorkflowRunStepContext({
|
||||
stepId: TRIGGER_STEP_ID,
|
||||
flow,
|
||||
context,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
@ -77,15 +81,20 @@ describe('getWorkflowRunStepContext', () => {
|
||||
},
|
||||
],
|
||||
} satisfies WorkflowRunFlow;
|
||||
const context = {
|
||||
[TRIGGER_STEP_ID]: { company: { id: '123' } },
|
||||
step1: { taskId: '456' },
|
||||
|
||||
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 },
|
||||
};
|
||||
|
||||
const result = getWorkflowRunStepContext({
|
||||
stepId: 'step2',
|
||||
flow,
|
||||
context,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
@ -149,16 +158,19 @@ describe('getWorkflowRunStepContext', () => {
|
||||
},
|
||||
],
|
||||
} satisfies WorkflowRunFlow;
|
||||
const context = {
|
||||
[TRIGGER_STEP_ID]: { company: { id: '123' } },
|
||||
step1: { taskId: '456' },
|
||||
step2: { emailId: '789' },
|
||||
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 },
|
||||
};
|
||||
|
||||
const result = getWorkflowRunStepContext({
|
||||
stepId: 'step1',
|
||||
flow,
|
||||
context,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
@ -237,17 +249,20 @@ describe('getWorkflowRunStepContext', () => {
|
||||
},
|
||||
],
|
||||
} satisfies WorkflowRunFlow;
|
||||
const context = {
|
||||
[TRIGGER_STEP_ID]: { company: { id: '123' } },
|
||||
step1: { noteId: '456' },
|
||||
step2: { noteId: '789' },
|
||||
step3: { noteId: '101' },
|
||||
const stepInfos = {
|
||||
[TRIGGER_STEP_ID]: {
|
||||
result: { company: { id: '123' } },
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
step1: { result: { noteId: '456' }, status: StepStatus.SUCCESS },
|
||||
step2: { result: { noteId: '789' }, status: StepStatus.SUCCESS },
|
||||
step3: { result: { noteId: '101' }, status: StepStatus.SUCCESS },
|
||||
};
|
||||
|
||||
const result = getWorkflowRunStepContext({
|
||||
stepId: 'step3',
|
||||
flow,
|
||||
context,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { getWorkflowRunStepExecutionStatus } from '../getWorkflowRunStepExecutionStatus';
|
||||
import { StepStatus } from 'twenty-shared/workflow';
|
||||
|
||||
describe('getWorkflowRunStepExecutionStatus', () => {
|
||||
const stepId = '453e0084-aca2-45b9-8d1c-458a2b8ac70a';
|
||||
@ -6,16 +7,16 @@ describe('getWorkflowRunStepExecutionStatus', () => {
|
||||
it('should return not-executed when the output is null', () => {
|
||||
expect(
|
||||
getWorkflowRunStepExecutionStatus({
|
||||
workflowRunOutput: null,
|
||||
workflowRunState: null,
|
||||
stepId,
|
||||
}),
|
||||
).toBe('not-executed');
|
||||
).toBe(StepStatus.NOT_STARTED);
|
||||
});
|
||||
|
||||
it('should return success when step has result', () => {
|
||||
expect(
|
||||
getWorkflowRunStepExecutionStatus({
|
||||
workflowRunOutput: {
|
||||
workflowRunState: {
|
||||
flow: {
|
||||
steps: [
|
||||
{
|
||||
@ -60,15 +61,20 @@ describe('getWorkflowRunStepExecutionStatus', () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
stepsOutput: {
|
||||
stepInfos: {
|
||||
trigger: {
|
||||
result: {},
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
[stepId]: {
|
||||
result: {},
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
},
|
||||
},
|
||||
stepId,
|
||||
}),
|
||||
).toBe('success');
|
||||
).toBe(StepStatus.SUCCESS);
|
||||
});
|
||||
|
||||
it('should return failure when workflow has error', () => {
|
||||
@ -76,7 +82,7 @@ describe('getWorkflowRunStepExecutionStatus', () => {
|
||||
|
||||
expect(
|
||||
getWorkflowRunStepExecutionStatus({
|
||||
workflowRunOutput: {
|
||||
workflowRunState: {
|
||||
flow: {
|
||||
steps: [
|
||||
{
|
||||
@ -121,16 +127,21 @@ describe('getWorkflowRunStepExecutionStatus', () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
error,
|
||||
stepsOutput: {
|
||||
workflowRunError: error,
|
||||
stepInfos: {
|
||||
trigger: {
|
||||
result: {},
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
[stepId]: {
|
||||
error,
|
||||
status: StepStatus.FAILED,
|
||||
},
|
||||
},
|
||||
},
|
||||
stepId,
|
||||
}),
|
||||
).toBe('failure');
|
||||
).toBe(StepStatus.FAILED);
|
||||
});
|
||||
|
||||
it('should return not-executed when step has no output', () => {
|
||||
@ -138,7 +149,7 @@ describe('getWorkflowRunStepExecutionStatus', () => {
|
||||
|
||||
expect(
|
||||
getWorkflowRunStepExecutionStatus({
|
||||
workflowRunOutput: {
|
||||
workflowRunState: {
|
||||
flow: {
|
||||
steps: [
|
||||
{
|
||||
@ -217,14 +228,23 @@ describe('getWorkflowRunStepExecutionStatus', () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
stepsOutput: {
|
||||
stepInfos: {
|
||||
trigger: {
|
||||
result: {},
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
[stepId]: {
|
||||
result: {},
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
[secondStepId]: {
|
||||
result: {},
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
},
|
||||
},
|
||||
stepId: secondStepId,
|
||||
}),
|
||||
).toBe('not-executed');
|
||||
).toBe(StepStatus.NOT_STARTED);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,14 +1,18 @@
|
||||
import { WorkflowRunContext, WorkflowRunFlow } from '@/workflow/types/Workflow';
|
||||
import { WorkflowRunFlow } from '@/workflow/types/Workflow';
|
||||
import { getPreviousSteps } from '@/workflow/workflow-steps/utils/getWorkflowPreviousSteps';
|
||||
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
||||
import {
|
||||
getWorkflowRunContext,
|
||||
WorkflowRunStepInfos,
|
||||
} from 'twenty-shared/workflow';
|
||||
|
||||
export const getWorkflowRunStepContext = ({
|
||||
stepId,
|
||||
flow,
|
||||
context,
|
||||
stepInfos,
|
||||
}: {
|
||||
stepId: string;
|
||||
context: WorkflowRunContext;
|
||||
stepInfos: WorkflowRunStepInfos;
|
||||
flow: WorkflowRunFlow;
|
||||
}) => {
|
||||
if (stepId === TRIGGER_STEP_ID) {
|
||||
@ -17,6 +21,8 @@ export const getWorkflowRunStepContext = ({
|
||||
|
||||
const previousSteps = getPreviousSteps(flow.steps, stepId);
|
||||
|
||||
const context = getWorkflowRunContext(stepInfos);
|
||||
|
||||
const previousStepsContext = previousSteps.map((step) => {
|
||||
return {
|
||||
id: step.id,
|
||||
|
||||
@ -1,37 +1,22 @@
|
||||
import { WorkflowRunOutput } from '@/workflow/types/Workflow';
|
||||
import { WorkflowDiagramRunStatus } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
||||
import { isNull } from '@sniptt/guards';
|
||||
import {
|
||||
WorkflowRunState,
|
||||
WorkflowRunStepStatus,
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { StepStatus } from 'twenty-shared/workflow';
|
||||
|
||||
export const getWorkflowRunStepExecutionStatus = ({
|
||||
workflowRunOutput,
|
||||
workflowRunState,
|
||||
stepId,
|
||||
}: {
|
||||
workflowRunOutput: WorkflowRunOutput | null;
|
||||
workflowRunState: WorkflowRunState | null;
|
||||
stepId: string;
|
||||
}): WorkflowDiagramRunStatus => {
|
||||
if (isNull(workflowRunOutput)) {
|
||||
return 'not-executed';
|
||||
}): WorkflowRunStepStatus => {
|
||||
const stepOutput = workflowRunState?.stepInfos?.[stepId];
|
||||
|
||||
if (isDefined(stepOutput)) {
|
||||
return stepOutput.status;
|
||||
}
|
||||
|
||||
if (stepId === TRIGGER_STEP_ID) {
|
||||
return 'success';
|
||||
}
|
||||
|
||||
const stepOutput = workflowRunOutput.stepsOutput?.[stepId];
|
||||
|
||||
if (isDefined(stepOutput?.error)) {
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
if (isDefined(stepOutput?.pendingEvent)) {
|
||||
return 'running';
|
||||
}
|
||||
|
||||
if (isDefined(stepOutput?.result)) {
|
||||
return 'success';
|
||||
}
|
||||
|
||||
return 'not-executed';
|
||||
return StepStatus.NOT_STARTED;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user