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:
+37
-10
@@ -1,4 +1,5 @@
|
||||
import { Action } from '@/action-menu/actions/components/Action';
|
||||
import { isBulkRecordsManualTrigger } from '@/action-menu/actions/record-actions/utils/isBulkRecordsManualTrigger';
|
||||
import { ActionScope } from '@/action-menu/actions/types/ActionScope';
|
||||
import { ActionType } from '@/action-menu/actions/types/ActionType';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
@@ -10,9 +11,11 @@ import { useRunWorkflowVersion } from '@/workflow/hooks/useRunWorkflowVersion';
|
||||
|
||||
import { type WorkflowVersion } from '@/workflow/types/Workflow';
|
||||
import { COMMAND_MENU_DEFAULT_ICON } from '@/workflow/workflow-trigger/constants/CommandMenuDefaultIcon';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
|
||||
export const useRunWorkflowRecordActions = ({
|
||||
objectMetadataItem,
|
||||
@@ -22,6 +25,9 @@ export const useRunWorkflowRecordActions = ({
|
||||
skip?: boolean;
|
||||
}) => {
|
||||
const { getIcon } = useIcons();
|
||||
const isIteratorEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_WORKFLOW_ITERATOR_ENABLED,
|
||||
);
|
||||
const contextStoreTargetedRecordsRule = useRecoilComponentValue(
|
||||
contextStoreTargetedRecordsRuleComponentState,
|
||||
);
|
||||
@@ -45,23 +51,44 @@ export const useRunWorkflowRecordActions = ({
|
||||
selectedRecordIds: string[],
|
||||
activeWorkflowVersion: WorkflowVersion,
|
||||
) => {
|
||||
for (const selectedRecordId of selectedRecordIds) {
|
||||
const selectedRecord = snapshot
|
||||
.getLoadable(recordStoreFamilyState(selectedRecordId))
|
||||
.getValue();
|
||||
|
||||
if (!isDefined(selectedRecord)) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
isIteratorEnabled &&
|
||||
isDefined(activeWorkflowVersion?.trigger) &&
|
||||
isBulkRecordsManualTrigger(activeWorkflowVersion.trigger)
|
||||
) {
|
||||
const objectNamePlural = objectMetadataItem.namePlural;
|
||||
const selectedRecords = selectedRecordIds
|
||||
.map((recordId) =>
|
||||
snapshot.getLoadable(recordStoreFamilyState(recordId)).getValue(),
|
||||
)
|
||||
.filter(isDefined);
|
||||
|
||||
await runWorkflowVersion({
|
||||
workflowId: activeWorkflowVersion.workflowId,
|
||||
workflowVersionId: activeWorkflowVersion.id,
|
||||
payload: selectedRecord,
|
||||
payload: {
|
||||
[objectNamePlural]: selectedRecords,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
for (const selectedRecordId of selectedRecordIds) {
|
||||
const selectedRecord = snapshot
|
||||
.getLoadable(recordStoreFamilyState(selectedRecordId))
|
||||
.getValue();
|
||||
|
||||
if (!isDefined(selectedRecord)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await runWorkflowVersion({
|
||||
workflowId: activeWorkflowVersion.workflowId,
|
||||
workflowVersionId: activeWorkflowVersion.id,
|
||||
payload: selectedRecord,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[runWorkflowVersion],
|
||||
[runWorkflowVersion, isIteratorEnabled, objectMetadataItem],
|
||||
);
|
||||
|
||||
return activeWorkflowVersions
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import { type WorkflowTrigger } from '@/workflow/types/Workflow';
|
||||
|
||||
export const isBulkRecordsManualTrigger = (trigger: WorkflowTrigger) => {
|
||||
return (
|
||||
trigger.type === 'MANUAL' &&
|
||||
trigger?.settings?.availability?.type === 'BULK_RECORDS'
|
||||
);
|
||||
};
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { type WorkflowTrigger } from '@/workflow/types/Workflow';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const isGlobalManualTrigger = (
|
||||
trigger: WorkflowTrigger,
|
||||
isIteratorEnabled: boolean,
|
||||
) => {
|
||||
if (trigger.type !== 'MANUAL') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isIteratorEnabled && isDefined(trigger.settings?.availability)) {
|
||||
return trigger.settings.availability.type === 'GLOBAL';
|
||||
}
|
||||
|
||||
return !isDefined(trigger.settings.objectType);
|
||||
};
|
||||
+22
-7
@@ -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),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
+1
@@ -158,6 +158,7 @@ export const WorkflowEditTriggerManual = ({
|
||||
type: availability.type,
|
||||
objectNameSingular,
|
||||
},
|
||||
objectType: objectNameSingular,
|
||||
outputSchema: {},
|
||||
},
|
||||
});
|
||||
|
||||
+4
@@ -197,6 +197,10 @@ export const WorkflowEditTriggerManualDeprecated = ({
|
||||
...trigger,
|
||||
settings: {
|
||||
...trigger.settings,
|
||||
availability: {
|
||||
objectNameSingular: updatedObject,
|
||||
type: 'SINGLE_RECORD',
|
||||
},
|
||||
objectType: updatedObject,
|
||||
outputSchema: {},
|
||||
},
|
||||
|
||||
+63
-49
@@ -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.");
|
||||
});
|
||||
|
||||
+4
@@ -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,
|
||||
|
||||
+3
@@ -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:
|
||||
|
||||
+9
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user