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;
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { MoreThan, Repository } from 'typeorm';
|
||||
import { Command, Option } from 'nest-commander';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { StepStatus, WorkflowRunStepInfos } from 'twenty-shared/workflow';
|
||||
|
||||
import {
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||
@ -15,10 +16,6 @@ import {
|
||||
WorkflowRunOutput,
|
||||
WorkflowRunWorkspaceEntity,
|
||||
} from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
|
||||
import {
|
||||
StepStatus,
|
||||
WorkflowRunStepInfo,
|
||||
} from 'src/modules/workflow/workflow-executor/types/workflow-run-step-info.type';
|
||||
|
||||
const DEFAULT_CHUNK_SIZE = 500;
|
||||
|
||||
@ -121,7 +118,7 @@ export class MigrateWorkflowRunStatesCommand extends ActiveOrSuspendedWorkspaces
|
||||
}
|
||||
|
||||
private buildRunStateFromOutput(output: WorkflowRunOutput): WorkflowRunState {
|
||||
const stepInfos: Record<string, WorkflowRunStepInfo> = Object.fromEntries(
|
||||
const stepInfos: WorkflowRunStepInfos = Object.fromEntries(
|
||||
output.flow.steps.map((step) => {
|
||||
const stepOutput = output.stepsOutput?.[step.id];
|
||||
const status = stepOutput?.pendingEvent
|
||||
|
||||
@ -0,0 +1,48 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
CacheLockOptions,
|
||||
CacheLockService,
|
||||
} from 'src/engine/core-modules/cache-lock/cache-lock.service';
|
||||
|
||||
export const WithLock = (
|
||||
lockKeyParamPath: string,
|
||||
options?: CacheLockOptions,
|
||||
): MethodDecorator => {
|
||||
const injectCacheLockService = Inject(CacheLockService);
|
||||
|
||||
return function (target, propertyKey, descriptor: PropertyDescriptor) {
|
||||
injectCacheLockService(target, 'cacheLockService');
|
||||
|
||||
const originalMethod = descriptor.value;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
descriptor.value = async function (...args: any[]) {
|
||||
const self = this as { cacheLockService: CacheLockService };
|
||||
|
||||
if (!self.cacheLockService) {
|
||||
throw new Error('cacheLockService not available on instance');
|
||||
}
|
||||
|
||||
if (typeof args[0] !== 'object') {
|
||||
throw new Error(
|
||||
`You must use one object parameter to use @WithLock decorator. Received ${args}`,
|
||||
);
|
||||
}
|
||||
|
||||
const key = args[0][lockKeyParamPath];
|
||||
|
||||
if (typeof key !== 'string') {
|
||||
throw new Error(
|
||||
`Could not resolve lock key from path "${lockKeyParamPath}" on first argument`,
|
||||
);
|
||||
}
|
||||
|
||||
return await self.cacheLockService.withLock(
|
||||
() => originalMethod.apply(self, args),
|
||||
key,
|
||||
options,
|
||||
);
|
||||
};
|
||||
};
|
||||
};
|
||||
@ -1,21 +1,11 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
import { isString } from '@nestjs/common/utils/shared.utils';
|
||||
|
||||
import { PROCESS_METADATA } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
|
||||
export interface MessageQueueProcessOptions {
|
||||
jobName: string;
|
||||
concurrency?: number;
|
||||
}
|
||||
|
||||
export function Process(jobName: string): MethodDecorator;
|
||||
export function Process(options: MessageQueueProcessOptions): MethodDecorator;
|
||||
export function Process(
|
||||
nameOrOptions: string | MessageQueueProcessOptions,
|
||||
): MethodDecorator {
|
||||
const options = isString(nameOrOptions)
|
||||
? { jobName: nameOrOptions }
|
||||
: nameOrOptions;
|
||||
|
||||
return SetMetadata(PROCESS_METADATA, options || {});
|
||||
export function Process(jobName: string): MethodDecorator {
|
||||
return SetMetadata(PROCESS_METADATA, { jobName });
|
||||
}
|
||||
|
||||
@ -1,12 +1,9 @@
|
||||
import { Scope, SetMetadata } from '@nestjs/common';
|
||||
import { SCOPE_OPTIONS_METADATA } from '@nestjs/common/constants';
|
||||
|
||||
import { MessageQueueWorkerOptions } from 'src/engine/core-modules/message-queue/interfaces/message-queue-worker-options.interface';
|
||||
|
||||
import {
|
||||
MessageQueue,
|
||||
PROCESSOR_METADATA,
|
||||
WORKER_METADATA,
|
||||
} from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
|
||||
export interface MessageQueueProcessorOptions {
|
||||
@ -24,16 +21,7 @@ export interface MessageQueueProcessorOptions {
|
||||
* Represents a worker that is able to process jobs from the queue.
|
||||
* @param queueName name of the queue to process
|
||||
*/
|
||||
export function Processor(queueName: string): ClassDecorator;
|
||||
/**
|
||||
* Represents a worker that is able to process jobs from the queue.
|
||||
* @param queueName name of the queue to process
|
||||
* @param workerOptions additional worker options
|
||||
*/
|
||||
export function Processor(
|
||||
queueName: string,
|
||||
workerOptions: MessageQueueWorkerOptions,
|
||||
): ClassDecorator;
|
||||
export function Processor(queueName: MessageQueue): ClassDecorator;
|
||||
/**
|
||||
* Represents a worker that is able to process jobs from the queue.
|
||||
* @param processorOptions processor options
|
||||
@ -41,21 +29,11 @@ export function Processor(
|
||||
export function Processor(
|
||||
processorOptions: MessageQueueProcessorOptions,
|
||||
): ClassDecorator;
|
||||
/**
|
||||
* Represents a worker that is able to process jobs from the queue.
|
||||
* @param processorOptions processor options (Nest-specific)
|
||||
* @param workerOptions additional Bull worker options
|
||||
*/
|
||||
export function Processor(
|
||||
processorOptions: MessageQueueProcessorOptions,
|
||||
workerOptions: MessageQueueWorkerOptions,
|
||||
): ClassDecorator;
|
||||
export function Processor(
|
||||
queueNameOrOptions?: string | MessageQueueProcessorOptions,
|
||||
maybeWorkerOptions?: MessageQueueWorkerOptions,
|
||||
queueNameOrOptions: string | MessageQueueProcessorOptions,
|
||||
): ClassDecorator {
|
||||
const options =
|
||||
queueNameOrOptions && typeof queueNameOrOptions === 'object'
|
||||
typeof queueNameOrOptions === 'object'
|
||||
? queueNameOrOptions
|
||||
: { queueName: queueNameOrOptions };
|
||||
|
||||
@ -63,7 +41,5 @@ export function Processor(
|
||||
return (target: Function) => {
|
||||
SetMetadata(SCOPE_OPTIONS_METADATA, options)(target);
|
||||
SetMetadata(PROCESSOR_METADATA, options)(target);
|
||||
maybeWorkerOptions &&
|
||||
SetMetadata(WORKER_METADATA, maybeWorkerOptions)(target);
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
export const PROCESSOR_METADATA = Symbol('message-queue:processor_metadata');
|
||||
export const PROCESS_METADATA = Symbol('message-queue:process_metadata');
|
||||
export const WORKER_METADATA = Symbol('bullmq:worker_metadata');
|
||||
export const QUEUE_DRIVER = Symbol('message-queue:queue_driver');
|
||||
|
||||
export enum MessageQueue {
|
||||
@ -13,11 +12,8 @@ export enum MessageQueue {
|
||||
contactCreationQueue = 'contact-creation-queue',
|
||||
billingQueue = 'billing-queue',
|
||||
workspaceQueue = 'workspace-queue',
|
||||
recordPositionBackfillQueue = 'record-position-backfill-queue',
|
||||
entityEventsToDbQueue = 'entity-events-to-db-queue',
|
||||
testQueue = 'test-queue',
|
||||
workflowQueue = 'workflow-queue',
|
||||
serverlessFunctionQueue = 'serverless-function-queue',
|
||||
deleteCascadeQueue = 'delete-cascade-queue',
|
||||
subscriptionsQueue = 'subscriptions-queue',
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { WorkflowRunStepInfos } from 'twenty-shared/workflow';
|
||||
|
||||
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface';
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
@ -29,7 +30,6 @@ import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-o
|
||||
import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
|
||||
import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
|
||||
import { WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type';
|
||||
import { WorkflowRunStepInfo } from 'src/modules/workflow/workflow-executor/types/workflow-run-step-info.type';
|
||||
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
import { WorkflowTrigger } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
|
||||
|
||||
@ -60,7 +60,7 @@ export type WorkflowRunState = {
|
||||
trigger: WorkflowTrigger;
|
||||
steps: WorkflowAction[];
|
||||
};
|
||||
stepInfos: Record<string, WorkflowRunStepInfo>;
|
||||
stepInfos: WorkflowRunStepInfos;
|
||||
workflowRunError?: string;
|
||||
};
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined, isValidUuid } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
import { StepStatus } from 'twenty-shared/workflow';
|
||||
|
||||
import { BASE_TYPESCRIPT_PROJECT_INPUT_SCHEMA } from 'src/engine/core-modules/serverless/drivers/constants/base-typescript-project-input-schema';
|
||||
import { CreateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/create-workflow-version-step-input.dto';
|
||||
@ -27,7 +28,6 @@ import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/work
|
||||
import { WorkflowSchemaWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service';
|
||||
import { insertStep } from 'src/modules/workflow/workflow-builder/workflow-step/utils/insert-step';
|
||||
import { removeStep } from 'src/modules/workflow/workflow-builder/workflow-step/utils/remove-step';
|
||||
import { StepStatus } from 'src/modules/workflow/workflow-executor/types/workflow-run-step-info.type';
|
||||
import { BaseWorkflowActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-settings.type';
|
||||
import {
|
||||
WorkflowAction,
|
||||
@ -302,7 +302,7 @@ export class WorkflowVersionStepWorkspaceService {
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const step = workflowRun.output?.flow?.steps?.find(
|
||||
const step = workflowRun.state?.flow?.steps?.find(
|
||||
(step) => step.id === stepId,
|
||||
);
|
||||
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import { StepStatus } from 'twenty-shared/workflow';
|
||||
|
||||
import {
|
||||
WorkflowAction,
|
||||
WorkflowActionType,
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
import { canExecuteStep } from 'src/modules/workflow/workflow-executor/utils/can-execute-step.utils';
|
||||
import { canExecuteStep } from 'src/modules/workflow/workflow-executor/utils/can-execute-step.util';
|
||||
|
||||
describe('canExecuteStep', () => {
|
||||
const steps = [
|
||||
@ -42,13 +44,19 @@ describe('canExecuteStep', () => {
|
||||
] as WorkflowAction[];
|
||||
|
||||
it('should return true if all parents succeeded', () => {
|
||||
const context = {
|
||||
trigger: 'trigger result',
|
||||
'step-1': 'step-1 result',
|
||||
'step-2': 'step-2 result',
|
||||
const stepInfos = {
|
||||
'step-1': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-2': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-3': {
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
};
|
||||
|
||||
const result = canExecuteStep({ context, steps, stepId: 'step-3' });
|
||||
const result = canExecuteStep({ stepInfos, steps, stepId: 'step-3' });
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
@ -56,9 +64,16 @@ describe('canExecuteStep', () => {
|
||||
it('should return false if one parent is not succeeded', () => {
|
||||
expect(
|
||||
canExecuteStep({
|
||||
context: {
|
||||
trigger: 'trigger result',
|
||||
'step-2': 'step-2 result',
|
||||
stepInfos: {
|
||||
'step-1': {
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
'step-2': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-3': {
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
},
|
||||
steps,
|
||||
stepId: 'step-3',
|
||||
@ -67,9 +82,16 @@ describe('canExecuteStep', () => {
|
||||
|
||||
expect(
|
||||
canExecuteStep({
|
||||
context: {
|
||||
trigger: 'trigger result',
|
||||
'step-1': 'step-1 result',
|
||||
stepInfos: {
|
||||
'step-1': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-2': {
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
'step-3': {
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
},
|
||||
steps,
|
||||
stepId: 'step-3',
|
||||
@ -78,9 +100,90 @@ describe('canExecuteStep', () => {
|
||||
|
||||
expect(
|
||||
canExecuteStep({
|
||||
context: {
|
||||
trigger: 'trigger result',
|
||||
'step-1': {},
|
||||
stepInfos: {
|
||||
'step-1': {
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
'step-2': {
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
'step-3': {
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
},
|
||||
steps,
|
||||
stepId: 'step-3',
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if step has already ran', () => {
|
||||
expect(
|
||||
canExecuteStep({
|
||||
stepInfos: {
|
||||
'step-1': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-2': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-3': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
},
|
||||
steps,
|
||||
stepId: 'step-3',
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
canExecuteStep({
|
||||
stepInfos: {
|
||||
'step-1': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-2': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-3': {
|
||||
status: StepStatus.PENDING,
|
||||
},
|
||||
},
|
||||
steps,
|
||||
stepId: 'step-3',
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
canExecuteStep({
|
||||
stepInfos: {
|
||||
'step-1': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-2': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-3': {
|
||||
status: StepStatus.FAILED,
|
||||
},
|
||||
},
|
||||
steps,
|
||||
stepId: 'step-3',
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
canExecuteStep({
|
||||
stepInfos: {
|
||||
'step-1': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-2': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-3': {
|
||||
status: StepStatus.RUNNING,
|
||||
},
|
||||
},
|
||||
steps,
|
||||
stepId: 'step-3',
|
||||
|
||||
@ -1,23 +1,30 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { StepStatus, WorkflowRunStepInfos } from 'twenty-shared/workflow';
|
||||
|
||||
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
|
||||
export const canExecuteStep = ({
|
||||
context,
|
||||
stepId,
|
||||
steps,
|
||||
stepInfos,
|
||||
}: {
|
||||
steps: WorkflowAction[];
|
||||
context: Record<string, unknown>;
|
||||
stepInfos: WorkflowRunStepInfos;
|
||||
stepId: string;
|
||||
}) => {
|
||||
if (
|
||||
isDefined(stepInfos[stepId]?.status) &&
|
||||
stepInfos[stepId].status !== StepStatus.NOT_STARTED
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parentSteps = steps.filter(
|
||||
(parentStep) =>
|
||||
isDefined(parentStep) && parentStep.nextStepIds?.includes(stepId),
|
||||
);
|
||||
|
||||
// TODO use workflowRun.state to check if step status is not COMPLETED. Return false in this case
|
||||
return parentSteps.every((parentStep) =>
|
||||
Object.keys(context).includes(parentStep.id),
|
||||
return parentSteps.every(
|
||||
(parentStep) => stepInfos[parentStep.id]?.status === StepStatus.SUCCESS,
|
||||
);
|
||||
};
|
||||
@ -1,5 +1,7 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { getWorkflowRunContext, StepStatus } from 'twenty-shared/workflow';
|
||||
|
||||
import { BILLING_FEATURE_USED } from 'src/engine/core-modules/billing/constants/billing-feature-used.constant';
|
||||
import { BILLING_WORKFLOW_EXECUTION_ERROR_MESSAGE } from 'src/engine/core-modules/billing/constants/billing-workflow-execution-error-message.constant';
|
||||
import { BillingMeterEventName } from 'src/engine/core-modules/billing/enums/billing-meter-event-names';
|
||||
@ -12,15 +14,14 @@ import {
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
import { WorkflowExecutorWorkspaceService } from 'src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service';
|
||||
import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service';
|
||||
import { StepStatus } from 'src/modules/workflow/workflow-executor/types/workflow-run-step-info.type';
|
||||
import { WorkflowRunStatus } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
|
||||
import { canExecuteStep } from 'src/modules/workflow/workflow-executor/utils/can-execute-step.utils';
|
||||
import { canExecuteStep } from 'src/modules/workflow/workflow-executor/utils/can-execute-step.util';
|
||||
|
||||
jest.mock(
|
||||
'src/modules/workflow/workflow-executor/utils/can-execute-step.utils',
|
||||
'src/modules/workflow/workflow-executor/utils/can-execute-step.util',
|
||||
() => {
|
||||
const actual = jest.requireActual(
|
||||
'src/modules/workflow/workflow-executor/utils/can-execute-step.utils',
|
||||
'src/modules/workflow/workflow-executor/utils/can-execute-step.util',
|
||||
);
|
||||
|
||||
return {
|
||||
@ -100,7 +101,6 @@ describe('WorkflowExecutorWorkspaceService', () => {
|
||||
describe('execute', () => {
|
||||
const mockWorkflowRunId = 'workflow-run-id';
|
||||
const mockWorkspaceId = 'workspace-id';
|
||||
const mockContext = { trigger: 'trigger-result' };
|
||||
const mockSteps = [
|
||||
{
|
||||
id: 'step-1',
|
||||
@ -125,10 +125,12 @@ describe('WorkflowExecutorWorkspaceService', () => {
|
||||
nextStepIds: [],
|
||||
},
|
||||
] as WorkflowAction[];
|
||||
const mockStepInfos = {
|
||||
trigger: { result: {}, status: StepStatus.SUCCESS },
|
||||
};
|
||||
|
||||
mockWorkflowRunWorkspaceService.getWorkflowRun.mockReturnValue({
|
||||
output: { flow: { steps: mockSteps } },
|
||||
context: mockContext,
|
||||
state: { flow: { steps: mockSteps }, stepInfos: mockStepInfos },
|
||||
});
|
||||
|
||||
it('should execute a step and continue to the next step on success', async () => {
|
||||
@ -151,7 +153,7 @@ describe('WorkflowExecutorWorkspaceService', () => {
|
||||
expect(mockWorkflowExecutor.execute).toHaveBeenCalledWith({
|
||||
currentStepId: 'step-1',
|
||||
steps: mockSteps,
|
||||
context: mockContext,
|
||||
context: getWorkflowRunContext(mockStepInfos),
|
||||
});
|
||||
|
||||
expect(workspaceEventEmitter.emitCustomBatchEvent).toHaveBeenCalledWith(
|
||||
@ -319,8 +321,10 @@ describe('WorkflowExecutorWorkspaceService', () => {
|
||||
] as WorkflowAction[];
|
||||
|
||||
mockWorkflowRunWorkspaceService.getWorkflowRun.mockReturnValueOnce({
|
||||
output: { flow: { steps: stepsWithContinueOnFailure } },
|
||||
context: mockContext,
|
||||
state: {
|
||||
flow: { steps: stepsWithContinueOnFailure },
|
||||
stepInfos: mockStepInfos,
|
||||
},
|
||||
});
|
||||
|
||||
mockWorkflowExecutor.execute.mockResolvedValueOnce({
|
||||
@ -385,8 +389,10 @@ describe('WorkflowExecutorWorkspaceService', () => {
|
||||
] as WorkflowAction[];
|
||||
|
||||
mockWorkflowRunWorkspaceService.getWorkflowRun.mockReturnValue({
|
||||
output: { flow: { steps: stepsWithRetryOnFailure } },
|
||||
context: mockContext,
|
||||
state: {
|
||||
flow: { steps: stepsWithRetryOnFailure },
|
||||
stepInfos: mockStepInfos,
|
||||
},
|
||||
});
|
||||
|
||||
mockWorkflowExecutor.execute.mockResolvedValue({
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { getWorkflowRunContext, StepStatus } from 'twenty-shared/workflow';
|
||||
|
||||
import { BILLING_FEATURE_USED } from 'src/engine/core-modules/billing/constants/billing-feature-used.constant';
|
||||
import { BILLING_WORKFLOW_EXECUTION_ERROR_MESSAGE } from 'src/engine/core-modules/billing/constants/billing-workflow-execution-error-message.constant';
|
||||
@ -19,8 +20,7 @@ import {
|
||||
WorkflowBranchExecutorInput,
|
||||
WorkflowExecutorInput,
|
||||
} from 'src/modules/workflow/workflow-executor/types/workflow-executor-input';
|
||||
import { StepStatus } from 'src/modules/workflow/workflow-executor/types/workflow-run-step-info.type';
|
||||
import { canExecuteStep } from 'src/modules/workflow/workflow-executor/utils/can-execute-step.utils';
|
||||
import { canExecuteStep } from 'src/modules/workflow/workflow-executor/utils/can-execute-step.util';
|
||||
import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service';
|
||||
|
||||
const MAX_RETRIES_ON_FAILURE = 3;
|
||||
@ -57,7 +57,7 @@ export class WorkflowExecutorWorkspaceService {
|
||||
workspaceId,
|
||||
}: WorkflowBranchExecutorInput) {
|
||||
const workflowRunInfo = await this.getWorkflowRunInfoOrEndWorkflowRun({
|
||||
stepId: stepId,
|
||||
stepId,
|
||||
workflowRunId,
|
||||
workspaceId,
|
||||
});
|
||||
@ -66,9 +66,9 @@ export class WorkflowExecutorWorkspaceService {
|
||||
return;
|
||||
}
|
||||
|
||||
const { stepToExecute, steps, context } = workflowRunInfo;
|
||||
const { stepToExecute, steps, stepInfos } = workflowRunInfo;
|
||||
|
||||
if (!canExecuteStep({ stepId: stepToExecute.id, steps, context })) {
|
||||
if (!canExecuteStep({ stepId, steps, stepInfos })) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -98,7 +98,7 @@ export class WorkflowExecutorWorkspaceService {
|
||||
actionOutput = await workflowAction.execute({
|
||||
currentStepId: stepId,
|
||||
steps,
|
||||
context,
|
||||
context: getWorkflowRunContext(stepInfos),
|
||||
});
|
||||
} catch (error) {
|
||||
actionOutput = {
|
||||
@ -219,31 +219,18 @@ export class WorkflowExecutorWorkspaceService {
|
||||
return;
|
||||
}
|
||||
|
||||
const steps = workflowRun.output?.flow.steps;
|
||||
|
||||
const context = workflowRun.context;
|
||||
|
||||
if (!isDefined(steps)) {
|
||||
if (!isDefined(workflowRun?.state)) {
|
||||
await this.workflowRunWorkspaceService.endWorkflowRun({
|
||||
workflowRunId,
|
||||
workspaceId,
|
||||
status: WorkflowRunStatus.FAILED,
|
||||
error: 'Steps undefined',
|
||||
error: `WorkflowRun ${workflowRunId} doesn't have any state`,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDefined(context)) {
|
||||
await this.workflowRunWorkspaceService.endWorkflowRun({
|
||||
workflowRunId,
|
||||
workspaceId,
|
||||
status: WorkflowRunStatus.FAILED,
|
||||
error: 'Context not found',
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
const steps = workflowRun.state.flow.steps;
|
||||
|
||||
const stepToExecute = steps.find((step) => step.id === stepId);
|
||||
|
||||
@ -258,7 +245,11 @@ export class WorkflowExecutorWorkspaceService {
|
||||
return;
|
||||
}
|
||||
|
||||
return { stepToExecute, steps, context };
|
||||
return {
|
||||
stepToExecute,
|
||||
steps,
|
||||
stepInfos: workflowRun.state.stepInfos,
|
||||
};
|
||||
}
|
||||
|
||||
private sendWorkflowNodeRunEvent(workspaceId: string) {
|
||||
|
||||
@ -144,7 +144,7 @@ export class RunWorkflowJob {
|
||||
);
|
||||
}
|
||||
|
||||
const lastExecutedStep = workflowRun.output?.flow?.steps?.find(
|
||||
const lastExecutedStep = workflowRun.state?.flow?.steps?.find(
|
||||
(step) => step.id === lastExecutedStepId,
|
||||
);
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ import { Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
|
||||
import { StepStatus } from 'twenty-shared/workflow';
|
||||
|
||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||
import { objectRecordChangedValues } from 'src/engine/core-modules/event-emitter/utils/object-record-changed-values';
|
||||
@ -28,9 +29,8 @@ import {
|
||||
WorkflowRunException,
|
||||
WorkflowRunExceptionCode,
|
||||
} from 'src/modules/workflow/workflow-runner/exceptions/workflow-run.exception';
|
||||
import { StepStatus } from 'src/modules/workflow/workflow-executor/types/workflow-run-step-info.type';
|
||||
import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
|
||||
import { CacheLockService } from 'src/engine/core-modules/cache-lock/cache-lock.service';
|
||||
import { WithLock } from 'src/engine/core-modules/cache-lock/with-lock.decorator';
|
||||
|
||||
@Injectable()
|
||||
export class WorkflowRunWorkspaceService {
|
||||
@ -43,7 +43,6 @@ export class WorkflowRunWorkspaceService {
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
private readonly recordPositionService: RecordPositionService,
|
||||
private readonly metricsService: MetricsService,
|
||||
private readonly cacheLockService: CacheLockService,
|
||||
) {}
|
||||
|
||||
async createWorkflowRun({
|
||||
@ -141,18 +140,8 @@ export class WorkflowRunWorkspaceService {
|
||||
return workflowRun.id;
|
||||
}
|
||||
|
||||
async startWorkflowRun(params: {
|
||||
workflowRunId: string;
|
||||
workspaceId: string;
|
||||
payload: object;
|
||||
}) {
|
||||
await this.cacheLockService.withLock(
|
||||
async () => await this.startWorkflowRunWithoutLock(params),
|
||||
params.workflowRunId,
|
||||
);
|
||||
}
|
||||
|
||||
private async startWorkflowRunWithoutLock({
|
||||
@WithLock('workflowRunId')
|
||||
async startWorkflowRun({
|
||||
workflowRunId,
|
||||
workspaceId,
|
||||
payload,
|
||||
@ -207,19 +196,8 @@ export class WorkflowRunWorkspaceService {
|
||||
await this.updateWorkflowRun({ workflowRunId, workspaceId, partialUpdate });
|
||||
}
|
||||
|
||||
async endWorkflowRun(params: {
|
||||
workflowRunId: string;
|
||||
workspaceId: string;
|
||||
status: WorkflowRunStatus;
|
||||
error?: string;
|
||||
}) {
|
||||
await this.cacheLockService.withLock(
|
||||
async () => await this.endWorkflowRunWithoutLock(params),
|
||||
params.workflowRunId,
|
||||
);
|
||||
}
|
||||
|
||||
private async endWorkflowRunWithoutLock({
|
||||
@WithLock('workflowRunId')
|
||||
async endWorkflowRun({
|
||||
workflowRunId,
|
||||
workspaceId,
|
||||
status,
|
||||
@ -259,19 +237,8 @@ export class WorkflowRunWorkspaceService {
|
||||
});
|
||||
}
|
||||
|
||||
async updateWorkflowRunStepStatus(params: {
|
||||
workflowRunId: string;
|
||||
stepId: string;
|
||||
workspaceId: string;
|
||||
stepStatus: StepStatus;
|
||||
}) {
|
||||
await this.cacheLockService.withLock(
|
||||
async () => await this.updateWorkflowRunStepStatusWithoutLock(params),
|
||||
params.workflowRunId,
|
||||
);
|
||||
}
|
||||
|
||||
private async updateWorkflowRunStepStatusWithoutLock({
|
||||
@WithLock('workflowRunId')
|
||||
async updateWorkflowRunStepStatus({
|
||||
workflowRunId,
|
||||
workspaceId,
|
||||
stepId,
|
||||
@ -303,19 +270,8 @@ export class WorkflowRunWorkspaceService {
|
||||
await this.updateWorkflowRun({ workflowRunId, workspaceId, partialUpdate });
|
||||
}
|
||||
|
||||
async saveWorkflowRunState(params: {
|
||||
workflowRunId: string;
|
||||
stepOutput: StepOutput;
|
||||
workspaceId: string;
|
||||
stepStatus: StepStatus;
|
||||
}) {
|
||||
await this.cacheLockService.withLock(
|
||||
async () => await this.saveWorkflowRunStateWithoutLock(params),
|
||||
params.workflowRunId,
|
||||
);
|
||||
}
|
||||
|
||||
private async saveWorkflowRunStateWithoutLock({
|
||||
@WithLock('workflowRunId')
|
||||
async saveWorkflowRunState({
|
||||
workflowRunId,
|
||||
stepOutput,
|
||||
workspaceId,
|
||||
@ -367,18 +323,8 @@ export class WorkflowRunWorkspaceService {
|
||||
await this.updateWorkflowRun({ workflowRunId, workspaceId, partialUpdate });
|
||||
}
|
||||
|
||||
async updateWorkflowRunStep(params: {
|
||||
workflowRunId: string;
|
||||
step: WorkflowAction;
|
||||
workspaceId: string;
|
||||
}) {
|
||||
await this.cacheLockService.withLock(
|
||||
async () => await this.updateWorkflowRunStepWithoutLock(params),
|
||||
params.workflowRunId,
|
||||
);
|
||||
}
|
||||
|
||||
private async updateWorkflowRunStepWithoutLock({
|
||||
@WithLock('workflowRunId')
|
||||
async updateWorkflowRunStep({
|
||||
workflowRunId,
|
||||
step,
|
||||
workspaceId,
|
||||
|
||||
@ -34,6 +34,7 @@
|
||||
"./translations/index.ts",
|
||||
"./types/index.ts",
|
||||
"./utils/index.ts",
|
||||
"./workflow/index.ts",
|
||||
"./workspace/index.ts"
|
||||
]
|
||||
},
|
||||
@ -44,6 +45,7 @@
|
||||
"translations",
|
||||
"types",
|
||||
"utils",
|
||||
"workflow",
|
||||
"workspace"
|
||||
]
|
||||
}
|
||||
|
||||
@ -24,6 +24,8 @@
|
||||
"{projectRoot}/types/dist",
|
||||
"{projectRoot}/utils/package.json",
|
||||
"{projectRoot}/utils/dist",
|
||||
"{projectRoot}/workflow/package.json",
|
||||
"{projectRoot}/workflow/dist",
|
||||
"{projectRoot}/workspace/package.json",
|
||||
"{projectRoot}/workspace/dist"
|
||||
]
|
||||
|
||||
15
packages/twenty-shared/src/workflow/index.ts
Normal file
15
packages/twenty-shared/src/workflow/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
/*
|
||||
* _____ _
|
||||
*|_ _|_ _____ _ __ | |_ _ _
|
||||
* | | \ \ /\ / / _ \ '_ \| __| | | | Auto-generated file
|
||||
* | | \ V V / __/ | | | |_| |_| | Any edits to this will be overridden
|
||||
* |_| \_/\_/ \___|_| |_|\__|\__, |
|
||||
* |___/
|
||||
*/
|
||||
|
||||
export type {
|
||||
WorkflowRunStepInfo,
|
||||
WorkflowRunStepInfos,
|
||||
} from './types/WorkflowRunStateStepInfos';
|
||||
export { StepStatus } from './types/WorkflowRunStateStepInfos';
|
||||
export { getWorkflowRunContext } from './utils/getWorkflowRunContext';
|
||||
@ -11,3 +11,5 @@ export type WorkflowRunStepInfo = {
|
||||
error?: string;
|
||||
status: StepStatus;
|
||||
};
|
||||
|
||||
export type WorkflowRunStepInfos = Record<string, WorkflowRunStepInfo>;
|
||||
@ -0,0 +1,37 @@
|
||||
import { getWorkflowRunContext } from '@/workflow/utils/getWorkflowRunContext';
|
||||
import {
|
||||
StepStatus,
|
||||
WorkflowRunStepInfos,
|
||||
} from '@/workflow/types/WorkflowRunStateStepInfos';
|
||||
|
||||
describe('getWorkflowRunContext', () => {
|
||||
it('returns a context with only steps that have a defined result', () => {
|
||||
const stepInfos: WorkflowRunStepInfos = {
|
||||
step1: { result: { res: 'value1' }, status: StepStatus.SUCCESS },
|
||||
step2: { result: {}, status: StepStatus.SUCCESS },
|
||||
step3: { status: StepStatus.NOT_STARTED },
|
||||
step4: { result: { res: 0 }, status: StepStatus.SUCCESS },
|
||||
step5: { result: { res: undefined }, status: StepStatus.SUCCESS },
|
||||
};
|
||||
|
||||
const context = getWorkflowRunContext(stepInfos);
|
||||
|
||||
expect(context).toEqual({
|
||||
step1: { res: 'value1' },
|
||||
step2: {},
|
||||
step4: { res: 0 },
|
||||
step5: { res: undefined },
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an empty object when no step has a defined result', () => {
|
||||
const stepInfos: WorkflowRunStepInfos = {
|
||||
step1: { status: StepStatus.NOT_STARTED },
|
||||
step2: { status: StepStatus.NOT_STARTED },
|
||||
};
|
||||
|
||||
const context = getWorkflowRunContext(stepInfos);
|
||||
|
||||
expect(context).toEqual({});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,12 @@
|
||||
import { isDefined } from '@/utils';
|
||||
import { WorkflowRunStepInfos } from '@/workflow/types/WorkflowRunStateStepInfos';
|
||||
|
||||
export const getWorkflowRunContext = (
|
||||
stepInfos: WorkflowRunStepInfos,
|
||||
): Record<string, unknown> => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(stepInfos)
|
||||
.filter(([, value]) => isDefined(value?.['result']))
|
||||
.map(([key, value]) => [key, value?.['result']]),
|
||||
);
|
||||
};
|
||||
4
packages/twenty-shared/workflow/package.json
Normal file
4
packages/twenty-shared/workflow/package.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"main": "dist/twenty-shared-workflow.cjs.js",
|
||||
"module": "dist/twenty-shared-workflow.esm.js"
|
||||
}
|
||||
Reference in New Issue
Block a user