Start using next step ids (#11683)
- update workflow executor - update next step ids on step creation/deletion - use these in workflow run - use these in variables
This commit is contained in:
@ -8,6 +8,7 @@ export const CREATE_WORKFLOW_VERSION_STEP = gql`
|
||||
type
|
||||
settings
|
||||
valid
|
||||
nextStepIds
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -8,6 +8,7 @@ export const DELETE_WORKFLOW_VERSION_STEP = gql`
|
||||
type
|
||||
settings
|
||||
valid
|
||||
nextStepIds
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -8,6 +8,7 @@ export const UPDATE_WORKFLOW_RUN_STEP = gql`
|
||||
type
|
||||
settings
|
||||
valid
|
||||
nextStepIds
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -8,6 +8,7 @@ export const UPDATE_WORKFLOW_VERSION_STEP = gql`
|
||||
type
|
||||
settings
|
||||
valid
|
||||
nextStepIds
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -6,13 +6,13 @@ import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordF
|
||||
import { DELETE_WORKFLOW_VERSION_STEP } from '@/workflow/graphql/mutations/deleteWorkflowVersionStep';
|
||||
import { WorkflowVersion } from '@/workflow/types/Workflow';
|
||||
import { useApolloClient, useMutation } from '@apollo/client';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
DeleteWorkflowVersionStepInput,
|
||||
DeleteWorkflowVersionStepMutation,
|
||||
DeleteWorkflowVersionStepMutationVariables,
|
||||
WorkflowAction,
|
||||
} from '~/generated/graphql';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useDeleteWorkflowVersionStep = () => {
|
||||
const apolloClient = useApolloClient();
|
||||
@ -47,9 +47,16 @@ export const useDeleteWorkflowVersionStep = () => {
|
||||
|
||||
const newCachedRecord = {
|
||||
...cachedRecord,
|
||||
steps: (cachedRecord.steps || []).filter(
|
||||
(step: WorkflowAction) => step.id !== deletedStep.id,
|
||||
),
|
||||
steps: (cachedRecord.steps || [])
|
||||
.filter((step: WorkflowAction) => step.id !== deletedStep.id)
|
||||
.map((step) => {
|
||||
return {
|
||||
...step,
|
||||
nextStepIds: step.nextStepIds?.filter(
|
||||
(nextStepId) => nextStepId !== deletedStep.id,
|
||||
),
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
const recordGqlFields = {
|
||||
|
||||
@ -21,6 +21,7 @@ export const baseWorkflowActionSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
valid: z.boolean(),
|
||||
nextStepIds: z.array(z.string()).optional().nullable(),
|
||||
});
|
||||
|
||||
export const baseTriggerSchema = z.object({
|
||||
|
||||
@ -3,7 +3,7 @@ import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThro
|
||||
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
|
||||
import { WorkflowRunStepJsonContainer } from '@/workflow/workflow-steps/components/WorkflowRunStepJsonContainer';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { getWorkflowPreviousStepId } from '@/workflow/workflow-steps/utils/getWorkflowPreviousStep';
|
||||
import { getWorkflowPreviousStepId } from '@/workflow/workflow-steps/utils/getWorkflowPreviousStepId';
|
||||
import { getWorkflowRunStepContext } from '@/workflow/workflow-steps/utils/getWorkflowRunStepContext';
|
||||
import { getWorkflowVariablesUsedInStep } from '@/workflow/workflow-steps/utils/getWorkflowVariablesUsedInStep';
|
||||
import { getActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionHeaderTypeOrThrow';
|
||||
|
||||
@ -6,12 +6,12 @@ import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordF
|
||||
import { CREATE_WORKFLOW_VERSION_STEP } from '@/workflow/graphql/mutations/createWorkflowVersionStep';
|
||||
import { WorkflowVersion } from '@/workflow/types/Workflow';
|
||||
import { useApolloClient, useMutation } from '@apollo/client';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
CreateWorkflowVersionStepInput,
|
||||
CreateWorkflowVersionStepMutation,
|
||||
CreateWorkflowVersionStepMutationVariables,
|
||||
} from '~/generated/graphql';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useCreateWorkflowVersionStep = () => {
|
||||
const apolloClient = useApolloClient();
|
||||
@ -34,6 +34,7 @@ export const useCreateWorkflowVersionStep = () => {
|
||||
const result = await mutate({
|
||||
variables: { input },
|
||||
});
|
||||
|
||||
const createdStep = result?.data?.createWorkflowVersionStep;
|
||||
if (!isDefined(createdStep)) {
|
||||
return;
|
||||
@ -42,18 +43,31 @@ export const useCreateWorkflowVersionStep = () => {
|
||||
const cachedRecord = getRecordFromCache<WorkflowVersion>(
|
||||
input.workflowVersionId,
|
||||
);
|
||||
|
||||
if (!isDefined(cachedRecord)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedExistingSteps =
|
||||
cachedRecord.steps?.map((step) => {
|
||||
if (step.id === input.parentStepId) {
|
||||
return {
|
||||
...step,
|
||||
nextStepIds: [...(step.nextStepIds || []), createdStep.id],
|
||||
};
|
||||
}
|
||||
return step;
|
||||
}) ?? [];
|
||||
|
||||
const newCachedRecord = {
|
||||
...cachedRecord,
|
||||
steps: [...(cachedRecord.steps || []), createdStep],
|
||||
steps: [...updatedExistingSteps, createdStep],
|
||||
};
|
||||
|
||||
const recordGqlFields = {
|
||||
steps: true,
|
||||
};
|
||||
|
||||
updateRecordFromCache({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
|
||||
@ -0,0 +1,111 @@
|
||||
import { WorkflowStep } from '@/workflow/types/Workflow';
|
||||
import { getPreviousSteps } from '../getWorkflowPreviousSteps';
|
||||
|
||||
const mockWorkflow: WorkflowStep[] = [
|
||||
{
|
||||
id: 'step1',
|
||||
name: 'First Step',
|
||||
type: 'CODE',
|
||||
valid: true,
|
||||
nextStepIds: ['step2', 'step3'],
|
||||
settings: {
|
||||
input: {
|
||||
serverlessFunctionId: 'func1',
|
||||
serverlessFunctionVersion: '1.0.0',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: true },
|
||||
continueOnFailure: { value: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'step2',
|
||||
name: 'Second Step',
|
||||
type: 'CODE',
|
||||
valid: true,
|
||||
nextStepIds: ['step4'],
|
||||
settings: {
|
||||
input: {
|
||||
serverlessFunctionId: 'func2',
|
||||
serverlessFunctionVersion: '1.0.0',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: true },
|
||||
continueOnFailure: { value: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'step3',
|
||||
name: 'Third Step',
|
||||
type: 'CODE',
|
||||
valid: true,
|
||||
nextStepIds: ['step4'],
|
||||
settings: {
|
||||
input: {
|
||||
serverlessFunctionId: 'func3',
|
||||
serverlessFunctionVersion: '1.0.0',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: true },
|
||||
continueOnFailure: { value: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'step4',
|
||||
name: 'Fourth Step',
|
||||
type: 'CODE',
|
||||
valid: true,
|
||||
nextStepIds: [],
|
||||
settings: {
|
||||
input: {
|
||||
serverlessFunctionId: 'func4',
|
||||
serverlessFunctionVersion: '1.0.0',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: true },
|
||||
continueOnFailure: { value: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('getWorkflowPreviousSteps', () => {
|
||||
it('should return empty array when there are no previous steps', () => {
|
||||
const result = getPreviousSteps(mockWorkflow, 'step1');
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return direct previous steps', () => {
|
||||
const result = getPreviousSteps(mockWorkflow, 'step2');
|
||||
expect(result).toEqual([mockWorkflow[0]]);
|
||||
});
|
||||
|
||||
it('should return all previous steps including indirect ones', () => {
|
||||
const result = getPreviousSteps(mockWorkflow, 'step4');
|
||||
expect(result).toEqual([mockWorkflow[0], mockWorkflow[1], mockWorkflow[2]]);
|
||||
});
|
||||
|
||||
it('should handle circular dependencies', () => {
|
||||
const circularWorkflow = [...mockWorkflow];
|
||||
circularWorkflow[3].nextStepIds = ['step1']; // Make step4 point back to step1
|
||||
|
||||
const result = getPreviousSteps(circularWorkflow, 'step4');
|
||||
expect(result).toEqual([mockWorkflow[0], mockWorkflow[1], mockWorkflow[2]]);
|
||||
});
|
||||
|
||||
it('should handle non-existent step ID', () => {
|
||||
const result = getPreviousSteps(mockWorkflow, 'non-existent-step');
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
@ -55,6 +55,7 @@ describe('getWorkflowRunStepContext', () => {
|
||||
outputSchema: {},
|
||||
},
|
||||
valid: true,
|
||||
nextStepIds: ['step2'],
|
||||
},
|
||||
{
|
||||
id: 'step2',
|
||||
@ -72,6 +73,7 @@ describe('getWorkflowRunStepContext', () => {
|
||||
outputSchema: {},
|
||||
},
|
||||
valid: true,
|
||||
nextStepIds: [],
|
||||
},
|
||||
],
|
||||
} satisfies WorkflowRunFlow;
|
||||
@ -195,6 +197,7 @@ describe('getWorkflowRunStepContext', () => {
|
||||
outputSchema: {},
|
||||
},
|
||||
valid: true,
|
||||
nextStepIds: ['step2'],
|
||||
},
|
||||
{
|
||||
id: 'step2',
|
||||
@ -212,6 +215,7 @@ describe('getWorkflowRunStepContext', () => {
|
||||
outputSchema: {},
|
||||
},
|
||||
valid: true,
|
||||
nextStepIds: ['step3'],
|
||||
},
|
||||
{
|
||||
id: 'step3',
|
||||
@ -229,6 +233,7 @@ describe('getWorkflowRunStepContext', () => {
|
||||
outputSchema: {},
|
||||
},
|
||||
valid: true,
|
||||
nextStepIds: [],
|
||||
},
|
||||
],
|
||||
} satisfies WorkflowRunFlow;
|
||||
|
||||
@ -16,10 +16,7 @@ export const getWorkflowPreviousStepId = ({
|
||||
return TRIGGER_STEP_ID;
|
||||
}
|
||||
|
||||
const stepIndex = steps.findIndex((step) => step.id === stepId);
|
||||
if (stepIndex === -1) {
|
||||
throw new Error('Step not found');
|
||||
}
|
||||
const previousStep = steps.find((step) => step.nextStepIds?.includes(stepId));
|
||||
|
||||
return steps[stepIndex - 1].id;
|
||||
return previousStep?.id;
|
||||
};
|
||||
@ -0,0 +1,23 @@
|
||||
import { WorkflowStep } from '@/workflow/types/Workflow';
|
||||
|
||||
export const getPreviousSteps = (
|
||||
steps: WorkflowStep[],
|
||||
currentStepId: string,
|
||||
visitedSteps: Set<string> = new Set([currentStepId]),
|
||||
): WorkflowStep[] => {
|
||||
const parentSteps = steps
|
||||
.filter((step) => step.nextStepIds?.includes(currentStepId))
|
||||
.filter((step) => !visitedSteps.has(step.id));
|
||||
|
||||
const grandParentSteps = parentSteps
|
||||
.map((step) => {
|
||||
if (visitedSteps.has(step.id)) {
|
||||
return [];
|
||||
}
|
||||
visitedSteps.add(step.id);
|
||||
return getPreviousSteps(steps, step.id, visitedSteps);
|
||||
})
|
||||
.flat();
|
||||
|
||||
return [...grandParentSteps, ...parentSteps];
|
||||
};
|
||||
@ -1,4 +1,5 @@
|
||||
import { WorkflowRunContext, WorkflowRunFlow } from '@/workflow/types/Workflow';
|
||||
import { getPreviousSteps } from '@/workflow/workflow-steps/utils/getWorkflowPreviousSteps';
|
||||
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
||||
|
||||
export const getWorkflowRunStepContext = ({
|
||||
@ -10,29 +11,26 @@ export const getWorkflowRunStepContext = ({
|
||||
context: WorkflowRunContext;
|
||||
flow: WorkflowRunFlow;
|
||||
}) => {
|
||||
const stepContext: Array<{ id: string; name: string; context: any }> = [];
|
||||
|
||||
if (stepId === TRIGGER_STEP_ID) {
|
||||
return stepContext;
|
||||
return [];
|
||||
}
|
||||
|
||||
stepContext.push({
|
||||
id: TRIGGER_STEP_ID,
|
||||
name: flow.trigger.name ?? 'Trigger',
|
||||
context: context[TRIGGER_STEP_ID],
|
||||
});
|
||||
const previousSteps = getPreviousSteps(flow.steps, stepId);
|
||||
|
||||
for (const step of flow.steps) {
|
||||
if (step.id === stepId) {
|
||||
break;
|
||||
}
|
||||
|
||||
stepContext.push({
|
||||
const previousStepsContext = previousSteps.map((step) => {
|
||||
return {
|
||||
id: step.id,
|
||||
name: step.name,
|
||||
context: context[step.id],
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return stepContext;
|
||||
return [
|
||||
{
|
||||
id: TRIGGER_STEP_ID,
|
||||
name: flow.trigger.name ?? 'Trigger',
|
||||
context: context[TRIGGER_STEP_ID],
|
||||
},
|
||||
...previousStepsContext,
|
||||
];
|
||||
};
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow';
|
||||
import { stepsOutputSchemaFamilySelector } from '@/workflow/states/selectors/stepsOutputSchemaFamilySelector';
|
||||
import { useWorkflowSelectedNodeOrThrow } from '@/workflow/workflow-diagram/hooks/useWorkflowSelectedNodeOrThrow';
|
||||
import { getPreviousSteps } from '@/workflow/workflow-steps/utils/getWorkflowPreviousSteps';
|
||||
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
||||
import {
|
||||
OutputSchema,
|
||||
@ -18,17 +19,12 @@ export const useAvailableVariablesInWorkflowStep = ({
|
||||
}): StepOutputSchema[] => {
|
||||
const workflowSelectedNode = useWorkflowSelectedNodeOrThrow();
|
||||
const flow = useFlowOrThrow();
|
||||
|
||||
const steps = flow.steps ?? [];
|
||||
|
||||
const previousStepIds: string[] = [];
|
||||
|
||||
for (const step of steps) {
|
||||
if (step.id === workflowSelectedNode) {
|
||||
break;
|
||||
}
|
||||
previousStepIds.push(step.id);
|
||||
}
|
||||
const previousStepIds: string[] = getPreviousSteps(
|
||||
steps,
|
||||
workflowSelectedNode,
|
||||
).map((step) => step.id);
|
||||
|
||||
const availableStepsOutputSchema: StepOutputSchema[] = useRecoilValue(
|
||||
stepsOutputSchemaFamilySelector({
|
||||
|
||||
Reference in New Issue
Block a user