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:
+11
-9
@@ -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}
|
||||
|
||||
+7
-4
@@ -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',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
+6
-9
@@ -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
|
||||
}
|
||||
|
||||
+6
-6
@@ -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,
|
||||
|
||||
+33
-18
@@ -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([
|
||||
|
||||
+32
-12
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
+9
-3
@@ -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,
|
||||
|
||||
+13
-28
@@ -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