diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 51c2634ad09..6f72afcc296 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -1825,7 +1825,7 @@ export type Mutation = { restorePageLayoutWidget: PageLayoutWidget; retryJobs: RetryJobsResponse; revokeApiKey?: Maybe; - runWorkflowVersion: WorkflowRun; + runWorkflowVersion: RunWorkflowVersionOutput; saveImapSmtpCaldavAccount: ImapSmtpCaldavConnectionSuccess; sendInvitations: SendInvitationsOutput; setMeteredSubscriptionPrice: BillingUpdateOutput; @@ -1836,6 +1836,7 @@ export type Mutation = { skipBookOnboardingStep: OnboardingStepSuccess; skipSyncEmailOnboardingStep: OnboardingStepSuccess; startChannelSync: ChannelSyncSuccess; + stopWorkflowRun: WorkflowRun; submitFormStep: Scalars['Boolean']; switchBillingPlan: BillingUpdateOutput; switchSubscriptionInterval: BillingUpdateOutput; @@ -2516,6 +2517,11 @@ export type MutationStartChannelSyncArgs = { }; +export type MutationStopWorkflowRunArgs = { + workflowRunId: Scalars['UUID']; +}; + + export type MutationSubmitFormStepArgs = { input: SubmitFormStepInput; }; @@ -3702,6 +3708,11 @@ export type RunWorkflowVersionInput = { workflowVersionId: Scalars['UUID']; }; +export type RunWorkflowVersionOutput = { + __typename?: 'RunWorkflowVersionOutput'; + workflowRunId: Scalars['UUID']; +}; + export type SsoConnection = { __typename?: 'SSOConnection'; id: Scalars['UUID']; @@ -4585,9 +4596,21 @@ export enum WorkflowActionType { export type WorkflowRun = { __typename?: 'WorkflowRun'; - workflowRunId: Scalars['UUID']; + id: Scalars['UUID']; + status: WorkflowRunStatusEnum; }; +/** Status of the workflow run */ +export enum WorkflowRunStatusEnum { + COMPLETED = 'COMPLETED', + ENQUEUED = 'ENQUEUED', + FAILED = 'FAILED', + NOT_STARTED = 'NOT_STARTED', + RUNNING = 'RUNNING', + STOPPED = 'STOPPED', + STOPPING = 'STOPPING' +} + export type WorkflowStepPosition = { __typename?: 'WorkflowStepPosition'; x: Scalars['Float']; @@ -6152,7 +6175,14 @@ export type RunWorkflowVersionMutationVariables = Exact<{ }>; -export type RunWorkflowVersionMutation = { __typename?: 'Mutation', runWorkflowVersion: { __typename?: 'WorkflowRun', workflowRunId: string } }; +export type RunWorkflowVersionMutation = { __typename?: 'Mutation', runWorkflowVersion: { __typename?: 'RunWorkflowVersionOutput', workflowRunId: string } }; + +export type StopWorkflowRunMutationVariables = Exact<{ + workflowRunId: Scalars['UUID']; +}>; + + +export type StopWorkflowRunMutation = { __typename?: 'Mutation', stopWorkflowRun: { __typename: 'WorkflowRun', id: string, status: WorkflowRunStatusEnum } }; export type UpdateWorkflowRunStepMutationVariables = Exact<{ input: UpdateWorkflowRunStepInput; @@ -13865,6 +13895,41 @@ export function useRunWorkflowVersionMutation(baseOptions?: Apollo.MutationHookO export type RunWorkflowVersionMutationHookResult = ReturnType; export type RunWorkflowVersionMutationResult = Apollo.MutationResult; export type RunWorkflowVersionMutationOptions = Apollo.BaseMutationOptions; +export const StopWorkflowRunDocument = gql` + mutation StopWorkflowRun($workflowRunId: UUID!) { + stopWorkflowRun(workflowRunId: $workflowRunId) { + id + status + __typename + } +} + `; +export type StopWorkflowRunMutationFn = Apollo.MutationFunction; + +/** + * __useStopWorkflowRunMutation__ + * + * To run a mutation, you first call `useStopWorkflowRunMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useStopWorkflowRunMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [stopWorkflowRunMutation, { data, loading, error }] = useStopWorkflowRunMutation({ + * variables: { + * workflowRunId: // value for 'workflowRunId' + * }, + * }); + */ +export function useStopWorkflowRunMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(StopWorkflowRunDocument, options); + } +export type StopWorkflowRunMutationHookResult = ReturnType; +export type StopWorkflowRunMutationResult = Apollo.MutationResult; +export type StopWorkflowRunMutationOptions = Apollo.BaseMutationOptions; export const UpdateWorkflowRunStepDocument = gql` mutation UpdateWorkflowRunStep($input: UpdateWorkflowRunStepInput!) { updateWorkflowRunStep(input: $input) { diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index 64645fd66af..1ebac1b270e 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -1780,7 +1780,7 @@ export type Mutation = { restorePageLayoutWidget: PageLayoutWidget; retryJobs: RetryJobsResponse; revokeApiKey?: Maybe; - runWorkflowVersion: WorkflowRun; + runWorkflowVersion: RunWorkflowVersionOutput; saveImapSmtpCaldavAccount: ImapSmtpCaldavConnectionSuccess; sendInvitations: SendInvitationsOutput; setMeteredSubscriptionPrice: BillingUpdateOutput; @@ -1791,6 +1791,7 @@ export type Mutation = { skipBookOnboardingStep: OnboardingStepSuccess; skipSyncEmailOnboardingStep: OnboardingStepSuccess; startChannelSync: ChannelSyncSuccess; + stopWorkflowRun: WorkflowRun; submitFormStep: Scalars['Boolean']; switchBillingPlan: BillingUpdateOutput; switchSubscriptionInterval: BillingUpdateOutput; @@ -2447,6 +2448,11 @@ export type MutationStartChannelSyncArgs = { }; +export type MutationStopWorkflowRunArgs = { + workflowRunId: Scalars['UUID']; +}; + + export type MutationSubmitFormStepArgs = { input: SubmitFormStepInput; }; @@ -3548,6 +3554,11 @@ export type RunWorkflowVersionInput = { workflowVersionId: Scalars['UUID']; }; +export type RunWorkflowVersionOutput = { + __typename?: 'RunWorkflowVersionOutput'; + workflowRunId: Scalars['UUID']; +}; + export type SsoConnection = { __typename?: 'SSOConnection'; id: Scalars['UUID']; @@ -4413,9 +4424,21 @@ export enum WorkflowActionType { export type WorkflowRun = { __typename?: 'WorkflowRun'; - workflowRunId: Scalars['UUID']; + id: Scalars['UUID']; + status: WorkflowRunStatusEnum; }; +/** Status of the workflow run */ +export enum WorkflowRunStatusEnum { + COMPLETED = 'COMPLETED', + ENQUEUED = 'ENQUEUED', + FAILED = 'FAILED', + NOT_STARTED = 'NOT_STARTED', + RUNNING = 'RUNNING', + STOPPED = 'STOPPED', + STOPPING = 'STOPPING' +} + export type WorkflowStepPosition = { __typename?: 'WorkflowStepPosition'; x: Scalars['Float']; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/constants/WorkflowRunsActionsConfig.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/constants/WorkflowRunsActionsConfig.tsx index 868a8adfb8d..5dd0574f45c 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/constants/WorkflowRunsActionsConfig.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/constants/WorkflowRunsActionsConfig.tsx @@ -3,21 +3,45 @@ import { NoSelectionRecordActionKeys } from '@/action-menu/actions/record-action import { SingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/types/SingleRecordActionsKey'; import { SeeVersionWorkflowRunSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-run-actions/components/SeeVersionWorkflowRunSingleRecordAction'; import { SeeWorkflowWorkflowRunSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-run-actions/components/SeeWorkflowWorkflowRunSingleRecordAction'; +import { StopWorkflowRunSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-run-actions/components/StopWorkflowRunSingleRecordAction'; import { WorkflowRunSingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/workflow-run-actions/types/WorkflowRunSingleRecordActionsKeys'; import { inheritActionsFromDefaultConfig } from '@/action-menu/actions/record-actions/utils/inheritActionsFromDefaultConfig'; import { ActionScope } from '@/action-menu/actions/types/ActionScope'; import { ActionType } from '@/action-menu/actions/types/ActionType'; import { ActionViewType } from '@/action-menu/actions/types/ActionViewType'; import { msg } from '@lingui/core/macro'; -import { IconSettingsAutomation, IconVersions } from 'twenty-ui/display'; +import { + IconPlayerStop, + IconSettingsAutomation, + IconVersions, +} from 'twenty-ui/display'; export const WORKFLOW_RUNS_ACTIONS_CONFIG = inheritActionsFromDefaultConfig({ config: { + [WorkflowRunSingleRecordActionKeys.STOP]: { + key: WorkflowRunSingleRecordActionKeys.STOP, + label: msg`Stop`, + shortLabel: msg`Stop`, + position: 0, + isPinned: true, + type: ActionType.Standard, + scope: ActionScope.RecordSelection, + Icon: IconPlayerStop, + shouldBeRegistered: ({ selectedRecord }) => { + return selectedRecord?.status === 'RUNNING'; + }, + availableOn: [ + ActionViewType.SHOW_PAGE, + ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, + ActionViewType.INDEX_PAGE_BULK_SELECTION, + ], + component: , + }, [WorkflowRunSingleRecordActionKeys.SEE_WORKFLOW]: { key: WorkflowRunSingleRecordActionKeys.SEE_WORKFLOW, label: msg`See workflow`, shortLabel: msg`See workflow`, - position: 0, + position: 1, isPinned: true, type: ActionType.Standard, scope: ActionScope.RecordSelection, @@ -33,7 +57,7 @@ export const WORKFLOW_RUNS_ACTIONS_CONFIG = inheritActionsFromDefaultConfig({ key: WorkflowRunSingleRecordActionKeys.SEE_VERSION, label: msg`See version`, shortLabel: msg`See version`, - position: 1, + position: 2, isPinned: true, type: ActionType.Standard, scope: ActionScope.RecordSelection, @@ -68,63 +92,63 @@ export const WORKFLOW_RUNS_ACTIONS_CONFIG = inheritActionsFromDefaultConfig({ propertiesToOverwrite: { [SingleRecordActionKeys.ADD_TO_FAVORITES]: { isPinned: false, - position: 2, + position: 3, }, [SingleRecordActionKeys.REMOVE_FROM_FAVORITES]: { isPinned: false, - position: 3, + position: 4, }, [SingleRecordActionKeys.EXPORT_FROM_RECORD_INDEX]: { - position: 4, + position: 5, label: msg`Export run`, }, [SingleRecordActionKeys.EXPORT_FROM_RECORD_SHOW]: { - position: 4, + position: 5, label: msg`Export run`, }, [MultipleRecordsActionKeys.EXPORT]: { - position: 5, + position: 6, label: msg`Export runs`, }, [NoSelectionRecordActionKeys.EXPORT_VIEW]: { - position: 6, + position: 7, label: msg`Export view`, }, [NoSelectionRecordActionKeys.SEE_DELETED_RECORDS]: { - position: 7, + position: 8, label: msg`See deleted runs`, }, [NoSelectionRecordActionKeys.HIDE_DELETED_RECORDS]: { - position: 8, + position: 9, label: msg`Hide deleted runs`, }, [SingleRecordActionKeys.NAVIGATE_TO_PREVIOUS_RECORD]: { - position: 9, - }, - [SingleRecordActionKeys.NAVIGATE_TO_NEXT_RECORD]: { position: 10, }, - [NoSelectionRecordActionKeys.GO_TO_WORKFLOWS]: { + [SingleRecordActionKeys.NAVIGATE_TO_NEXT_RECORD]: { position: 11, + }, + [NoSelectionRecordActionKeys.GO_TO_WORKFLOWS]: { + position: 12, isPinned: true, }, [NoSelectionRecordActionKeys.GO_TO_PEOPLE]: { - position: 12, - }, - [NoSelectionRecordActionKeys.GO_TO_COMPANIES]: { position: 13, }, - [NoSelectionRecordActionKeys.GO_TO_OPPORTUNITIES]: { + [NoSelectionRecordActionKeys.GO_TO_COMPANIES]: { position: 14, }, - [NoSelectionRecordActionKeys.GO_TO_SETTINGS]: { + [NoSelectionRecordActionKeys.GO_TO_OPPORTUNITIES]: { position: 15, }, - [NoSelectionRecordActionKeys.GO_TO_TASKS]: { + [NoSelectionRecordActionKeys.GO_TO_SETTINGS]: { position: 16, }, - [NoSelectionRecordActionKeys.GO_TO_NOTES]: { + [NoSelectionRecordActionKeys.GO_TO_TASKS]: { position: 17, }, + [NoSelectionRecordActionKeys.GO_TO_NOTES]: { + position: 18, + }, }, }); diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-run-actions/components/StopWorkflowRunSingleRecordAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-run-actions/components/StopWorkflowRunSingleRecordAction.tsx new file mode 100644 index 00000000000..a8eb6650d36 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-run-actions/components/StopWorkflowRunSingleRecordAction.tsx @@ -0,0 +1,29 @@ +import { Action } from '@/action-menu/actions/components/Action'; +import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { useStopWorkflowRun } from '@/workflow/hooks/useStopWorkflowRun'; + +export const StopWorkflowRunSingleRecordAction = () => { + const contextStoreTargetedRecordsRule = useRecoilComponentValue( + contextStoreTargetedRecordsRuleComponentState, + ); + + if ( + contextStoreTargetedRecordsRule.mode === 'exclusion' || + (contextStoreTargetedRecordsRule.mode === 'selection' && + contextStoreTargetedRecordsRule.selectedRecordIds.length === 0) + ) { + throw new Error('Selected record ID is required'); + } + + const selectedRecordIds = contextStoreTargetedRecordsRule.selectedRecordIds; + const { stopWorkflowRun } = useStopWorkflowRun(); + + const handleClick = async () => { + for (const selectedRecordId of selectedRecordIds) { + await stopWorkflowRun(selectedRecordId); + } + }; + + return ; +}; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-run-actions/types/WorkflowRunSingleRecordActionsKeys.ts b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-run-actions/types/WorkflowRunSingleRecordActionsKeys.ts index ead093f8c08..ad84a02da31 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-run-actions/types/WorkflowRunSingleRecordActionsKeys.ts +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-run-actions/types/WorkflowRunSingleRecordActionsKeys.ts @@ -1,4 +1,5 @@ export enum WorkflowRunSingleRecordActionKeys { + STOP = 'stop-workflow-run-single-record', SEE_WORKFLOW = 'see-workflow-workflow-run-single-record', SEE_VERSION = 'see-version-workflow-run-single-record', } diff --git a/packages/twenty-front/src/modules/workflow/graphql/mutations/stopWorkflowRun.ts b/packages/twenty-front/src/modules/workflow/graphql/mutations/stopWorkflowRun.ts new file mode 100644 index 00000000000..db8b736382e --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/graphql/mutations/stopWorkflowRun.ts @@ -0,0 +1,11 @@ +import { gql } from '@apollo/client'; + +export const STOP_WORKFLOW_RUN = gql` + mutation StopWorkflowRun($workflowRunId: UUID!) { + stopWorkflowRun(workflowRunId: $workflowRunId) { + id + status + __typename + } + } +`; diff --git a/packages/twenty-front/src/modules/workflow/hooks/useStopWorkflowRun.ts b/packages/twenty-front/src/modules/workflow/hooks/useStopWorkflowRun.ts new file mode 100644 index 00000000000..5b2c820f78b --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/hooks/useStopWorkflowRun.ts @@ -0,0 +1,29 @@ +import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; +import { useRegisterObjectOperation } from '@/object-record/hooks/useRegisterObjectOperation'; +import { useStopWorkflowRunMutation } from '~/generated-metadata/graphql'; + +export const useStopWorkflowRun = () => { + const apolloCoreClient = useApolloCoreClient(); + const [mutate] = useStopWorkflowRunMutation({ + client: apolloCoreClient, + }); + const { registerObjectOperation } = useRegisterObjectOperation(); + + const stopWorkflowRun = async (workflowRunId: string) => { + const { data } = await mutate({ + variables: { + workflowRunId, + }, + }); + + registerObjectOperation('workflowRun', { + type: 'update-one', + result: { + updatedRecord: data?.stopWorkflowRun, + updateInput: { id: workflowRunId }, + }, + }); + }; + + return { stopWorkflowRun }; +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getWorkflowRunStatusTagProps.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getWorkflowRunStatusTagProps.ts index 1bca44a4d3a..fff3983b92e 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getWorkflowRunStatusTagProps.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getWorkflowRunStatusTagProps.ts @@ -34,6 +34,20 @@ export const getWorkflowRunStatusTagProps = ({ }; } + if (workflowRunStatus === 'STOPPING') { + return { + color: 'orange', + text: 'Stopping', + }; + } + + if (workflowRunStatus === 'STOPPED') { + return { + color: 'gray', + text: 'Stopped', + }; + } + return { color: 'red', text: 'Failed', diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-add-workflow-run-stop-statuses.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-add-workflow-run-stop-statuses.command.ts new file mode 100644 index 00000000000..66e6f3eb3e3 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-add-workflow-run-stop-statuses.command.ts @@ -0,0 +1,124 @@ +import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; + +import { Command } from 'nest-commander'; +import { DataSource, Repository } from 'typeorm'; + +import { + ActiveOrSuspendedWorkspacesMigrationCommandRunner, + type RunOnWorkspaceArgs, +} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util'; +import { WORKFLOW_RUN_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; +import { WorkflowRunStatus } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity'; + +@Command({ + name: 'upgrade:1-10:add-workflow-run-stop-statuses', + description: 'Add stopped and stopping statuses to workflow runs', +}) +export class AddWorkflowRunStopStatusesCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { + constructor( + @InjectRepository(WorkspaceEntity) + protected readonly workspaceRepository: Repository, + protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + @InjectRepository(FieldMetadataEntity) + private readonly fieldMetadataRepository: Repository, + @InjectDataSource() + private readonly coreDataSource: DataSource, + ) { + super(workspaceRepository, twentyORMGlobalManager); + } + + override async runOnWorkspace({ + workspaceId, + options, + }: RunOnWorkspaceArgs): Promise { + this.logger.log( + `Adding stopped and stopping statuses to workflow runs for workspace ${workspaceId}`, + ); + + const workflowRunStatusFieldMetadata = + await this.fieldMetadataRepository.findOne({ + where: { + standardId: WORKFLOW_RUN_STANDARD_FIELD_IDS.status, + }, + }); + + if (!workflowRunStatusFieldMetadata) { + this.logger.error( + `Workflow run status field metadata not found for workspace ${workspaceId}`, + ); + + return; + } + + const workflowRunStatusFieldMetadataOptions = + workflowRunStatusFieldMetadata.options; + + if ( + workflowRunStatusFieldMetadataOptions?.some( + (option) => + option.value === WorkflowRunStatus.STOPPED || + option.value === WorkflowRunStatus.STOPPING, + ) + ) { + this.logger.log( + `Workflow run status field metadata options already contain stopped and stopping statuses for workspace ${workspaceId}`, + ); + + return; + } else if (options.dryRun) { + this.logger.log( + `Would add stopped and stopping statuses to workflow run status field metadata for workspace ${workspaceId}`, + ); + } else { + workflowRunStatusFieldMetadataOptions?.push({ + value: WorkflowRunStatus.STOPPING, + label: 'Stopping', + position: 5, + color: 'orange', + }); + workflowRunStatusFieldMetadataOptions?.push({ + value: WorkflowRunStatus.STOPPED, + label: 'Stopped', + position: 6, + color: 'gray', + }); + + await this.fieldMetadataRepository.save(workflowRunStatusFieldMetadata); + + this.logger.log( + `Stopped and stopping statuses added to workflow run status field metadata for workspace ${workspaceId}`, + ); + } + + const schemaName = getWorkspaceSchemaName(workspaceId); + + if (options.dryRun) { + this.logger.log( + `Would try to add stopped and stopping statuses to workflow run status enum for workspace ${workspaceId}`, + ); + } else { + try { + await this.coreDataSource.query( + `ALTER TYPE ${schemaName}."workflowRun_status_enum" ADD VALUE 'STOPPING'`, + ); + this.logger.log( + `Stopping status added to workflow run status enum for workspace ${workspaceId}`, + ); + await this.coreDataSource.query( + `ALTER TYPE ${schemaName}."workflowRun_status_enum" ADD VALUE 'STOPPED'`, + ); + this.logger.log( + `Stopped status added to workflow run status enum for workspace ${workspaceId}`, + ); + } catch (error) { + this.logger.error( + `Error adding stopped and stopping statuses to workflow run status enum for workspace ${workspaceId}: ${error}`, + ); + } + } + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-upgrade-version-command.module.ts index 72d2236be04..327a3e030c2 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-upgrade-version-command.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-upgrade-version-command.module.ts @@ -1,23 +1,27 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { AddWorkflowRunStopStatusesCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-add-workflow-run-stop-statuses.command'; import { MigrateAttachmentAuthorToCreatedByCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-author-to-created-by.command'; import { MigrateAttachmentTypeToFileCategoryCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-type-to-file-category.command'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module'; @Module({ imports: [ - TypeOrmModule.forFeature([WorkspaceEntity]), + TypeOrmModule.forFeature([WorkspaceEntity, FieldMetadataEntity]), WorkspaceSchemaManagerModule, ], providers: [ MigrateAttachmentAuthorToCreatedByCommand, MigrateAttachmentTypeToFileCategoryCommand, + AddWorkflowRunStopStatusesCommand, ], exports: [ MigrateAttachmentAuthorToCreatedByCommand, MigrateAttachmentTypeToFileCategoryCommand, + AddWorkflowRunStopStatusesCommand, ], }) export class V1_10_UpgradeVersionCommandModule {} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts index 1dd25e832ab..6301015319d 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts @@ -19,6 +19,7 @@ import { AddEnqueuedStatusToWorkflowRunCommand } from 'src/database/commands/upg import { FixSchemaArrayTypeCommand } from 'src/database/commands/upgrade-version-command/1-1/1-1-fix-schema-array-type.command'; import { FixUpdateStandardFieldsIsLabelSyncedWithName } from 'src/database/commands/upgrade-version-command/1-1/1-1-fix-update-standard-field-is-label-synced-with-name.command'; import { MigrateWorkflowRunStatesCommand } from 'src/database/commands/upgrade-version-command/1-1/1-1-migrate-workflow-run-state.command'; +import { AddWorkflowRunStopStatusesCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-add-workflow-run-stop-statuses.command'; import { MigrateAttachmentAuthorToCreatedByCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-author-to-created-by.command'; import { MigrateAttachmentTypeToFileCategoryCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-type-to-file-category.command'; import { AddEnqueuedStatusToWorkflowRunV2Command } from 'src/database/commands/upgrade-version-command/1-2/1-2-add-enqueued-status-to-workflow-run-v2.command'; @@ -101,6 +102,7 @@ export class UpgradeCommand extends UpgradeCommandRunner { // 1.10 Commands protected readonly migrateAttachmentAuthorToCreatedByCommand: MigrateAttachmentAuthorToCreatedByCommand, protected readonly migrateAttachmentTypeToFileCategoryCommand: MigrateAttachmentTypeToFileCategoryCommand, + protected readonly addWorkflowRunStopStatusesCommand: AddWorkflowRunStopStatusesCommand, ) { super( workspaceRepository, @@ -206,7 +208,7 @@ export class UpgradeCommand extends UpgradeCommandRunner { }; const commands_1100: VersionCommands = { - beforeSyncMetadata: [], + beforeSyncMetadata: [this.addWorkflowRunStopStatusesCommand], afterSyncMetadata: [ this.migrateAttachmentAuthorToCreatedByCommand, this.migrateAttachmentTypeToFileCategoryCommand, diff --git a/packages/twenty-server/src/engine/core-modules/metrics/types/metrics-keys.type.ts b/packages/twenty-server/src/engine/core-modules/metrics/types/metrics-keys.type.ts index e13f451d1c2..71ea6db61b4 100644 --- a/packages/twenty-server/src/engine/core-modules/metrics/types/metrics-keys.type.ts +++ b/packages/twenty-server/src/engine/core-modules/metrics/types/metrics-keys.type.ts @@ -19,6 +19,7 @@ export enum MetricsKeys { WorkflowRunStartedManualTrigger = 'workflow-run/started/manual-trigger', WorkflowRunCompleted = 'workflow-run/completed', WorkflowRunFailed = 'workflow-run/failed', + WorkflowRunStopped = 'workflow-run/stopped', WorkflowRunFailedThrottled = 'workflow-run/failed/throttled', WorkflowRunFailedToEnqueue = 'workflow-run/failed/to-enqueue', AIToolExecutionFailed = 'ai-tool-execution/failed', diff --git a/packages/twenty-server/src/engine/core-modules/workflow/dtos/run-workflow-version-output.dto.ts b/packages/twenty-server/src/engine/core-modules/workflow/dtos/run-workflow-version-output.dto.ts new file mode 100644 index 00000000000..1dc1656f362 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/workflow/dtos/run-workflow-version-output.dto.ts @@ -0,0 +1,9 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; + +@ObjectType('RunWorkflowVersionOutput') +export class RunWorkflowVersionOutput { + @Field(() => UUIDScalarType) + workflowRunId: string; +} diff --git a/packages/twenty-server/src/engine/core-modules/workflow/dtos/workflow-run.dto.ts b/packages/twenty-server/src/engine/core-modules/workflow/dtos/workflow-run.dto.ts index 7516eca15e4..d8d3b8554d1 100644 --- a/packages/twenty-server/src/engine/core-modules/workflow/dtos/workflow-run.dto.ts +++ b/packages/twenty-server/src/engine/core-modules/workflow/dtos/workflow-run.dto.ts @@ -1,9 +1,13 @@ import { Field, ObjectType } from '@nestjs/graphql'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; +import { WorkflowRunStatus } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity'; @ObjectType('WorkflowRun') export class WorkflowRunDTO { @Field(() => UUIDScalarType) - workflowRunId: string; + id: string; + + @Field(() => WorkflowRunStatus) + status: WorkflowRunStatus; } diff --git a/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-trigger.resolver.ts b/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-trigger.resolver.ts index fd2422ba239..9dbc7ff18b1 100644 --- a/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-trigger.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-trigger.resolver.ts @@ -7,6 +7,7 @@ import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe'; import { UserEntity } from 'src/engine/core-modules/user/user.entity'; import { RunWorkflowVersionInput } from 'src/engine/core-modules/workflow/dtos/run-workflow-version-input.dto'; +import { RunWorkflowVersionOutput } from 'src/engine/core-modules/workflow/dtos/run-workflow-version-output.dto'; import { WorkflowRunDTO } from 'src/engine/core-modules/workflow/dtos/workflow-run.dto'; import { WorkflowTriggerGraphqlApiExceptionFilter } from 'src/engine/core-modules/workflow/filters/workflow-trigger-graphql-api-exception.filter'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -59,7 +60,7 @@ export class WorkflowTriggerResolver { ); } - @Mutation(() => WorkflowRunDTO) + @Mutation(() => RunWorkflowVersionOutput) async runWorkflowVersion( @AuthUser() user: UserEntity, @AuthWorkspace() workspace: WorkspaceEntity, @@ -91,4 +92,12 @@ export class WorkflowTriggerResolver { }), }); } + + @Mutation(() => WorkflowRunDTO) + async stopWorkflowRun( + @Args('workflowRunId', { type: () => UUIDScalarType }) + workflowRunId: string, + ) { + return this.workflowTriggerWorkspaceService.stopWorkflowRun(workflowRunId); + } } diff --git a/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-run-update-one.pre-query.hook.ts b/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-run-update-one.pre-query.hook.ts index a76d281e305..57bf1cd9008 100644 --- a/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-run-update-one.pre-query.hook.ts +++ b/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-run-update-one.pre-query.hook.ts @@ -18,7 +18,10 @@ export class WorkflowRunUpdateOnePreQueryHook _objectName: string, payload: UpdateOneResolverArgs, ): Promise> { - if (Object.keys(payload.data).length === 1 && payload.data.name) { + const allowedFields = ['name']; + const payloadKeys = Object.keys(payload.data); + + if (payloadKeys.every((key) => allowedFields.includes(key))) { return payload; } diff --git a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-run.workspace-entity.ts b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-run.workspace-entity.ts index 03d9248479a..3df4e8790bf 100644 --- a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-run.workspace-entity.ts +++ b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-run.workspace-entity.ts @@ -1,3 +1,5 @@ +import { registerEnumType } from '@nestjs/graphql'; + import { msg } from '@lingui/core/macro'; import { FieldMetadataType } from 'twenty-shared/types'; import { type WorkflowRunStepInfos } from 'twenty-shared/workflow'; @@ -40,8 +42,15 @@ export enum WorkflowRunStatus { COMPLETED = 'COMPLETED', FAILED = 'FAILED', ENQUEUED = 'ENQUEUED', + STOPPING = 'STOPPING', + STOPPED = 'STOPPED', } +registerEnumType(WorkflowRunStatus, { + name: 'WorkflowRunStatusEnum', + description: 'Status of the workflow run', +}); + export type StepOutput = { id: string; output: WorkflowActionOutput; @@ -159,6 +168,18 @@ export class WorkflowRunWorkspaceEntity extends BaseWorkspaceEntity { position: 4, color: 'blue', }, + { + value: WorkflowRunStatus.STOPPING, + label: 'Stopping', + position: 5, + color: 'orange', + }, + { + value: WorkflowRunStatus.STOPPED, + label: 'Stopped', + position: 6, + color: 'gray', + }, ], defaultValue: "'NOT_STARTED'", }) diff --git a/packages/twenty-server/src/modules/workflow/common/utils/workflow-has-running-steps.util.ts b/packages/twenty-server/src/modules/workflow/common/utils/workflow-has-running-steps.util.ts new file mode 100644 index 00000000000..7c471b165a5 --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/common/utils/workflow-has-running-steps.util.ts @@ -0,0 +1,15 @@ +import { StepStatus, type WorkflowRunStepInfos } from 'twenty-shared/workflow'; + +import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type'; + +export const workflowHasRunningSteps = ({ + stepInfos, + steps, +}: { + stepInfos: WorkflowRunStepInfos; + steps: WorkflowAction[]; +}) => { + return steps.some( + (step) => stepInfos[step.id]?.status === StepStatus.RUNNING, + ); +}; diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service.ts index bcb250d9b16..24021e63f75 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service.ts @@ -20,6 +20,7 @@ import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queu import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; import { WorkflowRunStatus } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity'; +import { workflowHasRunningSteps } from 'src/modules/workflow/common/utils/workflow-has-running-steps.util'; import { WorkflowActionFactory } from 'src/modules/workflow/workflow-executor/factories/workflow-action.factory'; import { type WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type'; import { @@ -224,6 +225,18 @@ export class WorkflowExecutorWorkspaceService { const steps = workflowRun.state.flow.steps; + if (workflowRun.status === WorkflowRunStatus.STOPPING) { + if (!workflowHasRunningSteps({ stepInfos, steps })) { + await this.workflowRunWorkspaceService.endWorkflowRun({ + workflowRunId, + workspaceId, + status: WorkflowRunStatus.STOPPED, + }); + } + + return; + } + if (workflowShouldFail({ stepInfos, steps })) { await this.workflowRunWorkspaceService.endWorkflowRun({ workflowRunId, diff --git a/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service.ts index 8e700965e13..d2dfd75814b 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service.ts @@ -190,7 +190,7 @@ export class WorkflowRunWorkspaceService { }: { workflowRunId: string; workspaceId: string; - status: WorkflowRunStatus; + status: Extract; error?: string; }) { const workflowRunToUpdate = await this.getWorkflowRunOrFail({ @@ -199,13 +199,10 @@ export class WorkflowRunWorkspaceService { }); let updatedStepInfos = {}; - const shouldUpdateStepInfos = status === WorkflowRunStatus.FAILED; - if (shouldUpdateStepInfos) { - updatedStepInfos = this.markRunningStepsAsFailed({ - stepInfosToUpdate: workflowRunToUpdate.state?.stepInfos ?? {}, - }); - } + updatedStepInfos = this.markRunningStepsAsFailed({ + stepInfosToUpdate: workflowRunToUpdate.state?.stepInfos ?? {}, + }); const partialUpdate = { status, @@ -213,7 +210,7 @@ export class WorkflowRunWorkspaceService { state: { ...workflowRunToUpdate.state, workflowRunError: error, - ...(shouldUpdateStepInfos && { stepInfos: updatedStepInfos }), + stepInfos: updatedStepInfos, }, }; @@ -223,7 +220,9 @@ export class WorkflowRunWorkspaceService { key: status === WorkflowRunStatus.COMPLETED ? MetricsKeys.WorkflowRunCompleted - : MetricsKeys.WorkflowRunFailed, + : status === WorkflowRunStatus.STOPPED + ? MetricsKeys.WorkflowRunStopped + : MetricsKeys.WorkflowRunFailed, eventId: workflowRunId, }); } @@ -378,35 +377,7 @@ export class WorkflowRunWorkspaceService { return workflowRun; } - private getInitState( - workflowVersion: WorkflowVersionWorkspaceEntity, - triggerPayload: object, - ): WorkflowRunState | undefined { - if ( - !isDefined(workflowVersion.trigger) || - !isDefined(workflowVersion.steps) - ) { - return undefined; - } - - return { - flow: { - trigger: workflowVersion.trigger, - steps: workflowVersion.steps, - }, - stepInfos: { - trigger: { status: StepStatus.NOT_STARTED, result: triggerPayload }, - ...Object.fromEntries( - workflowVersion.steps.map((step) => [ - step.id, - { status: StepStatus.NOT_STARTED }, - ]), - ), - }, - }; - } - - private async updateWorkflowRun({ + async updateWorkflowRun({ workflowRunId, workspaceId, partialUpdate, @@ -441,6 +412,34 @@ export class WorkflowRunWorkspaceService { ); } + private getInitState( + workflowVersion: WorkflowVersionWorkspaceEntity, + triggerPayload: object, + ): WorkflowRunState | undefined { + if ( + !isDefined(workflowVersion.trigger) || + !isDefined(workflowVersion.steps) + ) { + return undefined; + } + + return { + flow: { + trigger: workflowVersion.trigger, + steps: workflowVersion.steps, + }, + stepInfos: { + trigger: { status: StepStatus.NOT_STARTED, result: triggerPayload }, + ...Object.fromEntries( + workflowVersion.steps.map((step) => [ + step.id, + { status: StepStatus.NOT_STARTED }, + ]), + ), + }, + }; + } + private markRunningStepsAsFailed({ stepInfosToUpdate, }: { 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 be529c550a7..49b2124e53c 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 @@ -14,9 +14,14 @@ import { 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 { workflowHasRunningSteps } from 'src/modules/workflow/common/utils/workflow-has-running-steps.util'; 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 { + WorkflowRunException, + WorkflowRunExceptionCode, +} from 'src/modules/workflow/workflow-runner/exceptions/workflow-run.exception'; import { RunWorkflowJob } from 'src/modules/workflow/workflow-runner/jobs/run-workflow.job'; import { type RunWorkflowJobData } from 'src/modules/workflow/workflow-runner/types/run-workflow-job-data.type'; import { WorkflowRunQueueWorkspaceService } from 'src/modules/workflow/workflow-runner/workflow-run-queue/workspace-services/workflow-run-queue.workspace-service'; @@ -191,4 +196,52 @@ export class WorkflowRunnerWorkspaceService { workspaceId, ); } + + async stopWorkflowRun(workspaceId: string, workflowRunId: string) { + const workflowRun = + await this.workflowRunWorkspaceService.getWorkflowRunOrFail({ + workflowRunId, + workspaceId, + }); + + if (workflowRun.status !== WorkflowRunStatus.RUNNING) { + throw new WorkflowRunException( + 'Workflow run is not running', + WorkflowRunExceptionCode.INVALID_OPERATION, + { + userFriendlyMessage: msg`Workflow run is not running`, + }, + ); + } + + let newStatus: WorkflowRunStatus; + + if ( + workflowHasRunningSteps({ + stepInfos: workflowRun.state.stepInfos, + steps: workflowRun.state.flow.steps, + }) + ) { + await this.workflowRunWorkspaceService.updateWorkflowRun({ + workflowRunId, + workspaceId, + partialUpdate: { + status: WorkflowRunStatus.STOPPING, + }, + }); + newStatus = WorkflowRunStatus.STOPPING; + } else { + await this.workflowRunWorkspaceService.endWorkflowRun({ + workflowRunId, + workspaceId, + status: WorkflowRunStatus.STOPPED, + }); + newStatus = WorkflowRunStatus.STOPPED; + } + + return { + id: workflowRun.id, + status: newStatus, + }; + } } diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts index 73001070198..1ce8241e524 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts @@ -142,6 +142,13 @@ export class WorkflowTriggerWorkspaceService { return true; } + async stopWorkflowRun(workflowRunId: string) { + return this.workflowRunnerWorkspaceService.stopWorkflowRun( + this.getWorkspaceId(), + workflowRunId, + ); + } + private async performActivationSteps( workflow: WorkflowWorkspaceEntity, workflowVersion: WorkflowVersionWorkspaceEntity, diff --git a/packages/twenty-shared/src/workflow/schemas/workflow-run-status-schema.ts b/packages/twenty-shared/src/workflow/schemas/workflow-run-status-schema.ts index e7067f8ffbe..efb800e1f07 100644 --- a/packages/twenty-shared/src/workflow/schemas/workflow-run-status-schema.ts +++ b/packages/twenty-shared/src/workflow/schemas/workflow-run-status-schema.ts @@ -6,4 +6,6 @@ export const workflowRunStatusSchema = z.enum([ 'COMPLETED', 'FAILED', 'ENQUEUED', + 'STOPPING', + 'STOPPED', ]);