From 8b345c9fd2512ce3c9ff41418207798c0051f413 Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Wed, 17 Sep 2025 17:36:42 +0200 Subject: [PATCH] Change workflow node type (#14570) - Update step endpoint now supports the case where node type is updated - Added a new page for edition that re-uses the existing select action component Additional work: - moved submitForm function to workflow runner service https://github.com/user-attachments/assets/1fcb0adc-fe17-41f9-b2ea-4f84318db860 --- .../constants/CommandMenuPagesConfig.tsx | 9 +- .../__tests__/useWorkflowCommandMenu.test.tsx | 29 +- .../hooks/useWorkflowCommandMenu.ts | 32 +- .../CommandMenuWorkflowSelectAction.tsx | 87 +++++- .../CommandMenuWorkflowCreateStep.tsx | 20 ++ .../CommandMenuWorkflowCreateStepContent.tsx} | 85 +----- .../CommandMenuWorkflowEditStepType.tsx | 20 ++ ...CommandMenuWorkflowEditStepTypeContent.tsx | 75 +++++ .../command-menu/types/CommandMenuPages.ts | 3 +- .../hooks/useStartNodeCreation.ts | 6 +- .../components/WorkflowActionFooter.tsx | 16 +- .../workflow-steps/hooks/useUpdateStep.ts | 10 +- .../decorators/WorkflowStepDecorator.tsx | 20 +- .../workflow-version-step.resolver.ts | 4 +- .../workflow/workflow-api.module.ts | 6 +- ...low-version-step.workspace-service.spec.ts | 13 +- .../workflow-version-step.module.ts | 9 +- ...workflow-version-step.workspace-service.ts | 275 ++++++++---------- .../workflow-run/workflow-run.module.ts | 6 +- .../workflow-runner/workflow-runner.module.ts | 4 +- .../workflow-runner.workspace-service.ts | 75 +++++ 21 files changed, 521 insertions(+), 283 deletions(-) create mode 100644 packages/twenty-front/src/modules/command-menu/pages/workflow/step/create/components/CommandMenuWorkflowCreateStep.tsx rename packages/twenty-front/src/modules/command-menu/pages/workflow/{action/components/CommandMenuWorkflowSelectActionContent.tsx => step/create/components/CommandMenuWorkflowCreateStepContent.tsx} (50%) create mode 100644 packages/twenty-front/src/modules/command-menu/pages/workflow/step/edit/components/CommandMenuWorkflowEditStepType.tsx create mode 100644 packages/twenty-front/src/modules/command-menu/pages/workflow/step/edit/components/CommandMenuWorkflowEditStepTypeContent.tsx diff --git a/packages/twenty-front/src/modules/command-menu/constants/CommandMenuPagesConfig.tsx b/packages/twenty-front/src/modules/command-menu/constants/CommandMenuPagesConfig.tsx index 7c318080d6..8a78fee849 100644 --- a/packages/twenty-front/src/modules/command-menu/constants/CommandMenuPagesConfig.tsx +++ b/packages/twenty-front/src/modules/command-menu/constants/CommandMenuPagesConfig.tsx @@ -10,8 +10,9 @@ import { CommandMenuMergeRecordPage } from '@/command-menu/pages/record-page/com import { CommandMenuRecordPage } from '@/command-menu/pages/record-page/components/CommandMenuRecordPage'; import { CommandMenuEditRichTextPage } from '@/command-menu/pages/rich-text-page/components/CommandMenuEditRichTextPage'; import { CommandMenuSearchRecordsPage } from '@/command-menu/pages/search/components/CommandMenuSearchRecordsPage'; -import { CommandMenuWorkflowSelectAction } from '@/command-menu/pages/workflow/action/components/CommandMenuWorkflowSelectAction'; +import { CommandMenuWorkflowCreateStep } from '@/command-menu/pages/workflow/step/create/components/CommandMenuWorkflowCreateStep'; import { CommandMenuWorkflowEditStep } from '@/command-menu/pages/workflow/step/edit/components/CommandMenuWorkflowEditStep'; +import { CommandMenuWorkflowEditStepType } from '@/command-menu/pages/workflow/step/edit/components/CommandMenuWorkflowEditStepType'; import { CommandMenuWorkflowRunViewStep } from '@/command-menu/pages/workflow/step/view-run/components/CommandMenuWorkflowRunViewStep'; import { CommandMenuWorkflowViewStep } from '@/command-menu/pages/workflow/step/view/components/CommandMenuWorkflowViewStep'; import { CommandMenuWorkflowSelectTriggerType } from '@/command-menu/pages/workflow/trigger-type/components/CommandMenuWorkflowSelectTriggerType'; @@ -31,10 +32,8 @@ export const COMMAND_MENU_PAGES_CONFIG = new Map< CommandMenuPages.WorkflowStepSelectTriggerType, , ], - [ - CommandMenuPages.WorkflowStepSelectAction, - , - ], + [CommandMenuPages.WorkflowStepCreate, ], + [CommandMenuPages.WorkflowStepEditType, ], [CommandMenuPages.WorkflowStepEdit, ], [CommandMenuPages.WorkflowStepView, ], [CommandMenuPages.WorkflowRunStepView, ], diff --git a/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useWorkflowCommandMenu.test.tsx b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useWorkflowCommandMenu.test.tsx index ae2d188934..97f289b9a6 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useWorkflowCommandMenu.test.tsx +++ b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useWorkflowCommandMenu.test.tsx @@ -62,8 +62,9 @@ const renderHooks = () => { () => { const { openWorkflowTriggerTypeInCommandMenu, - openStepSelectInCommandMenu, + openWorkflowCreateStepInCommandMenu, openWorkflowEditStepInCommandMenu, + openWorkflowEditStepTypeInCommandMenu, openWorkflowViewStepInCommandMenu, } = useWorkflowCommandMenu(); const commandMenuPage = useRecoilValue(commandMenuPageState); @@ -107,8 +108,9 @@ const renderHooks = () => { return { openWorkflowTriggerTypeInCommandMenu, - openStepSelectInCommandMenu, + openWorkflowCreateStepInCommandMenu, openWorkflowEditStepInCommandMenu, + openWorkflowEditStepTypeInCommandMenu, openWorkflowViewStepInCommandMenu, workflowId, workflowVersionId, @@ -152,23 +154,40 @@ describe('useWorkflowCommandMenu', () => { }); }); - it('should navigate to the workflow step select action page', () => { + it('should navigate to the workflow step create action page', () => { const { result } = renderHooks(); act(() => { - result.current.openStepSelectInCommandMenu('test-workflow-id'); + result.current.openWorkflowCreateStepInCommandMenu('test-workflow-id'); }); expect(result.current.workflowId).toBe('test-workflow-id'); expect(mockNavigateCommandMenu).toHaveBeenCalledWith({ - page: CommandMenuPages.WorkflowStepSelectAction, + page: CommandMenuPages.WorkflowStepCreate, pageTitle: t`Select Action`, pageIcon: IconSettingsAutomation, pageId: 'mocked-uuid', }); }); + it('should navigate to the workflow step edit type page', () => { + const { result } = renderHooks(); + + act(() => { + result.current.openWorkflowEditStepTypeInCommandMenu('test-workflow-id'); + }); + + expect(result.current.workflowId).toBe('test-workflow-id'); + + expect(mockNavigateCommandMenu).toHaveBeenCalledWith({ + page: CommandMenuPages.WorkflowStepEditType, + pageTitle: t`Select action`, + pageIcon: IconSettingsAutomation, + pageId: 'mocked-uuid', + }); + }); + it('should navigate to the workflow step edit page', () => { const { result } = renderHooks(); diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useWorkflowCommandMenu.ts b/packages/twenty-front/src/modules/command-menu/hooks/useWorkflowCommandMenu.ts index 292ff73410..fee19b16a8 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useWorkflowCommandMenu.ts +++ b/packages/twenty-front/src/modules/command-menu/hooks/useWorkflowCommandMenu.ts @@ -3,6 +3,7 @@ import { commandMenuWorkflowIdComponentState } from '@/command-menu/pages/workfl import { commandMenuWorkflowRunIdComponentState } from '@/command-menu/pages/workflow/states/commandMenuWorkflowRunIdComponentState'; import { commandMenuWorkflowVersionIdComponentState } from '@/command-menu/pages/workflow/states/commandMenuWorkflowVersionIdComponentState'; import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; +import { type WorkflowRunStepStatus } from '@/workflow/types/Workflow'; import { useSetInitialWorkflowRunRightDrawerTab } from '@/workflow/workflow-diagram/hooks/useSetInitialWorkflowRunRightDrawerTab'; import { t } from '@lingui/core/macro'; import { useRecoilCallback } from 'recoil'; @@ -12,7 +13,6 @@ import { IconSettingsAutomation, } from 'twenty-ui/display'; import { v4 } from 'uuid'; -import { type WorkflowRunStepStatus } from '@/workflow/types/Workflow'; export const useWorkflowCommandMenu = () => { const { navigateCommandMenu } = useNavigateCommandMenu(); @@ -42,7 +42,7 @@ export const useWorkflowCommandMenu = () => { [navigateCommandMenu], ); - const openStepSelectInCommandMenu = useRecoilCallback( + const openWorkflowCreateStepInCommandMenu = useRecoilCallback( ({ set }) => { return (workflowId: string) => { const pageId = v4(); @@ -55,7 +55,7 @@ export const useWorkflowCommandMenu = () => { ); navigateCommandMenu({ - page: CommandMenuPages.WorkflowStepSelectAction, + page: CommandMenuPages.WorkflowStepCreate, pageTitle: t`Select Action`, pageIcon: IconSettingsAutomation, pageId, @@ -88,6 +88,29 @@ export const useWorkflowCommandMenu = () => { [navigateCommandMenu], ); + const openWorkflowEditStepTypeInCommandMenu = useRecoilCallback( + ({ set }) => { + return (workflowId: string) => { + const pageId = v4(); + + set( + commandMenuWorkflowIdComponentState.atomFamily({ + instanceId: pageId, + }), + workflowId, + ); + + navigateCommandMenu({ + page: CommandMenuPages.WorkflowStepEditType, + pageTitle: t`Select action`, + pageIcon: IconSettingsAutomation, + pageId, + }); + }; + }, + [navigateCommandMenu], + ); + const openWorkflowViewStepInCommandMenu = useRecoilCallback( ({ set }) => { return ({ @@ -177,8 +200,9 @@ export const useWorkflowCommandMenu = () => { return { openWorkflowTriggerTypeInCommandMenu, - openStepSelectInCommandMenu, + openWorkflowCreateStepInCommandMenu, openWorkflowEditStepInCommandMenu, + openWorkflowEditStepTypeInCommandMenu, openWorkflowViewStepInCommandMenu, openWorkflowRunViewStepInCommandMenu, }; diff --git a/packages/twenty-front/src/modules/command-menu/pages/workflow/action/components/CommandMenuWorkflowSelectAction.tsx b/packages/twenty-front/src/modules/command-menu/pages/workflow/action/components/CommandMenuWorkflowSelectAction.tsx index 0b7bd7fa29..88ea2896ce 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/workflow/action/components/CommandMenuWorkflowSelectAction.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/workflow/action/components/CommandMenuWorkflowSelectAction.tsx @@ -1,20 +1,77 @@ -import { CommandMenuWorkflowSelectActionContent } from '@/command-menu/pages/workflow/action/components/CommandMenuWorkflowSelectActionContent'; -import { useCommandMenuWorkflowIdOrThrow } from '@/command-menu/pages/workflow/hooks/useCommandMenuWorkflowIdOrThrow'; -import { getWorkflowVisualizerComponentInstanceId } from '@/workflow/utils/getWorkflowVisualizerComponentInstanceId'; -import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext'; +import { WorkflowActionMenuItems } from '@/command-menu/pages/workflow/action/components/WorkflowActionMenuItems'; +import { type WorkflowActionType } from '@/workflow/types/Workflow'; +import { RightDrawerStepListContainer } from '@/workflow/workflow-steps/components/RightDrawerWorkflowSelectStepContainer'; +import { RightDrawerWorkflowSelectStepTitle } from '@/workflow/workflow-steps/components/RightDrawerWorkflowSelectStepTitle'; +import { AI_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/AiActions'; +import { CORE_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/CoreActions'; +import { HUMAN_INPUT_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/HumanInputActions'; +import { OTHER_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/OtherActions'; +import { RECORD_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/RecordActions'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; +import { useLingui } from '@lingui/react/macro'; +import { FeatureFlagKey } from '~/generated/graphql'; -export const CommandMenuWorkflowSelectAction = () => { - const workflowId = useCommandMenuWorkflowIdOrThrow(); +export const CommandMenuWorkflowSelectAction = ({ + onActionSelected, +}: { + onActionSelected: (actionType: WorkflowActionType) => void; +}) => { + const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED); + const isIteratorEnabled = useIsFeatureEnabled( + FeatureFlagKey.IS_WORKFLOW_ITERATOR_ENABLED, + ); + + const { t } = useLingui(); return ( - - - + + + {t`Data`} + + + + {isAiEnabled && ( + <> + + {t`AI`} + + + + )} + + + {t`Core`} + + + + + {t`Human Input`} + + + + {isIteratorEnabled && ( + <> + + {t`Others`} + + + + )} + ); }; diff --git a/packages/twenty-front/src/modules/command-menu/pages/workflow/step/create/components/CommandMenuWorkflowCreateStep.tsx b/packages/twenty-front/src/modules/command-menu/pages/workflow/step/create/components/CommandMenuWorkflowCreateStep.tsx new file mode 100644 index 0000000000..1b7669e8d9 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/workflow/step/create/components/CommandMenuWorkflowCreateStep.tsx @@ -0,0 +1,20 @@ +import { useCommandMenuWorkflowIdOrThrow } from '@/command-menu/pages/workflow/hooks/useCommandMenuWorkflowIdOrThrow'; +import { CommandMenuWorkflowCreateStepContent } from '@/command-menu/pages/workflow/step/create/components/CommandMenuWorkflowCreateStepContent'; +import { getWorkflowVisualizerComponentInstanceId } from '@/workflow/utils/getWorkflowVisualizerComponentInstanceId'; +import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext'; + +export const CommandMenuWorkflowCreateStep = () => { + const workflowId = useCommandMenuWorkflowIdOrThrow(); + + return ( + + + + ); +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/workflow/action/components/CommandMenuWorkflowSelectActionContent.tsx b/packages/twenty-front/src/modules/command-menu/pages/workflow/step/create/components/CommandMenuWorkflowCreateStepContent.tsx similarity index 50% rename from packages/twenty-front/src/modules/command-menu/pages/workflow/action/components/CommandMenuWorkflowSelectActionContent.tsx rename to packages/twenty-front/src/modules/command-menu/pages/workflow/step/create/components/CommandMenuWorkflowCreateStepContent.tsx index 757eb5cf54..5b61b8bcf4 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/workflow/action/components/CommandMenuWorkflowSelectActionContent.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/workflow/step/create/components/CommandMenuWorkflowCreateStepContent.tsx @@ -1,46 +1,34 @@ import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; -import { WorkflowActionMenuItems } from '@/command-menu/pages/workflow/action/components/WorkflowActionMenuItems'; +import { CommandMenuWorkflowSelectAction } from '@/command-menu/pages/workflow/action/components/CommandMenuWorkflowSelectAction'; import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; import { type WorkflowActionType } from '@/workflow/types/Workflow'; import { useCloseRightClickMenu } from '@/workflow/workflow-diagram/hooks/useCloseRightClickMenu'; -import { RightDrawerStepListContainer } from '@/workflow/workflow-steps/components/RightDrawerWorkflowSelectStepContainer'; -import { RightDrawerWorkflowSelectStepTitle } from '@/workflow/workflow-steps/components/RightDrawerWorkflowSelectStepTitle'; import { useCreateStep } from '@/workflow/workflow-steps/hooks/useCreateStep'; import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState'; -import { AI_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/AiActions'; -import { CORE_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/CoreActions'; -import { HUMAN_INPUT_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/HumanInputActions'; -import { OTHER_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/OtherActions'; -import { RECORD_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/RecordActions'; import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon'; -import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; -import { useLingui } from '@lingui/react/macro'; import { useSetRecoilState } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; import { useIcons } from 'twenty-ui/display'; -import { FeatureFlagKey } from '~/generated/graphql'; -export const CommandMenuWorkflowSelectActionContent = () => { +export const CommandMenuWorkflowCreateStepContent = () => { const { getIcon } = useIcons(); + const workflowVisualizerWorkflowId = useRecoilComponentValue( + workflowVisualizerWorkflowIdComponentState, + ); const { createStep } = useCreateStep(); + const { openWorkflowEditStepInCommandMenu } = useWorkflowCommandMenu(); const { closeRightClickMenu } = useCloseRightClickMenu(); - - const [workflowInsertStepIds, setWorkflowInsertStepIds] = - useRecoilComponentState(workflowInsertStepIdsComponentState); - const setCommandMenuNavigationStack = useSetRecoilState( commandMenuNavigationStackState, ); - const workflowVisualizerWorkflowId = useRecoilComponentValue( - workflowVisualizerWorkflowIdComponentState, - ); - const { openWorkflowEditStepInCommandMenu } = useWorkflowCommandMenu(); + const [workflowInsertStepIds, setWorkflowInsertStepIds] = + useRecoilComponentState(workflowInsertStepIdsComponentState); const handleCreateStep = async (actionType: WorkflowActionType) => { if (!isDefined(workflowVisualizerWorkflowId)) { @@ -81,62 +69,7 @@ export const CommandMenuWorkflowSelectActionContent = () => { ); }; - const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED); - const isIteratorEnabled = useIsFeatureEnabled( - FeatureFlagKey.IS_WORKFLOW_ITERATOR_ENABLED, - ); - - const { t } = useLingui(); - return ( - - - {t`Data`} - - - - {isAiEnabled && ( - <> - - {t`AI`} - - - - )} - - - {t`Core`} - - - - - {t`Human Input`} - - - - {isIteratorEnabled && ( - <> - - {t`Others`} - - - - )} - + ); }; diff --git a/packages/twenty-front/src/modules/command-menu/pages/workflow/step/edit/components/CommandMenuWorkflowEditStepType.tsx b/packages/twenty-front/src/modules/command-menu/pages/workflow/step/edit/components/CommandMenuWorkflowEditStepType.tsx new file mode 100644 index 0000000000..e80bf478f3 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/workflow/step/edit/components/CommandMenuWorkflowEditStepType.tsx @@ -0,0 +1,20 @@ +import { useCommandMenuWorkflowIdOrThrow } from '@/command-menu/pages/workflow/hooks/useCommandMenuWorkflowIdOrThrow'; +import { CommandMenuWorkflowEditStepTypeContent } from '@/command-menu/pages/workflow/step/edit/components/CommandMenuWorkflowEditStepTypeContent'; +import { getWorkflowVisualizerComponentInstanceId } from '@/workflow/utils/getWorkflowVisualizerComponentInstanceId'; +import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext'; + +export const CommandMenuWorkflowEditStepType = () => { + const workflowId = useCommandMenuWorkflowIdOrThrow(); + + return ( + + + + ); +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/workflow/step/edit/components/CommandMenuWorkflowEditStepTypeContent.tsx b/packages/twenty-front/src/modules/command-menu/pages/workflow/step/edit/components/CommandMenuWorkflowEditStepTypeContent.tsx new file mode 100644 index 0000000000..7285ac63a9 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/workflow/step/edit/components/CommandMenuWorkflowEditStepTypeContent.tsx @@ -0,0 +1,75 @@ +import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; +import { CommandMenuWorkflowSelectAction } from '@/command-menu/pages/workflow/action/components/CommandMenuWorkflowSelectAction'; +import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow'; +import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; +import { + type WorkflowAction, + type WorkflowActionType, +} from '@/workflow/types/Workflow'; +import { useCloseRightClickMenu } from '@/workflow/workflow-diagram/hooks/useCloseRightClickMenu'; +import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState'; +import { useUpdateStep } from '@/workflow/workflow-steps/hooks/useUpdateStep'; +import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon'; +import { useSetRecoilState } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; +import { useIcons } from 'twenty-ui/display'; + +export const CommandMenuWorkflowEditStepTypeContent = () => { + const { getIcon } = useIcons(); + const workflowSelectedNode = useRecoilComponentValue( + workflowSelectedNodeComponentState, + ); + const workflowVisualizerWorkflowId = useRecoilComponentValue( + workflowVisualizerWorkflowIdComponentState, + ); + const flow = useFlowOrThrow(); + + const { updateStep } = useUpdateStep(); + + const { openWorkflowEditStepInCommandMenu } = useWorkflowCommandMenu(); + const { closeRightClickMenu } = useCloseRightClickMenu(); + const setCommandMenuNavigationStack = useSetRecoilState( + commandMenuNavigationStackState, + ); + + const handleUpdateStepType = async (actionType: WorkflowActionType) => { + if (!isDefined(workflowVisualizerWorkflowId)) { + throw new Error( + 'Workflow ID must be configured for the edge when creating a step', + ); + } + + const existingStep = flow.steps?.find( + (step) => step.id === workflowSelectedNode, + ); + + if (!isDefined(existingStep)) { + throw new Error('Step not found'); + } + + const { updatedStep } = await updateStep({ + ...existingStep, + type: actionType, + } as WorkflowAction); + + if (!isDefined(updatedStep)) { + return; + } + + closeRightClickMenu(); + + setCommandMenuNavigationStack([]); + + openWorkflowEditStepInCommandMenu( + workflowVisualizerWorkflowId, + updatedStep.name, + getIcon(getActionIcon(updatedStep.type as WorkflowActionType)), + ); + }; + + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/command-menu/types/CommandMenuPages.ts b/packages/twenty-front/src/modules/command-menu/types/CommandMenuPages.ts index b6e55830b1..30169741b8 100644 --- a/packages/twenty-front/src/modules/command-menu/types/CommandMenuPages.ts +++ b/packages/twenty-front/src/modules/command-menu/types/CommandMenuPages.ts @@ -7,7 +7,8 @@ export enum CommandMenuPages { EditRichText = 'edit-rich-text', Copilot = 'copilot', WorkflowStepSelectTriggerType = 'workflow-step-select-trigger-type', - WorkflowStepSelectAction = 'workflow-step-select-action', + WorkflowStepCreate = 'workflow-step-create', + WorkflowStepEditType = 'workflow-step-edit-type', WorkflowStepView = 'workflow-step-view', WorkflowStepEdit = 'workflow-step-edit', WorkflowRunStepView = 'workflow-run-step-view', diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useStartNodeCreation.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useStartNodeCreation.ts index 9aa6a31696..a3f1717814 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useStartNodeCreation.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useStartNodeCreation.ts @@ -16,7 +16,7 @@ export const useStartNodeCreation = () => { const [workflowInsertStepIds, setWorkflowInsertStepIds] = useRecoilComponentState(workflowInsertStepIdsComponentState); - const { openStepSelectInCommandMenu } = useWorkflowCommandMenu(); + const { openWorkflowCreateStepInCommandMenu } = useWorkflowCommandMenu(); const workflowVisualizerWorkflowId = useRecoilComponentValue( workflowVisualizerWorkflowIdComponentState, @@ -57,13 +57,13 @@ export const useStartNodeCreation = () => { setCommandMenuNavigationStack([]); } - openStepSelectInCommandMenu(workflowVisualizerWorkflowId); + openWorkflowCreateStepInCommandMenu(workflowVisualizerWorkflowId); }, [ setWorkflowInsertStepIds, workflowVisualizerWorkflowId, isInRightDrawer, - openStepSelectInCommandMenu, + openWorkflowCreateStepInCommandMenu, setCommandMenuNavigationStack, ], ); diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowActionFooter.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowActionFooter.tsx index 1a55246f17..58bef2e8c7 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowActionFooter.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowActionFooter.tsx @@ -1,3 +1,5 @@ +import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; +import { useCommandMenuWorkflowIdOrThrow } from '@/command-menu/pages/workflow/hooks/useCommandMenuWorkflowIdOrThrow'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; @@ -8,7 +10,7 @@ import { useDuplicateStep } from '@/workflow/workflow-steps/hooks/useDuplicateSt import { useTheme } from '@emotion/react'; import { useLingui } from '@lingui/react/macro'; import { useId } from 'react'; -import { IconCopyPlus } from 'twenty-ui/display'; +import { IconCopyPlus, IconPencil } from 'twenty-ui/display'; import { Button } from 'twenty-ui/input'; import { MenuItem } from 'twenty-ui/navigation'; import { getOsControlSymbol } from 'twenty-ui/utilities'; @@ -25,6 +27,8 @@ export const WorkflowActionFooter = ({ const theme = useTheme(); const { duplicateStep } = useDuplicateStep(); const { closeDropdown } = useCloseDropdown(); + const workflowId = useCommandMenuWorkflowIdOrThrow(); + const { openWorkflowEditStepTypeInCommandMenu } = useWorkflowCommandMenu(); const OptionsDropdown = () => { return ( @@ -46,8 +50,16 @@ export const WorkflowActionFooter = ({ + { + closeDropdown(dropdownId); + openWorkflowEditStepTypeInCommandMenu(workflowId); + }} + text={t`Change node type`} + LeftIcon={IconPencil} + /> { closeDropdown(dropdownId); diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useUpdateStep.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useUpdateStep.ts index 486947af70..46f892d6c1 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useUpdateStep.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useUpdateStep.ts @@ -1,5 +1,5 @@ import { useGetUpdatableWorkflowVersionOrThrow } from '@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow'; -import { type WorkflowStep } from '@/workflow/types/Workflow'; +import { type WorkflowAction } from '@/workflow/types/Workflow'; import { useUpdateWorkflowVersionStep } from '@/workflow/workflow-steps/hooks/useUpdateWorkflowVersionStep'; export const useUpdateStep = () => { @@ -7,13 +7,17 @@ export const useUpdateStep = () => { useGetUpdatableWorkflowVersionOrThrow(); const { updateWorkflowVersionStep } = useUpdateWorkflowVersionStep(); - const updateStep = async (updatedStep: T) => { + const updateStep = async (updatedStep: WorkflowAction) => { const workflowVersionId = await getUpdatableWorkflowVersion(); - await updateWorkflowVersionStep({ + const result = await updateWorkflowVersionStep({ workflowVersionId, step: updatedStep, }); + + return { + updatedStep: result?.data?.updateWorkflowVersionStep, + }; }; return { diff --git a/packages/twenty-front/src/testing/decorators/WorkflowStepDecorator.tsx b/packages/twenty-front/src/testing/decorators/WorkflowStepDecorator.tsx index 17bbcd77ec..96661cc4d4 100644 --- a/packages/twenty-front/src/testing/decorators/WorkflowStepDecorator.tsx +++ b/packages/twenty-front/src/testing/decorators/WorkflowStepDecorator.tsx @@ -1,3 +1,5 @@ +import { commandMenuWorkflowIdComponentState } from '@/command-menu/pages/workflow/states/commandMenuWorkflowIdComponentState'; +import { CommandMenuPageComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuPageComponentInstanceContext'; import { useStepsOutputSchema } from '@/workflow/hooks/useStepsOutputSchema'; import { flowComponentState } from '@/workflow/states/flowComponentState'; import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; @@ -59,6 +61,12 @@ export const WorkflowStepDecorator: Decorator = (Story) => { steps: workflowVersion.steps, }, ); + set( + commandMenuWorkflowIdComponentState.atomFamily({ + instanceId: workflowVisualizerComponentInstanceId, + }), + getWorkflowMock().id, + ); populateStepsOutputSchema(workflowVersion); setReady(true); }, @@ -70,12 +78,18 @@ export const WorkflowStepDecorator: Decorator = (Story) => { }, [handleMount]); return ( - - {ready && } - + + {ready && } + + ); }; diff --git a/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-version-step.resolver.ts b/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-version-step.resolver.ts index fabb445af7..1069eb68e1 100644 --- a/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-version-step.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-version-step.resolver.ts @@ -24,6 +24,7 @@ import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-module import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.workspace-service'; import { WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type'; import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service'; +import { WorkflowRunnerWorkspaceService } from 'src/modules/workflow/workflow-runner/workspace-services/workflow-runner.workspace-service'; @Resolver() @UsePipes(ResolverValidationPipe) @@ -40,6 +41,7 @@ import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runne export class WorkflowVersionStepResolver { constructor( private readonly workflowVersionStepWorkspaceService: WorkflowVersionStepWorkspaceService, + private readonly workflowRunnerWorkspaceService: WorkflowRunnerWorkspaceService, private readonly workflowRunWorkspaceService: WorkflowRunWorkspaceService, private readonly featureFlagService: FeatureFlagService, ) {} @@ -101,7 +103,7 @@ export class WorkflowVersionStepResolver { @Args('input') { stepId, workflowRunId, response }: SubmitFormStepInput, ) { - await this.workflowVersionStepWorkspaceService.submitFormStep({ + await this.workflowRunnerWorkspaceService.submitFormStep({ workspaceId, stepId, workflowRunId, diff --git a/packages/twenty-server/src/engine/core-modules/workflow/workflow-api.module.ts b/packages/twenty-server/src/engine/core-modules/workflow/workflow-api.module.ts index bb2834eaaf..5452e4cca9 100644 --- a/packages/twenty-server/src/engine/core-modules/workflow/workflow-api.module.ts +++ b/packages/twenty-server/src/engine/core-modules/workflow/workflow-api.module.ts @@ -3,16 +3,17 @@ import { Module } from '@nestjs/common'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { WorkflowTriggerController } from 'src/engine/core-modules/workflow/controllers/workflow-trigger.controller'; import { WorkflowBuilderResolver } from 'src/engine/core-modules/workflow/resolvers/workflow-builder.resolver'; -import { WorkflowVersionStepResolver } from 'src/engine/core-modules/workflow/resolvers/workflow-version-step.resolver'; import { WorkflowTriggerResolver } from 'src/engine/core-modules/workflow/resolvers/workflow-trigger.resolver'; +import { WorkflowVersionEdgeResolver } from 'src/engine/core-modules/workflow/resolvers/workflow-version-edge.resolver'; +import { WorkflowVersionStepResolver } from 'src/engine/core-modules/workflow/resolvers/workflow-version-step.resolver'; import { WorkflowVersionResolver } from 'src/engine/core-modules/workflow/resolvers/workflow-version.resolver'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module'; import { WorkflowBuilderModule } from 'src/modules/workflow/workflow-builder/workflow-builder.module'; import { WorkflowVersionModule } from 'src/modules/workflow/workflow-builder/workflow-version/workflow-version.module'; import { WorkflowRunModule } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.module'; +import { WorkflowRunnerModule } from 'src/modules/workflow/workflow-runner/workflow-runner.module'; import { WorkflowTriggerModule } from 'src/modules/workflow/workflow-trigger/workflow-trigger.module'; -import { WorkflowVersionEdgeResolver } from 'src/engine/core-modules/workflow/resolvers/workflow-version-edge.resolver'; @Module({ imports: [ @@ -22,6 +23,7 @@ import { WorkflowVersionEdgeResolver } from 'src/engine/core-modules/workflow/re WorkflowCommonModule, WorkflowVersionModule, WorkflowRunModule, + WorkflowRunnerModule, PermissionsModule, ], controllers: [WorkflowTriggerController], diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/__tests__/workflow-version-step.workspace-service.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/__tests__/workflow-version-step.workspace-service.spec.ts index b398fd09a8..5a35ef28e5 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/__tests__/workflow-version-step.workspace-service.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/__tests__/workflow-version-step.workspace-service.spec.ts @@ -5,6 +5,7 @@ import { TRIGGER_STEP_ID } from 'twenty-shared/workflow'; import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { type WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity'; +import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; import { WorkflowSchemaWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service'; import { WorkflowVersionStepOperationsWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service'; import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.workspace-service'; @@ -12,8 +13,6 @@ import { type WorkflowAction, WorkflowActionType, } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type'; -import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service'; -import { WorkflowRunnerWorkspaceService } from 'src/modules/workflow/workflow-runner/workspace-services/workflow-runner.workspace-service'; import { WorkflowTriggerType } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type'; type MockWorkspaceRepository = Partial< @@ -125,8 +124,14 @@ describe('WorkflowVersionStepWorkspaceService', () => { runWorkflowVersionStepDeletionSideEffects: jest.fn(), }, }, - { provide: WorkflowRunWorkspaceService, useValue: {} }, - { provide: WorkflowRunnerWorkspaceService, useValue: {} }, + { + provide: WorkflowCommonWorkspaceService, + useValue: { + getWorkflowVersionOrFail: jest + .fn() + .mockResolvedValue(mockWorkflowVersion), + }, + }, ], }).compile(); diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.module.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.module.ts index 3df4b2f848..728f463a68 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.module.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.module.ts @@ -9,15 +9,11 @@ import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-commo import { WorkflowSchemaModule } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.module'; import { WorkflowVersionStepOperationsWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service'; import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.workspace-service'; -import { WorkflowRunModule } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.module'; -import { WorkflowRunnerModule } from 'src/modules/workflow/workflow-runner/workflow-runner.module'; @Module({ imports: [ WorkflowSchemaModule, ServerlessFunctionModule, - WorkflowRunnerModule, - WorkflowRunModule, WorkflowCommonModule, NestjsQueryTypeOrmModule.forFeature([ObjectMetadataEntity, AgentEntity]), ], @@ -25,6 +21,9 @@ import { WorkflowRunnerModule } from 'src/modules/workflow/workflow-runner/workf WorkflowVersionStepWorkspaceService, WorkflowVersionStepOperationsWorkspaceService, ], - exports: [WorkflowVersionStepWorkspaceService], + exports: [ + WorkflowVersionStepWorkspaceService, + WorkflowVersionStepOperationsWorkspaceService, + ], }) export class WorkflowVersionStepModule {} diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.workspace-service.ts index a147dfc3b2..525693d4d1 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.workspace-service.ts @@ -1,8 +1,7 @@ import { Injectable } from '@nestjs/common'; -import { t } from '@lingui/core/macro'; import { isDefined } from 'twenty-shared/utils'; -import { StepStatus, TRIGGER_STEP_ID } from 'twenty-shared/workflow'; +import { TRIGGER_STEP_ID } from 'twenty-shared/workflow'; import { type CreateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/create-workflow-version-step-input.dto'; import { type WorkflowVersionStepChangesDTO } from 'src/engine/core-modules/workflow/dtos/workflow-version-step-changes.dto'; @@ -13,24 +12,21 @@ import { } from 'src/modules/workflow/common/exceptions/workflow-version-step.exception'; import { type WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity'; import { assertWorkflowVersionIsDraft } from 'src/modules/workflow/common/utils/assert-workflow-version-is-draft.util'; +import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; import { computeWorkflowVersionStepChanges } from 'src/modules/workflow/workflow-builder/utils/compute-workflow-version-step-updates.util'; import { WorkflowSchemaWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service'; import { insertStep } from 'src/modules/workflow/workflow-builder/workflow-version-step/utils/insert-step'; import { removeStep } from 'src/modules/workflow/workflow-builder/workflow-version-step/utils/remove-step'; import { WorkflowVersionStepOperationsWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service'; -import { isWorkflowFormAction } from 'src/modules/workflow/workflow-executor/workflow-actions/form/guards/is-workflow-form-action.guard'; import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type'; -import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service'; -import { WorkflowRunnerWorkspaceService } from 'src/modules/workflow/workflow-runner/workspace-services/workflow-runner.workspace-service'; @Injectable() export class WorkflowVersionStepWorkspaceService { constructor( private readonly twentyORMGlobalManager: TwentyORMGlobalManager, private readonly workflowSchemaWorkspaceService: WorkflowSchemaWorkspaceService, - private readonly workflowRunWorkspaceService: WorkflowRunWorkspaceService, - private readonly workflowRunnerWorkspaceService: WorkflowRunnerWorkspaceService, private readonly workflowVersionStepOperationsWorkspaceService: WorkflowVersionStepOperationsWorkspaceService, + private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService, ) {} async createWorkflowVersionStep({ @@ -65,25 +61,11 @@ export class WorkflowVersionStepWorkspaceService { workspaceId, }); - const workflowVersionRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( + const workflowVersion = + await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail({ + workflowVersionId, workspaceId, - 'workflowVersion', - { shouldBypassPermissionChecks: true }, - ); - - const workflowVersion = await workflowVersionRepository.findOne({ - where: { - id: workflowVersionId, - }, - }); - - if (!isDefined(workflowVersion)) { - throw new WorkflowVersionStepException( - 'WorkflowVersion not found', - WorkflowVersionStepExceptionCode.NOT_FOUND, - ); - } + }); assertWorkflowVersionIsDraft(workflowVersion); @@ -100,6 +82,13 @@ export class WorkflowVersionStepWorkspaceService { parentStepConnectionOptions, }); + const workflowVersionRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + 'workflowVersion', + { shouldBypassPermissionChecks: true }, + ); + await workflowVersionRepository.update(workflowVersion.id, { trigger: updatedTrigger, steps: updatedSteps, @@ -121,25 +110,11 @@ export class WorkflowVersionStepWorkspaceService { workflowVersionId: string; step: WorkflowAction; }): Promise { - const workflowVersionRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( + const workflowVersion = + await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail({ + workflowVersionId, workspaceId, - 'workflowVersion', - { shouldBypassPermissionChecks: true }, - ); - - const workflowVersion = await workflowVersionRepository.findOne({ - where: { - id: workflowVersionId, - }, - }); - - if (!isDefined(workflowVersion)) { - throw new WorkflowVersionStepException( - 'WorkflowVersion not found', - WorkflowVersionStepExceptionCode.NOT_FOUND, - ); - } + }); assertWorkflowVersionIsDraft(workflowVersion); @@ -150,25 +125,51 @@ export class WorkflowVersionStepWorkspaceService { ); } - const enrichedNewStep = - await this.workflowSchemaWorkspaceService.enrichOutputSchema({ - step, - workspaceId, - }); + const existingStep = workflowVersion.steps.find( + (existingStep) => existingStep.id === step.id, + ); + + if (!isDefined(existingStep)) { + throw new WorkflowVersionStepException( + 'Step not found', + WorkflowVersionStepExceptionCode.NOT_FOUND, + ); + } + + const isStepTypeChanged = existingStep.type !== step.type; + + const updatedStep = isStepTypeChanged + ? await this.updateWorkflowVersionStepType({ + existingStep, + newStep: step, + workspaceId, + workflowVersionId, + }) + : await this.updateWorkflowVersionStepSettings({ + newStep: step, + workspaceId, + }); const updatedSteps = workflowVersion.steps.map((existingStep) => { if (existingStep.id === step.id) { - return enrichedNewStep; + return updatedStep; } else { return existingStep; } }); + const workflowVersionRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + 'workflowVersion', + { shouldBypassPermissionChecks: true }, + ); + await workflowVersionRepository.update(workflowVersion.id, { steps: updatedSteps, }); - return enrichedNewStep; + return updatedStep; } async deleteWorkflowVersionStep({ @@ -180,25 +181,11 @@ export class WorkflowVersionStepWorkspaceService { workflowVersionId: string; stepIdToDelete: string; }): Promise { - const workflowVersionRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( + const workflowVersion = + await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail({ + workflowVersionId, workspaceId, - 'workflowVersion', - { shouldBypassPermissionChecks: true }, - ); - - const workflowVersion = await workflowVersionRepository.findOne({ - where: { - id: workflowVersionId, - }, - }); - - if (!isDefined(workflowVersion)) { - throw new WorkflowVersionStepException( - 'WorkflowVersion not found', - WorkflowVersionStepExceptionCode.NOT_FOUND, - ); - } + }); assertWorkflowVersionIsDraft(workflowVersion); @@ -236,6 +223,13 @@ export class WorkflowVersionStepWorkspaceService { stepToDeleteChildrenIds, }); + const workflowVersionRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + 'workflowVersion', + { shouldBypassPermissionChecks: true }, + ); + await workflowVersionRepository.update(workflowVersion.id, { steps: updatedSteps, trigger: updatedTrigger, @@ -273,25 +267,11 @@ export class WorkflowVersionStepWorkspaceService { workflowVersionId: string; stepId: string; }): Promise { - const workflowVersionRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( + const workflowVersion = + await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail({ + workflowVersionId, workspaceId, - 'workflowVersion', - { shouldBypassPermissionChecks: true }, - ); - - const workflowVersion = await workflowVersionRepository.findOne({ - where: { - id: workflowVersionId, - }, - }); - - if (!isDefined(workflowVersion)) { - throw new WorkflowVersionStepException( - 'WorkflowVersion not found', - WorkflowVersionStepExceptionCode.NOT_FOUND, - ); - } + }); assertWorkflowVersionIsDraft(workflowVersion); @@ -320,6 +300,13 @@ export class WorkflowVersionStepWorkspaceService { insertedStep: duplicatedStep, }); + const workflowVersionRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + 'workflowVersion', + { shouldBypassPermissionChecks: true }, + ); + await workflowVersionRepository.update(workflowVersion.id, { steps: updatedSteps, trigger: updatedTrigger, @@ -332,70 +319,6 @@ export class WorkflowVersionStepWorkspaceService { }); } - async submitFormStep({ - workspaceId, - stepId, - workflowRunId, - response, - }: { - workspaceId: string; - stepId: string; - workflowRunId: string; - response: object; - }) { - const workflowRun = - await this.workflowRunWorkspaceService.getWorkflowRunOrFail({ - workflowRunId, - workspaceId, - }); - - const step = workflowRun.state?.flow?.steps?.find( - (step) => step.id === stepId, - ); - - if (!isDefined(step)) { - throw new WorkflowVersionStepException( - 'Step not found', - WorkflowVersionStepExceptionCode.NOT_FOUND, - ); - } - - if (!isWorkflowFormAction(step)) { - throw new WorkflowVersionStepException( - 'Step is not a form', - WorkflowVersionStepExceptionCode.INVALID_REQUEST, - { - userFriendlyMessage: t`Step is not a form`, - }, - ); - } - - const enrichedResponse = - await this.workflowVersionStepOperationsWorkspaceService.enrichFormStepResponse( - { - workspaceId, - step, - response, - }, - ); - - await this.workflowRunWorkspaceService.updateWorkflowRunStepInfo({ - stepId, - stepInfo: { - status: StepStatus.SUCCESS, - result: enrichedResponse, - }, - workspaceId, - workflowRunId, - }); - - await this.workflowRunnerWorkspaceService.resume({ - workspaceId, - workflowRunId, - lastExecutedStepId: stepId, - }); - } - async createDraftStep({ step, workspaceId, @@ -408,4 +331,56 @@ export class WorkflowVersionStepWorkspaceService { workspaceId, }); } + + private async updateWorkflowVersionStepType({ + existingStep, + newStep, + workspaceId, + workflowVersionId, + }: { + existingStep: WorkflowAction; + newStep: WorkflowAction; + workspaceId: string; + workflowVersionId: string; + }): Promise { + await this.workflowVersionStepOperationsWorkspaceService.runWorkflowVersionStepDeletionSideEffects( + { + step: existingStep, + workspaceId, + }, + ); + + const defaultStep = + await this.workflowVersionStepOperationsWorkspaceService.runStepCreationSideEffectsAndBuildStep( + { + type: newStep.type, + workspaceId, + position: newStep.position, + workflowVersionId, + }, + ); + + return this.workflowSchemaWorkspaceService.enrichOutputSchema({ + step: { + ...defaultStep, + id: existingStep.id, + nextStepIds: existingStep.nextStepIds, + position: existingStep.position, + }, + workspaceId, + }); + } + + private async updateWorkflowVersionStepSettings({ + newStep, + workspaceId, + }: { + newStep: WorkflowAction; + workspaceId: string; + }): Promise { + return this.workflowSchemaWorkspaceService.enrichOutputSchema({ + step: newStep, + workspaceId, + }); + } } diff --git a/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run/workflow-run.module.ts b/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run/workflow-run.module.ts index 99f4c52412..59cc910b1d 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run/workflow-run.module.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run/workflow-run.module.ts @@ -2,15 +2,15 @@ import { Module } from '@nestjs/common'; import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; +import { CacheLockModule } from 'src/engine/core-modules/cache-lock/cache-lock.module'; import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module'; import { RecordPositionModule } from 'src/engine/core-modules/record-position/record-position.module'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory'; import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module'; -import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service'; -import { CacheLockModule } from 'src/engine/core-modules/cache-lock/cache-lock.module'; import { DeleteWorkflowRunsCommand } from 'src/modules/workflow/workflow-runner/workflow-run/command/delete-workflow-runs.command'; -import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service'; @Module({ imports: [ diff --git a/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-runner.module.ts b/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-runner.module.ts index 34296f4595..5ec1a5a700 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-runner.module.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-runner.module.ts @@ -1,15 +1,16 @@ import { Module } from '@nestjs/common'; import { BillingModule } from 'src/engine/core-modules/billing/billing.module'; +import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module'; import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module'; import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module'; +import { WorkflowVersionStepModule } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.module'; import { WorkflowExecutorModule } from 'src/modules/workflow/workflow-executor/workflow-executor.module'; import { RunWorkflowJob } from 'src/modules/workflow/workflow-runner/jobs/run-workflow.job'; import { WorkflowRunQueueModule } from 'src/modules/workflow/workflow-runner/workflow-run-queue/workflow-run-queue.module'; import { WorkflowRunModule } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.module'; import { WorkflowRunnerWorkspaceService } from 'src/modules/workflow/workflow-runner/workspace-services/workflow-runner.workspace-service'; -import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; @Module({ imports: [ @@ -21,6 +22,7 @@ import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature- MetricsModule, WorkflowRunQueueModule, FeatureFlagModule, + WorkflowVersionStepModule, ], providers: [WorkflowRunnerWorkspaceService, RunWorkflowJob], exports: [WorkflowRunnerWorkspaceService], diff --git a/packages/twenty-server/src/modules/workflow/workflow-runner/workspace-services/workflow-runner.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-runner/workspace-services/workflow-runner.workspace-service.ts index 5a372290ca..baeb05a043 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-runner/workspace-services/workflow-runner.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-runner/workspace-services/workflow-runner.workspace-service.ts @@ -1,12 +1,22 @@ import { Injectable, Logger } from '@nestjs/common'; +import { t } from '@lingui/core/macro'; +import { isDefined } from 'twenty-shared/utils'; +import { StepStatus } from 'twenty-shared/workflow'; + import { BillingUsageService } from 'src/engine/core-modules/billing/services/billing-usage.service'; import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; import { type ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; +import { + WorkflowVersionStepException, + WorkflowVersionStepExceptionCode, +} from 'src/modules/workflow/common/exceptions/workflow-version-step.exception'; import { WorkflowRunStatus } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity'; import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; +import { WorkflowVersionStepOperationsWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service'; +import { isWorkflowFormAction } from 'src/modules/workflow/workflow-executor/workflow-actions/form/guards/is-workflow-form-action.guard'; import { RunWorkflowJob, RunWorkflowJobData, @@ -25,6 +35,7 @@ export class WorkflowRunnerWorkspaceService { @InjectMessageQueue(MessageQueue.workflowQueue) private readonly messageQueueService: MessageQueueService, private readonly billingUsageService: BillingUsageService, + private readonly workflowVersionStepOperationsWorkspaceService: WorkflowVersionStepOperationsWorkspaceService, ) {} async run({ @@ -101,6 +112,70 @@ export class WorkflowRunnerWorkspaceService { ); } + async submitFormStep({ + workspaceId, + stepId, + workflowRunId, + response, + }: { + workspaceId: string; + stepId: string; + workflowRunId: string; + response: object; + }) { + const workflowRun = + await this.workflowRunWorkspaceService.getWorkflowRunOrFail({ + workflowRunId, + workspaceId, + }); + + const step = workflowRun.state?.flow?.steps?.find( + (step) => step.id === stepId, + ); + + if (!isDefined(step)) { + throw new WorkflowVersionStepException( + 'Step not found', + WorkflowVersionStepExceptionCode.NOT_FOUND, + ); + } + + if (!isWorkflowFormAction(step)) { + throw new WorkflowVersionStepException( + 'Step is not a form', + WorkflowVersionStepExceptionCode.INVALID_REQUEST, + { + userFriendlyMessage: t`Step is not a form`, + }, + ); + } + + const enrichedResponse = + await this.workflowVersionStepOperationsWorkspaceService.enrichFormStepResponse( + { + workspaceId, + step, + response, + }, + ); + + await this.workflowRunWorkspaceService.updateWorkflowRunStepInfo({ + stepId, + stepInfo: { + status: StepStatus.SUCCESS, + result: enrichedResponse, + }, + workspaceId, + workflowRunId, + }); + + await this.resume({ + workspaceId, + workflowRunId, + lastExecutedStepId: stepId, + }); + } + private async enqueueWorkflowRun( workspaceId: string, workflowRunId: string,