Workflow run over a list of records + backfill availability on manual triggers (#14761)

- Allow to run workflow on a list of records
- Use new availability in manual trigger when iterator feature flag is
enabled
- Build a command to backfill availability in workflow trigger



https://github.com/user-attachments/assets/d685c01f-4059-4647-92a1-f5f529b560cf
This commit is contained in:
Thomas Trompette
2025-09-29 15:45:51 +02:00
committed by GitHub
parent 59be3aab19
commit 4329501743
17 changed files with 285 additions and 85 deletions
@@ -1,3 +1,4 @@
import { isGlobalManualTrigger } from '@/action-menu/actions/record-actions/utils/isGlobalManualTrigger';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
@@ -7,7 +8,9 @@ import {
type ManualTriggerWorkflowVersion,
type Workflow,
} from '@/workflow/types/Workflow';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { isDefined } from 'twenty-shared/utils';
import { FeatureFlagKey } from '~/generated/graphql';
export const useActiveWorkflowVersionsWithManualTrigger = ({
objectMetadataItem,
@@ -16,6 +19,10 @@ export const useActiveWorkflowVersionsWithManualTrigger = ({
objectMetadataItem?: ObjectMetadataItem;
skip?: boolean;
}) => {
const isIteratorEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_WORKFLOW_ITERATOR_ENABLED,
);
const filters = [
{
status: {
@@ -29,12 +36,20 @@ export const useActiveWorkflowVersionsWithManualTrigger = ({
},
];
const objectTypeFilter = isIteratorEnabled
? {
trigger: {
like: `%"objectNameSingular": "${objectMetadataItem?.nameSingular}"%`,
},
}
: {
trigger: {
like: `%"objectType": "${objectMetadataItem?.nameSingular}"%`,
},
};
if (isDefined(objectMetadataItem)) {
filters.push({
trigger: {
like: `%"objectType": "${objectMetadataItem.nameSingular}"%`,
},
});
filters.push(objectTypeFilter);
}
const { objectMetadataItem: workflowVersionObjectMetadataItem } =
@@ -64,8 +79,8 @@ export const useActiveWorkflowVersionsWithManualTrigger = ({
records: records.filter(
(record) =>
record.status === 'ACTIVE' &&
record.trigger?.type === 'MANUAL' &&
!isDefined(record.trigger?.settings.objectType),
isDefined(record.trigger) &&
isGlobalManualTrigger(record.trigger, isIteratorEnabled),
),
};
}
@@ -158,6 +158,7 @@ export const WorkflowEditTriggerManual = ({
type: availability.type,
objectNameSingular,
},
objectType: objectNameSingular,
outputSchema: {},
},
});
@@ -197,6 +197,10 @@ export const WorkflowEditTriggerManualDeprecated = ({
...trigger,
settings: {
...trigger.settings,
availability: {
objectNameSingular: updatedObject,
type: 'SINGLE_RECORD',
},
objectType: updatedObject,
outputSchema: {},
},
@@ -3,57 +3,71 @@ import { COMMAND_MENU_DEFAULT_ICON } from '@/workflow/workflow-trigger/constants
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
import { getManualTriggerDefaultSettingsDeprecated } from '../getManualTriggerDefaultSettingsDeprecated';
it('returns settings for a manual trigger that can be activated from any where', () => {
expect(
getManualTriggerDefaultSettingsDeprecated({
availability: 'EVERYWHERE',
activeNonSystemObjectMetadataItems: generatedMockObjectMetadataItems,
}),
).toStrictEqual({
objectType: undefined,
outputSchema: {},
icon: COMMAND_MENU_DEFAULT_ICON,
isPinned: false,
describe('getManualTriggerDefaultSettingsDeprecated', () => {
it('returns settings for a manual trigger that can be activated from any where', () => {
expect(
getManualTriggerDefaultSettingsDeprecated({
availability: 'EVERYWHERE',
activeNonSystemObjectMetadataItems: generatedMockObjectMetadataItems,
}),
).toStrictEqual({
objectType: undefined,
outputSchema: {},
icon: COMMAND_MENU_DEFAULT_ICON,
isPinned: false,
availability: {
type: 'GLOBAL',
locations: [],
},
});
});
});
it('returns settings for a manual trigger that can be activated from any where', () => {
expect(
getManualTriggerDefaultSettingsDeprecated({
availability: 'WHEN_RECORD_SELECTED',
activeNonSystemObjectMetadataItems: generatedMockObjectMetadataItems,
it('returns settings for a manual trigger that can be activated from any where', () => {
expect(
getManualTriggerDefaultSettingsDeprecated({
availability: 'WHEN_RECORD_SELECTED',
activeNonSystemObjectMetadataItems: generatedMockObjectMetadataItems,
icon: 'IconTest',
}),
).toStrictEqual({
objectType: generatedMockObjectMetadataItems[0].nameSingular,
outputSchema: {},
icon: 'IconTest',
}),
).toStrictEqual({
objectType: generatedMockObjectMetadataItems[0].nameSingular,
outputSchema: {},
icon: 'IconTest',
isPinned: false,
isPinned: false,
availability: {
type: 'SINGLE_RECORD',
objectNameSingular: generatedMockObjectMetadataItems[0].nameSingular,
},
});
});
it('returns settings for WHEN_RECORD_SELECTED with default icon when no custom icon provided', () => {
expect(
getManualTriggerDefaultSettingsDeprecated({
availability: 'WHEN_RECORD_SELECTED',
activeNonSystemObjectMetadataItems: generatedMockObjectMetadataItems,
}),
).toStrictEqual({
objectType: generatedMockObjectMetadataItems[0].nameSingular,
outputSchema: {},
icon: COMMAND_MENU_DEFAULT_ICON,
isPinned: false,
availability: {
type: 'SINGLE_RECORD',
objectNameSingular: generatedMockObjectMetadataItems[0].nameSingular,
},
});
});
it('throws error for unsupported availability type', () => {
const invalidAvailability =
'INVALID_AVAILABILITY' as WorkflowManualTriggerAvailability;
expect(() =>
getManualTriggerDefaultSettingsDeprecated({
availability: invalidAvailability,
activeNonSystemObjectMetadataItems: generatedMockObjectMetadataItems,
}),
).toThrow("Didn't expect to get here.");
});
});
it('returns settings for WHEN_RECORD_SELECTED with default icon when no custom icon provided', () => {
expect(
getManualTriggerDefaultSettingsDeprecated({
availability: 'WHEN_RECORD_SELECTED',
activeNonSystemObjectMetadataItems: generatedMockObjectMetadataItems,
}),
).toStrictEqual({
objectType: generatedMockObjectMetadataItems[0].nameSingular,
outputSchema: {},
icon: COMMAND_MENU_DEFAULT_ICON,
isPinned: false,
});
});
it('throws error for unsupported availability type', () => {
const invalidAvailability =
'INVALID_AVAILABILITY' as WorkflowManualTriggerAvailability;
expect(() =>
getManualTriggerDefaultSettingsDeprecated({
availability: invalidAvailability,
activeNonSystemObjectMetadataItems: generatedMockObjectMetadataItems,
}),
).toThrow("Didn't expect to get here.");
});
@@ -116,6 +116,10 @@ describe('getTriggerDefaultDefinition', () => {
name: 'Launch manually',
settings: {
objectType: generatedMockObjectMetadataItems[0].nameSingular,
availability: {
objectNameSingular: generatedMockObjectMetadataItems[0].nameSingular,
type: 'SINGLE_RECORD',
},
outputSchema: {},
icon: COMMAND_MENU_DEFAULT_ICON,
isPinned: false,
@@ -17,6 +17,7 @@ export const getManualTriggerDefaultSettings = ({
switch (availabilityType) {
case 'GLOBAL': {
return {
objectType: undefined,
availability: {
type: 'GLOBAL',
locations: undefined,
@@ -28,6 +29,7 @@ export const getManualTriggerDefaultSettings = ({
}
case 'SINGLE_RECORD': {
return {
objectType: activeNonSystemObjectMetadataItems[0].nameSingular,
availability: {
type: 'SINGLE_RECORD',
objectNameSingular:
@@ -40,6 +42,7 @@ export const getManualTriggerDefaultSettings = ({
}
case 'BULK_RECORDS': {
return {
objectType: activeNonSystemObjectMetadataItems[0].nameSingular,
availability: {
type: 'BULK_RECORDS',
objectNameSingular:
@@ -24,6 +24,10 @@ export const getManualTriggerDefaultSettingsDeprecated = ({
outputSchema: {},
icon: icon || COMMAND_MENU_DEFAULT_ICON,
isPinned: isPinned || false,
availability: {
type: 'GLOBAL',
locations: [],
},
};
}
case 'WHEN_RECORD_SELECTED': {
@@ -32,6 +36,11 @@ export const getManualTriggerDefaultSettingsDeprecated = ({
outputSchema: {},
icon: icon || COMMAND_MENU_DEFAULT_ICON,
isPinned: isPinned || false,
availability: {
type: 'SINGLE_RECORD',
objectNameSingular:
activeNonSystemObjectMetadataItems[0].nameSingular,
},
};
}
}