Stop workflow in any status (#17271)

Workflows can now be cancelled when not started or enqueued.
Also displaying the stop option when selecting all.

We got the case of a client that did an infinite loop. So he had a lot
of workflows to stop. Supporting select all would have avoid him to wait
for 4 hours so we process the runs throttled. This is not something that
is supposed to happen often so I did not see the point of refactoring
the endpoint. But I wanted the users to have this option just in case
This commit is contained in:
Thomas Trompette
2026-01-20 17:38:39 +01:00
committed by GitHub
parent e40130f575
commit 799b08e1f2
7 changed files with 82 additions and 16 deletions
@@ -60,8 +60,13 @@ export const WORKFLOW_RUNS_ACTIONS_CONFIG = inheritActionsFromDefaultConfig({
type: ActionType.Standard,
scope: ActionScope.RecordSelection,
Icon: IconPlayerStop,
shouldBeRegistered: ({ selectedRecord }) => {
return selectedRecord?.status === 'RUNNING';
shouldBeRegistered: ({ selectedRecord, isSelectAll }) => {
if (isSelectAll === true) {
return true;
}
const stoppableStatuses = ['NOT_STARTED', 'ENQUEUED', 'RUNNING'];
return stoppableStatuses.includes(selectedRecord?.status);
},
availableOn: [
ActionViewType.SHOW_PAGE,
@@ -1,27 +1,67 @@
import { Action } from '@/action-menu/actions/components/Action';
import { contextStoreAnyFieldFilterValueComponentState } from '@/context-store/states/contextStoreAnyFieldFilterValueComponentState';
import { contextStoreFilterGroupsComponentState } from '@/context-store/states/contextStoreFilterGroupsComponentState';
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize';
import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords';
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useStopWorkflowRun } from '@/workflow/hooks/useStopWorkflowRun';
export const StopWorkflowRunSingleRecordAction = () => {
const { objectMetadataItem } = useRecordIndexIdFromCurrentContextStore();
const contextStoreTargetedRecordsRule = useRecoilComponentValue(
contextStoreTargetedRecordsRuleComponentState,
);
if (
contextStoreTargetedRecordsRule.mode === 'exclusion' ||
(contextStoreTargetedRecordsRule.mode === 'selection' &&
contextStoreTargetedRecordsRule.selectedRecordIds.length === 0)
) {
throw new Error('Selected record ID is required');
}
const contextStoreFilters = useRecoilComponentValue(
contextStoreFiltersComponentState,
);
const contextStoreFilterGroups = useRecoilComponentValue(
contextStoreFilterGroupsComponentState,
);
const contextStoreAnyFieldFilterValue = useRecoilComponentValue(
contextStoreAnyFieldFilterValueComponentState,
);
const { filterValueDependencies } = useFilterValueDependencies();
const graphqlFilter = computeContextStoreFilters({
contextStoreTargetedRecordsRule,
contextStoreFilters,
contextStoreFilterGroups,
objectMetadataItem,
filterValueDependencies,
contextStoreAnyFieldFilterValue,
});
const { fetchAllRecords: fetchAllRecordIds } = useLazyFetchAllRecords({
objectNameSingular: CoreObjectNameSingular.WorkflowRun,
filter: graphqlFilter,
limit: DEFAULT_QUERY_PAGE_SIZE,
recordGqlFields: { id: true },
});
const selectedRecordIds = contextStoreTargetedRecordsRule.selectedRecordIds;
const { stopWorkflowRun } = useStopWorkflowRun();
const handleClick = async () => {
for (const selectedRecordId of selectedRecordIds) {
await stopWorkflowRun(selectedRecordId);
if (contextStoreTargetedRecordsRule.mode === 'selection') {
for (const selectedRecordId of contextStoreTargetedRecordsRule.selectedRecordIds) {
await stopWorkflowRun(selectedRecordId);
}
} else {
const records = await fetchAllRecordIds();
for (const record of records) {
await stopWorkflowRun(record.id);
}
}
};
@@ -15,6 +15,8 @@ export type ShouldBeRegisteredFunctionParams = {
isFavorite?: boolean;
isRemote?: boolean;
isNoteOrTask?: boolean;
isSelectAll?: boolean;
loadedRecords?: ObjectRecord[];
selectedRecord?: ObjectRecord;
numberOfSelectedRecords?: number;
workflowWithCurrentVersion?: WorkflowWithCurrentVersion;
@@ -73,6 +73,8 @@ export const useShouldActionBeRegisteredParams = ({
contextStoreTargetedRecordsRule,
);
const isSelectAll = contextStoreTargetedRecordsRule.mode === 'exclusion';
const getObjectReadPermission = useRecoilCallback(
({ snapshot }) =>
(objectMetadataNameSingular: string) => {
@@ -113,6 +115,7 @@ export const useShouldActionBeRegisteredParams = ({
isInRightDrawer,
hasAnySoftDeleteFilterOnView,
isShowPage,
isSelectAll,
selectedRecord,
numberOfSelectedRecords,
viewType: viewType ?? undefined,
@@ -80,6 +80,13 @@ export class RunWorkflowJob {
workspaceId,
});
if (
workflowRun.status !== WorkflowRunStatus.ENQUEUED &&
workflowRun.status !== WorkflowRunStatus.NOT_STARTED
) {
return;
}
const workflowVersion =
await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail({
workspaceId,
@@ -156,7 +156,10 @@ export class WorkflowRunWorkspaceService {
workflowRunToUpdate.status !== WorkflowRunStatus.ENQUEUED &&
workflowRunToUpdate.status !== WorkflowRunStatus.NOT_STARTED
) {
return;
throw new WorkflowRunException(
'Workflow run is not enqueued or not started',
WorkflowRunExceptionCode.INVALID_OPERATION,
);
}
const partialUpdate = {
@@ -200,12 +200,18 @@ export class WorkflowRunnerWorkspaceService {
workspaceId,
});
if (workflowRun.status !== WorkflowRunStatus.RUNNING) {
const stoppableStatuses = [
WorkflowRunStatus.NOT_STARTED,
WorkflowRunStatus.ENQUEUED,
WorkflowRunStatus.RUNNING,
];
if (!stoppableStatuses.includes(workflowRun.status)) {
throw new WorkflowRunException(
'Workflow run is not running',
'Workflow run cannot be stopped',
WorkflowRunExceptionCode.INVALID_OPERATION,
{
userFriendlyMessage: msg`Workflow run is not running`,
userFriendlyMessage: msg`Workflow run cannot be stopped in its current status`,
},
);
}