8723 workflow add editor in serverless function code step (#8805)

- create a serverless function when creating a new workflow code step
- add code editor in workflow code step
- move workflowVersion steps management from frontend to backend
  - add a custom resolver for workflow-version management
  - fix optimistic rendering on frontend
- fix css
- delete serverless function when deleting workflow code step

TODO
- Don't update serverlessFunction if no code change
- Factorize what can be between crud trigger and crud step
- Publish serverless version when activating workflow
- delete serverless functions when deleting workflow or workflowVersion
- fix optimistic rendering for code updates
- Unify CRUD types

<img width="1279" alt="image"
src="https://github.com/user-attachments/assets/3d97ee9f-4b96-4abc-9d36-5c0280058be4">
This commit is contained in:
martmull
2024-12-03 09:41:13 +01:00
committed by GitHub
parent 9d7632cb4f
commit d0ff1ffd5f
75 changed files with 2192 additions and 1527 deletions

View File

@ -3,10 +3,9 @@ import { renderHook } from '@testing-library/react';
import { useCreateStep } from '../useCreateStep';
const mockOpenRightDrawer = jest.fn();
const mockUpdateOneWorkflowVersion = jest.fn();
const mockCreateNewWorkflowVersion = jest.fn();
const mockComputeStepOutputSchema = jest.fn().mockResolvedValue({
data: { computeStepOutputSchema: { type: 'object' } },
const mockCreateWorkflowVersionStep = jest.fn().mockResolvedValue({
data: { createWorkflowVersionStep: { id: '1' } },
});
jest.mock('recoil', () => ({
@ -15,19 +14,15 @@ jest.mock('recoil', () => ({
atom: (params: any) => params,
}));
jest.mock('@/workflow/states/workflowCreateStepFromParentStepIdState', () => ({
workflowCreateStepFromParentStepIdState: 'mockAtomState',
}));
jest.mock('@/ui/layout/right-drawer/hooks/useRightDrawer', () => ({
useRightDrawer: () => ({
openRightDrawer: mockOpenRightDrawer,
}),
}));
jest.mock('@/object-record/hooks/useUpdateOneRecord', () => ({
useUpdateOneRecord: () => ({
updateOneRecord: mockUpdateOneWorkflowVersion,
jest.mock('@/workflow/hooks/useCreateWorkflowVersionStep', () => ({
useCreateWorkflowVersionStep: () => ({
createWorkflowVersionStep: mockCreateWorkflowVersionStep,
}),
}));
@ -37,24 +32,6 @@ jest.mock('@/workflow/hooks/useCreateNewWorkflowVersion', () => ({
}),
}));
jest.mock('@/workflow/hooks/useComputeStepOutputSchema', () => ({
useComputeStepOutputSchema: () => ({
computeStepOutputSchema: mockComputeStepOutputSchema,
}),
}));
jest.mock('@/object-metadata/hooks/useFilteredObjectMetadataItems', () => ({
useFilteredObjectMetadataItems: () => ({
activeObjectMetadataItems: [],
}),
}));
jest.mock('@/workflow/utils/insertStep', () => ({
insertStep: jest
.fn()
.mockImplementation(({ steps, stepToAdd }) => [...steps, stepToAdd]),
}));
describe('useCreateStep', () => {
const mockWorkflow = {
id: '123',
@ -75,7 +52,7 @@ describe('useCreateStep', () => {
);
await result.current.createStep('CODE');
expect(mockUpdateOneWorkflowVersion).toHaveBeenCalled();
expect(mockCreateWorkflowVersionStep).toHaveBeenCalled();
expect(mockOpenRightDrawer).toHaveBeenCalled();
});
});

View File

@ -0,0 +1,62 @@
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
import { renderHook } from '@testing-library/react';
import { useDeleteStep } from '@/workflow/hooks/useDeleteStep';
const mockCloseRightDrawer = jest.fn();
const mockCreateNewWorkflowVersion = jest.fn();
const mockDeleteWorkflowVersionStep = jest.fn();
const updateOneRecordMock = jest.fn();
jest.mock('@/object-record/hooks/useUpdateOneRecord', () => ({
useUpdateOneRecord: () => ({
updateOneRecord: updateOneRecordMock,
}),
}));
jest.mock('recoil', () => ({
useRecoilValue: () => 'parent-step-id',
useSetRecoilState: () => jest.fn(),
atom: (params: any) => params,
}));
jest.mock('@/ui/layout/right-drawer/hooks/useRightDrawer', () => ({
useRightDrawer: () => ({
closeRightDrawer: mockCloseRightDrawer,
}),
}));
jest.mock('@/workflow/hooks/useDeleteWorkflowVersionStep', () => ({
useDeleteWorkflowVersionStep: () => ({
deleteWorkflowVersionStep: mockDeleteWorkflowVersionStep,
}),
}));
jest.mock('@/workflow/hooks/useCreateNewWorkflowVersion', () => ({
useCreateNewWorkflowVersion: () => ({
createNewWorkflowVersion: mockCreateNewWorkflowVersion,
}),
}));
describe('useDeleteStep', () => {
const mockWorkflow = {
id: '123',
currentVersion: {
id: '456',
status: 'DRAFT',
steps: [],
trigger: { type: 'manual' },
},
versions: [],
};
it('should delete step in draft version', async () => {
const { result } = renderHook(() =>
useDeleteStep({
workflow: mockWorkflow as unknown as WorkflowWithCurrentVersion,
}),
);
await result.current.deleteStep('1');
expect(mockDeleteWorkflowVersionStep).toHaveBeenCalled();
});
});

View File

@ -0,0 +1,62 @@
import { renderHook } from '@testing-library/react';
import { useGetUpdatableWorkflowVersion } from '@/workflow/hooks/useGetUpdatableWorkflowVersion';
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
const mockCreateNewWorkflowVersion = jest.fn().mockResolvedValue({
id: '457',
name: 'toto',
createdAt: '2024-07-03T20:03:35.064Z',
updatedAt: '2024-07-03T20:03:35.064Z',
workflowId: '123',
__typename: 'WorkflowVersion',
status: 'DRAFT',
steps: [],
trigger: null,
});
jest.mock('@/workflow/hooks/useCreateNewWorkflowVersion', () => ({
useCreateNewWorkflowVersion: () => ({
createNewWorkflowVersion: mockCreateNewWorkflowVersion,
}),
}));
describe('useGetUpdatableWorkflowVersion', () => {
const mockWorkflow = (status: 'ACTIVE' | 'DRAFT') =>
({
id: '123',
__typename: 'Workflow',
statuses: [],
lastPublishedVersionId: '1',
name: 'toto',
versions: [],
currentVersion: {
id: '456',
name: 'toto',
createdAt: '2024-07-03T20:03:35.064Z',
updatedAt: '2024-07-03T20:03:35.064Z',
workflowId: '123',
__typename: 'WorkflowVersion',
status,
steps: [],
trigger: null,
},
}) as WorkflowWithCurrentVersion;
it('should not create workflow version if draft version exists', async () => {
const { result } = renderHook(() => useGetUpdatableWorkflowVersion());
const workflowVersion = await result.current.getUpdatableWorkflowVersion(
mockWorkflow('DRAFT'),
);
expect(mockCreateNewWorkflowVersion).not.toHaveBeenCalled();
expect(workflowVersion.id === '456');
});
it('should create workflow version if no draft version exists', async () => {
const { result } = renderHook(() => useGetUpdatableWorkflowVersion());
const workflowVersion = await result.current.getUpdatableWorkflowVersion(
mockWorkflow('ACTIVE'),
);
expect(mockCreateNewWorkflowVersion).toHaveBeenCalled();
expect(workflowVersion.id === '457');
});
});

View File

@ -0,0 +1,66 @@
import { renderHook, act } from '@testing-library/react';
import { useTriggerNodeSelection } from '@/workflow/hooks/useTriggerNodeSelection';
import { RecoilRoot, useRecoilState } from 'recoil';
import { useReactFlow } from '@xyflow/react';
import { workflowDiagramTriggerNodeSelectionState } from '@/workflow/states/workflowDiagramTriggerNodeSelectionState';
jest.mock('@xyflow/react', () => ({
useReactFlow: jest.fn(),
}));
const wrapper = ({ children }: { children: React.ReactNode }) => (
<RecoilRoot>{children}</RecoilRoot>
);
describe('useTriggerNodeSelection', () => {
const mockUpdateNode = jest.fn();
beforeEach(() => {
(useReactFlow as jest.Mock).mockReturnValue({
updateNode: mockUpdateNode,
});
});
afterEach(() => {
jest.clearAllMocks();
});
it('should trigger node selection', () => {
const { result } = renderHook(
() => {
const [
workflowDiagramTriggerNodeSelection,
setWorkflowDiagramTriggerNodeSelection,
] = useRecoilState(workflowDiagramTriggerNodeSelectionState);
useTriggerNodeSelection();
return {
workflowDiagramTriggerNodeSelection,
setWorkflowDiagramTriggerNodeSelection,
};
},
{
wrapper,
},
);
const mockNodeId = 'test-node-id';
act(() => {
result.current.setWorkflowDiagramTriggerNodeSelection(mockNodeId);
});
expect(mockUpdateNode).toHaveBeenCalledWith(mockNodeId, { selected: true });
expect(result.current.workflowDiagramTriggerNodeSelection).toBeUndefined();
});
it('should not trigger update if state is not defined', () => {
renderHook(() => useTriggerNodeSelection(), {
wrapper,
});
// Ensure updateNode is not called when state is undefined
expect(mockUpdateNode).not.toHaveBeenCalled();
});
});

View File

@ -0,0 +1,69 @@
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
import { renderHook } from '@testing-library/react';
import { useUpdateStep } from '@/workflow/hooks/useUpdateStep';
const mockCreateNewWorkflowVersion = jest.fn();
const mockUpdateWorkflowVersionStep = jest.fn();
jest.mock('recoil', () => ({
useRecoilValue: () => 'parent-step-id',
useSetRecoilState: () => jest.fn(),
atom: (params: any) => params,
}));
jest.mock('@/workflow/hooks/useUpdateWorkflowVersionStep', () => ({
useUpdateWorkflowVersionStep: () => ({
updateWorkflowVersionStep: mockUpdateWorkflowVersionStep,
}),
}));
jest.mock('@/workflow/hooks/useCreateNewWorkflowVersion', () => ({
useCreateNewWorkflowVersion: () => ({
createNewWorkflowVersion: mockCreateNewWorkflowVersion,
}),
}));
describe('useUpdateStep', () => {
const mockWorkflow = {
id: '123',
currentVersion: {
id: '456',
status: 'DRAFT',
steps: [],
trigger: { type: 'manual' },
},
versions: [],
};
it('should update step in draft version', async () => {
const { result } = renderHook(() =>
useUpdateStep({
workflow: mockWorkflow as unknown as WorkflowWithCurrentVersion,
}),
);
await result.current.updateStep({
id: '1',
name: 'name',
valid: true,
type: 'CODE',
settings: {
input: {
serverlessFunctionId: 'id',
serverlessFunctionVersion: '1',
serverlessFunctionInput: {},
},
outputSchema: {},
errorHandlingOptions: {
retryOnFailure: {
value: true,
},
continueOnFailure: {
value: true,
},
},
},
});
expect(mockUpdateWorkflowVersionStep).toHaveBeenCalled();
});
});

View File

@ -67,5 +67,5 @@ export const useAllActiveWorkflowVersions = ({
};
}
return { records };
return { records: records.filter((record) => isDefined(record.workflow)) };
};

View File

@ -8,13 +8,13 @@ export const useCreateNewWorkflowVersion = () => {
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
});
const createNewWorkflowVersion = (
const createNewWorkflowVersion = async (
workflowVersionData: Pick<
WorkflowVersion,
'workflowId' | 'name' | 'status' | 'trigger' | 'steps'
>,
) => {
return createOneWorkflowVersion(workflowVersionData);
return await createOneWorkflowVersion(workflowVersionData);
};
return {

View File

@ -1,23 +1,16 @@
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { useComputeStepOutputSchema } from '@/workflow/hooks/useComputeStepOutputSchema';
import { useCreateNewWorkflowVersion } from '@/workflow/hooks/useCreateNewWorkflowVersion';
import { workflowCreateStepFromParentStepIdState } from '@/workflow/states/workflowCreateStepFromParentStepIdState';
import { workflowDiagramTriggerNodeSelectionState } from '@/workflow/states/workflowDiagramTriggerNodeSelectionState';
import { workflowSelectedNodeState } from '@/workflow/states/workflowSelectedNodeState';
import {
WorkflowStep,
WorkflowStepType,
WorkflowVersion,
WorkflowWithCurrentVersion,
} from '@/workflow/types/Workflow';
import { getStepDefaultDefinition } from '@/workflow/utils/getStepDefaultDefinition';
import { insertStep } from '@/workflow/utils/insertStep';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-ui';
import { useCreateWorkflowVersionStep } from '@/workflow/hooks/useCreateWorkflowVersionStep';
import { useGetUpdatableWorkflowVersion } from '@/workflow/hooks/useGetUpdatableWorkflowVersion';
export const useCreateStep = ({
workflow,
@ -25,6 +18,7 @@ export const useCreateStep = ({
workflow: WorkflowWithCurrentVersion;
}) => {
const { openRightDrawer } = useRightDrawer();
const { createWorkflowVersionStep } = useCreateWorkflowVersionStep();
const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState);
const workflowCreateStepFromParentStepId = useRecoilValue(
@ -35,82 +29,27 @@ export const useCreateStep = ({
workflowDiagramTriggerNodeSelectionState,
);
const { updateOneRecord: updateOneWorkflowVersion } =
useUpdateOneRecord<WorkflowVersion>({
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
});
const { createNewWorkflowVersion } = useCreateNewWorkflowVersion();
const { computeStepOutputSchema } = useComputeStepOutputSchema();
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
const insertNodeAndSave = async ({
parentNodeId,
nodeToAdd,
}: {
parentNodeId: string;
nodeToAdd: WorkflowStep;
}) => {
const currentVersion = workflow.currentVersion;
if (!isDefined(currentVersion)) {
throw new Error("Can't add a node when there is no current version.");
}
const updatedSteps = insertStep({
steps: currentVersion.steps ?? [],
parentStepId: parentNodeId,
stepToAdd: nodeToAdd,
});
if (workflow.currentVersion.status === 'DRAFT') {
await updateOneWorkflowVersion({
idToUpdate: currentVersion.id,
updateOneRecordInput: {
steps: updatedSteps,
},
});
return;
}
await createNewWorkflowVersion({
workflowId: workflow.id,
name: `v${workflow.versions.length + 1}`,
status: 'DRAFT',
trigger: workflow.currentVersion.trigger,
steps: updatedSteps,
});
};
const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion();
const createStep = async (newStepType: WorkflowStepType) => {
if (!isDefined(workflowCreateStepFromParentStepId)) {
throw new Error('Select a step to create a new step from first.');
}
const newStep = getStepDefaultDefinition({
type: newStepType,
activeObjectMetadataItems,
});
const workflowVersion = await getUpdatableWorkflowVersion(workflow);
const outputSchema = (
await computeStepOutputSchema({
step: newStep,
const createdStep = (
await createWorkflowVersionStep({
workflowVersionId: workflowVersion.id,
stepType: newStepType,
})
)?.data?.computeStepOutputSchema;
)?.data?.createWorkflowVersionStep;
newStep.settings = {
...newStep.settings,
outputSchema: outputSchema || {},
};
if (!createdStep) {
return;
}
await insertNodeAndSave({
parentNodeId: workflowCreateStepFromParentStepId,
nodeToAdd: newStep,
});
setWorkflowSelectedNode(newStep.id);
setWorkflowSelectedNode(createdStep.id);
openRightDrawer(RightDrawerPages.WorkflowStepEdit);
/**
@ -120,7 +59,7 @@ export const useCreateStep = ({
*
* Selecting the node will cause a right drawer to open in order to edit the step.
*/
setWorkflowDiagramTriggerNodeSelection(newStep.id);
setWorkflowDiagramTriggerNodeSelection(createdStep.id);
};
return {

View File

@ -0,0 +1,64 @@
import { useApolloClient, useMutation } from '@apollo/client';
import {
CreateWorkflowVersionStepMutation,
CreateWorkflowVersionStepMutationVariables,
CreateWorkflowVersionStepInput,
} from '~/generated/graphql';
import { CREATE_WORKFLOW_VERSION_STEP } from '@/workflow/graphql/mutations/createWorkflowVersionStep';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { isDefined } from 'twenty-ui';
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
import { WorkflowVersion } from '@/workflow/types/Workflow';
export const useCreateWorkflowVersionStep = () => {
const apolloClient = useApolloClient();
const { objectMetadataItems } = useObjectMetadataItems();
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
});
const getRecordFromCache = useGetRecordFromCache({
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
});
const [mutate] = useMutation<
CreateWorkflowVersionStepMutation,
CreateWorkflowVersionStepMutationVariables
>(CREATE_WORKFLOW_VERSION_STEP, {
client: apolloClient,
});
const createWorkflowVersionStep = async (
input: CreateWorkflowVersionStepInput,
) => {
const result = await mutate({
variables: { input },
});
const createdStep = result?.data?.createWorkflowVersionStep;
if (!isDefined(createdStep)) {
return;
}
const cachedRecord = getRecordFromCache<WorkflowVersion>(
input.workflowVersionId,
);
if (!cachedRecord) {
return;
}
const newCachedRecord = {
...cachedRecord,
steps: [...(cachedRecord.steps || []), createdStep],
};
updateRecordFromCache({
objectMetadataItems,
objectMetadataItem,
cache: apolloClient.cache,
record: newCachedRecord,
});
return result;
};
return { createWorkflowVersionStep };
};

View File

@ -1,81 +0,0 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { TRIGGER_STEP_ID } from '@/workflow/constants/TriggerStepId';
import { useCreateNewWorkflowVersion } from '@/workflow/hooks/useCreateNewWorkflowVersion';
import {
WorkflowVersion,
WorkflowWithCurrentVersion,
} from '@/workflow/types/Workflow';
import { removeStep } from '@/workflow/utils/removeStep';
export const useDeleteOneStep = ({
stepId,
workflow,
}: {
stepId: string;
workflow: WorkflowWithCurrentVersion;
}) => {
const { closeRightDrawer } = useRightDrawer();
const { updateOneRecord: updateOneWorkflowVersion } =
useUpdateOneRecord<WorkflowVersion>({
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
});
const { createNewWorkflowVersion } = useCreateNewWorkflowVersion();
const deleteOneStep = async () => {
closeRightDrawer();
if (workflow.currentVersion.status !== 'DRAFT') {
const newVersionName = `v${workflow.versions.length + 1}`;
if (stepId === TRIGGER_STEP_ID) {
await createNewWorkflowVersion({
workflowId: workflow.id,
name: newVersionName,
status: 'DRAFT',
trigger: null,
steps: workflow.currentVersion.steps,
});
} else {
await createNewWorkflowVersion({
workflowId: workflow.id,
name: newVersionName,
status: 'DRAFT',
trigger: workflow.currentVersion.trigger,
steps: removeStep({
steps: workflow.currentVersion.steps ?? [],
stepId,
}),
});
}
return;
}
if (stepId === TRIGGER_STEP_ID) {
await updateOneWorkflowVersion({
idToUpdate: workflow.currentVersion.id,
updateOneRecordInput: {
trigger: null,
},
});
} else {
await updateOneWorkflowVersion({
idToUpdate: workflow.currentVersion.id,
updateOneRecordInput: {
steps: removeStep({
steps: workflow.currentVersion.steps ?? [],
stepId,
}),
},
});
}
};
return {
deleteOneStep,
};
};

View File

@ -0,0 +1,47 @@
import {
WorkflowWithCurrentVersion,
WorkflowVersion,
} from '@/workflow/types/Workflow';
import { useDeleteWorkflowVersionStep } from '@/workflow/hooks/useDeleteWorkflowVersionStep';
import { useGetUpdatableWorkflowVersion } from '@/workflow/hooks/useGetUpdatableWorkflowVersion';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { TRIGGER_STEP_ID } from '@/workflow/constants/TriggerStepId';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
export const useDeleteStep = ({
workflow,
}: {
workflow: WorkflowWithCurrentVersion;
}) => {
const { deleteWorkflowVersionStep } = useDeleteWorkflowVersionStep();
const { updateOneRecord: updateOneWorkflowVersion } =
useUpdateOneRecord<WorkflowVersion>({
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
});
const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion();
const { closeRightDrawer } = useRightDrawer();
const deleteStep = async (stepId: string) => {
closeRightDrawer();
const workflowVersion = await getUpdatableWorkflowVersion(workflow);
if (stepId === TRIGGER_STEP_ID) {
await updateOneWorkflowVersion({
idToUpdate: workflow.currentVersion.id,
updateOneRecordInput: {
trigger: null,
},
});
return;
}
await deleteWorkflowVersionStep({
workflowVersionId: workflowVersion.id,
stepId,
});
};
return {
deleteStep,
};
};

View File

@ -0,0 +1,64 @@
import { useApolloClient, useMutation } from '@apollo/client';
import {
DeleteWorkflowVersionStepMutation,
DeleteWorkflowVersionStepMutationVariables,
DeleteWorkflowVersionStepInput,
WorkflowAction,
} from '~/generated/graphql';
import { DELETE_WORKFLOW_VERSION_STEP } from '@/workflow/graphql/mutations/deleteWorkflowVersionStep';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { isDefined } from 'twenty-ui';
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
import { WorkflowVersion } from '@/workflow/types/Workflow';
export const useDeleteWorkflowVersionStep = () => {
const apolloClient = useApolloClient();
const { objectMetadataItems } = useObjectMetadataItems();
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
});
const getRecordFromCache = useGetRecordFromCache({
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
});
const [mutate] = useMutation<
DeleteWorkflowVersionStepMutation,
DeleteWorkflowVersionStepMutationVariables
>(DELETE_WORKFLOW_VERSION_STEP, {
client: apolloClient,
});
const deleteWorkflowVersionStep = async (
input: DeleteWorkflowVersionStepInput,
) => {
const result = await mutate({ variables: { input } });
const deletedStep = result?.data?.deleteWorkflowVersionStep;
if (!isDefined(deletedStep)) {
return;
}
const cachedRecord = getRecordFromCache<WorkflowVersion>(
input.workflowVersionId,
);
if (!cachedRecord) {
return;
}
const newCachedRecord = {
...cachedRecord,
steps: (cachedRecord.steps || []).filter(
(step: WorkflowAction) => step.id !== deletedStep.id,
),
};
updateRecordFromCache({
objectMetadataItems,
objectMetadataItem,
cache: apolloClient.cache,
record: newCachedRecord,
});
};
return { deleteWorkflowVersionStep };
};

View File

@ -0,0 +1,23 @@
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
import { useCreateNewWorkflowVersion } from '@/workflow/hooks/useCreateNewWorkflowVersion';
export const useGetUpdatableWorkflowVersion = () => {
const { createNewWorkflowVersion } = useCreateNewWorkflowVersion();
const getUpdatableWorkflowVersion = async (
workflow: WorkflowWithCurrentVersion,
) => {
if (workflow.currentVersion.status === 'DRAFT') {
return workflow.currentVersion;
}
return await createNewWorkflowVersion({
workflowId: workflow.id,
name: `v${workflow.versions.length + 1}`,
status: 'DRAFT',
trigger: workflow.currentVersion.trigger,
steps: workflow.currentVersion.steps,
});
};
return { getUpdatableWorkflowVersion };
};

View File

@ -5,18 +5,16 @@ import {
} from '@/workflow/types/WorkflowDiagram';
import { useReactFlow } from '@xyflow/react';
import { useEffect } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { useRecoilState } from 'recoil';
import { isDefined } from 'twenty-ui';
export const useTriggerNodeSelection = () => {
const reactflow = useReactFlow<WorkflowDiagramNode, WorkflowDiagramEdge>();
const workflowDiagramTriggerNodeSelection = useRecoilValue(
workflowDiagramTriggerNodeSelectionState,
);
const setWorkflowDiagramTriggerNodeSelection = useSetRecoilState(
workflowDiagramTriggerNodeSelectionState,
);
const [
workflowDiagramTriggerNodeSelection,
setWorkflowDiagramTriggerNodeSelection,
] = useRecoilState(workflowDiagramTriggerNodeSelectionState);
useEffect(() => {
if (!isDefined(workflowDiagramTriggerNodeSelection)) {

View File

@ -0,0 +1,32 @@
import {
WorkflowStep,
WorkflowWithCurrentVersion,
} from '@/workflow/types/Workflow';
import { isDefined } from 'twenty-ui';
import { useGetUpdatableWorkflowVersion } from '@/workflow/hooks/useGetUpdatableWorkflowVersion';
import { useUpdateWorkflowVersionStep } from '@/workflow/hooks/useUpdateWorkflowVersionStep';
export const useUpdateStep = ({
workflow,
}: {
workflow: WorkflowWithCurrentVersion;
}) => {
const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion();
const { updateWorkflowVersionStep } = useUpdateWorkflowVersionStep();
const updateStep = async <T extends WorkflowStep>(updatedStep: T) => {
if (!isDefined(workflow.currentVersion)) {
throw new Error('Can not update an undefined workflow version.');
}
const workflowVersion = await getUpdatableWorkflowVersion(workflow);
await updateWorkflowVersionStep({
workflowVersionId: workflowVersion.id,
step: updatedStep,
});
};
return {
updateStep,
};
};

View File

@ -1,73 +1,69 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { useCreateNewWorkflowVersion } from '@/workflow/hooks/useCreateNewWorkflowVersion';
import { useApolloClient, useMutation } from '@apollo/client';
import {
WorkflowStep,
WorkflowVersion,
WorkflowWithCurrentVersion,
} from '@/workflow/types/Workflow';
import { replaceStep } from '@/workflow/utils/replaceStep';
UpdateWorkflowVersionStepInput,
UpdateWorkflowVersionStepMutation,
UpdateWorkflowVersionStepMutationVariables,
WorkflowAction,
} from '~/generated/graphql';
import { UPDATE_WORKFLOW_VERSION_STEP } from '@/workflow/graphql/mutations/updateWorkflowVersionStep';
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { isDefined } from 'twenty-ui';
import { useComputeStepOutputSchema } from '@/workflow/hooks/useComputeStepOutputSchema';
import { WorkflowVersion } from '@/workflow/types/Workflow';
export const useUpdateWorkflowVersionStep = ({
workflow,
stepId,
}: {
workflow: WorkflowWithCurrentVersion;
stepId: string;
}) => {
const { updateOneRecord: updateOneWorkflowVersion } =
useUpdateOneRecord<WorkflowVersion>({
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
});
const { createNewWorkflowVersion } = useCreateNewWorkflowVersion();
const { computeStepOutputSchema } = useComputeStepOutputSchema();
const updateStep = async <T extends WorkflowStep>(updatedStep: T) => {
if (!isDefined(workflow.currentVersion)) {
throw new Error('Can not update an undefined workflow version.');
}
const outputSchema = (
await computeStepOutputSchema({
step: updatedStep,
})
)?.data?.computeStepOutputSchema;
updatedStep.settings = {
...updatedStep.settings,
outputSchema: outputSchema || {},
};
const updatedSteps = replaceStep({
steps: workflow.currentVersion.steps ?? [],
stepId,
stepToReplace: updatedStep,
});
if (workflow.currentVersion.status === 'DRAFT') {
await updateOneWorkflowVersion({
idToUpdate: workflow.currentVersion.id,
updateOneRecordInput: {
steps: updatedSteps,
},
});
export const useUpdateWorkflowVersionStep = () => {
const apolloClient = useApolloClient();
const { objectMetadataItems } = useObjectMetadataItems();
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
});
const getRecordFromCache = useGetRecordFromCache({
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
});
const [mutate] = useMutation<
UpdateWorkflowVersionStepMutation,
UpdateWorkflowVersionStepMutationVariables
>(UPDATE_WORKFLOW_VERSION_STEP, {
client: apolloClient,
});
const updateWorkflowVersionStep = async (
input: UpdateWorkflowVersionStepInput,
) => {
const result = await mutate({ variables: { input } });
const updatedStep = result?.data?.updateWorkflowVersionStep;
if (!isDefined(updatedStep)) {
return;
}
await createNewWorkflowVersion({
workflowId: workflow.id,
name: `v${workflow.versions.length + 1}`,
status: 'DRAFT',
trigger: workflow.currentVersion.trigger,
steps: updatedSteps,
const cachedRecord = getRecordFromCache<WorkflowVersion>(
input.workflowVersionId,
);
if (!cachedRecord) {
return;
}
const newCachedRecord = {
...cachedRecord,
steps: (cachedRecord.steps || []).map((step: WorkflowAction) => {
if (step.id === updatedStep.id) {
return updatedStep;
}
return step;
}),
};
updateRecordFromCache({
objectMetadataItems,
objectMetadataItem,
cache: apolloClient.cache,
record: newCachedRecord,
});
return result;
};
return {
updateStep,
};
return { updateWorkflowVersionStep };
};

View File

@ -1,6 +1,5 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { useCreateNewWorkflowVersion } from '@/workflow/hooks/useCreateNewWorkflowVersion';
import {
WorkflowTrigger,
WorkflowVersion,
@ -8,6 +7,7 @@ import {
} from '@/workflow/types/Workflow';
import { isDefined } from 'twenty-ui';
import { useComputeStepOutputSchema } from '@/workflow/hooks/useComputeStepOutputSchema';
import { useGetUpdatableWorkflowVersion } from '@/workflow/hooks/useGetUpdatableWorkflowVersion';
export const useUpdateWorkflowVersionTrigger = ({
workflow,
@ -19,7 +19,7 @@ export const useUpdateWorkflowVersionTrigger = ({
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
});
const { createNewWorkflowVersion } = useCreateNewWorkflowVersion();
const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion();
const { computeStepOutputSchema } = useComputeStepOutputSchema();
@ -28,34 +28,24 @@ export const useUpdateWorkflowVersionTrigger = ({
throw new Error('Can not update an undefined workflow version.');
}
if (workflow.currentVersion.status === 'DRAFT') {
const outputSchema = (
await computeStepOutputSchema({
step: updatedTrigger,
})
)?.data?.computeStepOutputSchema;
const workflowVersion = await getUpdatableWorkflowVersion(workflow);
updatedTrigger.settings = {
...updatedTrigger.settings,
outputSchema: outputSchema || {},
};
const outputSchema = (
await computeStepOutputSchema({
step: updatedTrigger,
})
)?.data?.computeStepOutputSchema;
await updateOneWorkflowVersion({
idToUpdate: workflow.currentVersion.id,
updateOneRecordInput: {
trigger: updatedTrigger,
},
});
updatedTrigger.settings = {
...updatedTrigger.settings,
outputSchema: outputSchema || {},
};
return;
}
await createNewWorkflowVersion({
workflowId: workflow.id,
name: `v${workflow.versions.length + 1}`,
status: 'DRAFT',
trigger: updatedTrigger,
steps: workflow.currentVersion.steps,
await updateOneWorkflowVersion({
idToUpdate: workflowVersion.id,
updateOneRecordInput: {
trigger: updatedTrigger,
},
});
};

View File

@ -1,9 +1,7 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import {
Workflow,
WorkflowVersion,
WorkflowWithCurrentVersion,
} from '@/workflow/types/Workflow';
import { useMemo } from 'react';
@ -19,40 +17,25 @@ export const useWorkflowWithCurrentVersion = (
id: true,
name: true,
statuses: true,
versions: {
totalCount: true,
},
versions: true,
},
skip: !isDefined(workflowId),
});
const {
records: mostRecentWorkflowVersions,
loading: loadingMostRecentWorkflowVersions,
} = useFindManyRecords<WorkflowVersion>({
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
filter: {
workflowId: {
eq: workflowId,
},
},
orderBy: [
{
createdAt: 'DescNullsLast',
},
],
skip: !isDefined(workflowId),
});
const workflowWithCurrentVersion = useMemo(() => {
if (!isDefined(workflow) || loadingMostRecentWorkflowVersions) {
return useMemo(() => {
if (!isDefined(workflow)) {
return undefined;
}
const draftVersion = mostRecentWorkflowVersions.find(
const draftVersion = workflow.versions.find(
(workflowVersion) => workflowVersion.status === 'DRAFT',
);
const latestVersion = mostRecentWorkflowVersions[0];
const workflowVersions = [...workflow.versions];
workflowVersions.sort((a, b) => (a.createdAt > b.createdAt ? -1 : 1));
const latestVersion = workflowVersions[0];
const currentVersion = draftVersion ?? latestVersion;
@ -64,7 +47,5 @@ export const useWorkflowWithCurrentVersion = (
...workflow,
currentVersion,
};
}, [loadingMostRecentWorkflowVersions, mostRecentWorkflowVersions, workflow]);
return workflowWithCurrentVersion;
}, [workflow]);
};