diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 3cf53dc876..aad4f42d90 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -980,6 +980,7 @@ export enum FeatureFlagKey { IS_STRIPE_INTEGRATION_ENABLED = 'IS_STRIPE_INTEGRATION_ENABLED', IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED', IS_WORKFLOW_BRANCH_ENABLED = 'IS_WORKFLOW_BRANCH_ENABLED', + IS_WORKFLOW_ITERATOR_ENABLED = 'IS_WORKFLOW_ITERATOR_ENABLED', IS_WORKSPACE_MIGRATION_V2_ENABLED = 'IS_WORKSPACE_MIGRATION_V2_ENABLED' } diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index 39d7feae44..ab86491a2b 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -944,6 +944,7 @@ export enum FeatureFlagKey { IS_STRIPE_INTEGRATION_ENABLED = 'IS_STRIPE_INTEGRATION_ENABLED', IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED', IS_WORKFLOW_BRANCH_ENABLED = 'IS_WORKFLOW_BRANCH_ENABLED', + IS_WORKFLOW_ITERATOR_ENABLED = 'IS_WORKFLOW_ITERATOR_ENABLED', IS_WORKSPACE_MIGRATION_V2_ENABLED = 'IS_WORKSPACE_MIGRATION_V2_ENABLED' } 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/action/components/CommandMenuWorkflowSelectActionContent.tsx index b355721b73..3cc6cf0eb9 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/action/components/CommandMenuWorkflowSelectActionContent.tsx @@ -16,6 +16,7 @@ import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/s 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'; @@ -91,6 +92,9 @@ export const CommandMenuWorkflowSelectActionContent = ({ const theme = useTheme(); const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED); + const isIteratorEnabled = useIsFeatureEnabled( + FeatureFlagKey.IS_WORKFLOW_ITERATOR_ENABLED, + ); const { t } = useLingui(); @@ -116,6 +120,14 @@ export const CommandMenuWorkflowSelectActionContent = ({ {t`Human Input`} {WorkflowActionMenuItems(HUMAN_INPUT_ACTIONS, theme, handleCreateStep)} + {isIteratorEnabled && ( + <> + + {t`Others`} + + {WorkflowActionMenuItems(OTHER_ACTIONS, theme, handleCreateStep)} + + )} ); }; diff --git a/packages/twenty-front/src/modules/workflow/types/Workflow.ts b/packages/twenty-front/src/modules/workflow/types/Workflow.ts index 53973041df..cdd250367e 100644 --- a/packages/twenty-front/src/modules/workflow/types/Workflow.ts +++ b/packages/twenty-front/src/modules/workflow/types/Workflow.ts @@ -9,6 +9,7 @@ import { type workflowFindRecordsActionSchema, type workflowFormActionSchema, type workflowHttpRequestActionSchema, + type workflowIteratorActionSchema, type workflowManualTriggerSchema, type workflowRunSchema, type workflowRunStateSchema, @@ -42,7 +43,9 @@ export type WorkflowFormAction = z.infer; export type WorkflowHttpRequestAction = z.infer< typeof workflowHttpRequestActionSchema >; - +export type WorkflowIteratorAction = z.infer< + typeof workflowIteratorActionSchema +>; export type WorkflowAiAgentAction = z.infer; export type WorkflowAction = @@ -55,7 +58,8 @@ export type WorkflowAction = | WorkflowFilterAction | WorkflowFormAction | WorkflowHttpRequestAction - | WorkflowAiAgentAction; + | WorkflowAiAgentAction + | WorkflowIteratorAction; export type WorkflowActionType = WorkflowAction['type']; export type WorkflowStep = WorkflowAction; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowRunStepNodeDetail.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowRunStepNodeDetail.tsx index 1e564ded2b..c6396fb686 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowRunStepNodeDetail.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowRunStepNodeDetail.tsx @@ -14,6 +14,7 @@ import { WorkflowEditActionFilter } from '@/workflow/workflow-steps/workflow-act import { WorkflowEditActionFindRecords } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords'; import { WorkflowEditActionFormFiller } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormFiller'; import { WorkflowEditActionHttpRequest } from '@/workflow/workflow-steps/workflow-actions/http-request-action/components/WorkflowEditActionHttpRequest'; +import { WorkflowEditActionIterator } from '@/workflow/workflow-steps/workflow-actions/iterator-action/WorkflowEditActionIterator'; import { WorkflowEditTriggerCronForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerCronForm'; import { WorkflowEditTriggerDatabaseEventForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerDatabaseEventForm'; import { WorkflowEditTriggerManualForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerManualForm'; @@ -214,6 +215,17 @@ export const WorkflowRunStepNodeDetail = ({ /> ); } + case 'ITERATOR': { + return ( + + ); + } } } } diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowStepDetail.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowStepDetail.tsx index 732ae3cb27..146c5a4949 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowStepDetail.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowStepDetail.tsx @@ -13,6 +13,7 @@ import { WorkflowEditActionFilter } from '@/workflow/workflow-steps/workflow-act import { WorkflowEditActionFindRecords } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords'; import { WorkflowEditActionFormBuilder } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormBuilder'; import { WorkflowEditActionHttpRequest } from '@/workflow/workflow-steps/workflow-actions/http-request-action/components/WorkflowEditActionHttpRequest'; +import { WorkflowEditActionIterator } from '@/workflow/workflow-steps/workflow-actions/iterator-action/WorkflowEditActionIterator'; import { WorkflowEditTriggerCronForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerCronForm'; import { WorkflowEditTriggerDatabaseEventForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerDatabaseEventForm'; import { WorkflowEditTriggerManualForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerManualForm'; @@ -196,7 +197,15 @@ export const WorkflowStepDetail = ({ /> ); } - + case 'ITERATOR': { + return ( + + ); + } default: return assertUnreachable( stepDefinition.definition, diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/constants/OtherActions.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/constants/OtherActions.ts index 141c77440a..6791bd27dd 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/constants/OtherActions.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/constants/OtherActions.ts @@ -2,35 +2,12 @@ import { type WorkflowActionType } from '@/workflow/types/Workflow'; export const OTHER_ACTIONS: Array<{ label: string; - type: Exclude< - WorkflowActionType, - 'CREATE_RECORD' | 'UPDATE_RECORD' | 'DELETE_RECORD' | 'FIND_RECORDS' - >; + type: Extract; icon: string; }> = [ { - label: 'Send Email', - type: 'SEND_EMAIL', - icon: 'IconSend', - }, - { - label: 'Code', - type: 'CODE', - icon: 'IconCode', - }, - { - label: 'Form', - type: 'FORM', - icon: 'IconForms', - }, - { - label: 'HTTP Request', - type: 'HTTP_REQUEST', - icon: 'IconWorld', - }, - { - label: 'AI Agent', - type: 'AI_AGENT', - icon: 'IconBrain', + label: 'Iterator', + type: 'ITERATOR', + icon: 'IconRepeat', }, ]; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/iterator-action/WorkflowEditActionIterator.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/iterator-action/WorkflowEditActionIterator.tsx new file mode 100644 index 0000000000..e03a7e81ad --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/iterator-action/WorkflowEditActionIterator.tsx @@ -0,0 +1,121 @@ +import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput'; +import { type WorkflowIteratorAction } from '@/workflow/types/Workflow'; +import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody'; +import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader'; +import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader'; +import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker'; +import { useLingui } from '@lingui/react/macro'; +import { useState } from 'react'; +import { useDebouncedCallback } from 'use-debounce'; + +type WorkflowEditActionIteratorProps = { + action: WorkflowIteratorAction; + actionOptions: + | { + readonly: true; + } + | { + readonly?: false; + onActionUpdate: (action: WorkflowIteratorAction) => void; + }; +}; + +export const WorkflowEditActionIterator = ({ + action, + actionOptions, +}: WorkflowEditActionIteratorProps) => { + const { headerTitle, headerIcon, headerIconColor, headerType, getIcon } = + useWorkflowActionHeader({ + action, + defaultTitle: 'Iterator', + }); + + const { t } = useLingui(); + + const [formData, setFormData] = useState({ + items: action.settings.input.items || [], + initialLoopStepIds: action.settings.input.initialLoopStepIds || [], + }); + + const saveAction = useDebouncedCallback( + (updatedFormData: typeof formData) => { + if (actionOptions.readonly === true) { + return; + } + + actionOptions.onActionUpdate?.({ + ...action, + settings: { + ...action.settings, + input: { + items: updatedFormData.items, + initialLoopStepIds: updatedFormData.initialLoopStepIds, + }, + }, + }); + }, + 1000, + ); + + const handleFieldChange = (field: string, value: any) => { + if (actionOptions.readonly === true) { + return; + } + + const updatedFormData = { ...formData, [field]: value }; + setFormData(updatedFormData); + saveAction(updatedFormData); + }; + + const handleTitleChange = (newName: string) => { + if (actionOptions.readonly === true) { + return; + } + + actionOptions.onActionUpdate?.({ + ...action, + name: newName, + }); + }; + + return ( + <> + + + handleFieldChange('items', value)} + readonly={actionOptions.readonly} + VariablePicker={WorkflowVariablePicker} + /> + {/* TODO: remove once the UI is implemented */} + + handleFieldChange('initialLoopStepIds', value) + } + readonly={actionOptions.readonly} + /> + + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getActionHeaderTypeOrThrow.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getActionHeaderTypeOrThrow.ts index 306d09aabf..e065250579 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getActionHeaderTypeOrThrow.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getActionHeaderTypeOrThrow.ts @@ -20,7 +20,9 @@ export const getActionHeaderTypeOrThrow = (actionType: WorkflowActionType) => { case 'FILTER': { return msg`Filter`; } - + case 'ITERATOR': { + return msg`Iterator`; + } default: assertUnreachable(actionType, `Unsupported action type: ${actionType}`); } diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getActionIcon.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getActionIcon.ts index 1da2091338..040fb29b15 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getActionIcon.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getActionIcon.ts @@ -21,6 +21,8 @@ export const getActionIcon = (actionType: WorkflowActionType) => { return CORE_ACTIONS.find((item) => item.type === actionType)?.icon; case 'FORM': return HUMAN_INPUT_ACTIONS.find((item) => item.type === actionType)?.icon; + case 'ITERATOR': + return 'IconRepeat'; default: return 'IconDefault'; } diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getActionIconColorOrThrow.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getActionIconColorOrThrow.ts index 9adfb9eff8..52e87d047f 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getActionIconColorOrThrow.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getActionIconColorOrThrow.ts @@ -21,6 +21,7 @@ export const getActionIconColorOrThrow = ({ return theme.font.color.tertiary; case 'FORM': return theme.color.orange; + case 'ITERATOR': case 'FILTER': return theme.font.color.tertiary; case 'AI_AGENT': diff --git a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts index 6ee344972c..932eed7933 100644 --- a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts +++ b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts @@ -15,4 +15,5 @@ export enum FeatureFlagKey { IS_API_KEY_ROLES_ENABLED = 'IS_API_KEY_ROLES_ENABLED', IS_PAGE_LAYOUT_ENABLED = 'IS_PAGE_LAYOUT_ENABLED', IS_MESSAGE_FOLDER_CONTROL_ENABLED = 'IS_MESSAGE_FOLDER_CONTROL_ENABLED', + IS_WORKFLOW_ITERATOR_ENABLED = 'IS_WORKFLOW_ITERATOR_ENABLED', } diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts index 6c6ebfa3e1..047c6e4ebf 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts @@ -138,6 +138,7 @@ describe('WorkspaceEntityManager', () => { IS_API_KEY_ROLES_ENABLED: false, IS_PAGE_LAYOUT_ENABLED: false, IS_MESSAGE_FOLDER_CONTROL_ENABLED: false, + IS_WORKFLOW_ITERATOR_ENABLED: false, }, eventEmitterService: { emitMutationEvent: jest.fn(), @@ -163,6 +164,7 @@ describe('WorkspaceEntityManager', () => { IS_WORKSPACE_MIGRATION_V2_ENABLED: false, IS_API_KEY_ROLES_ENABLED: false, IS_PAGE_LAYOUT_ENABLED: false, + IS_WORKFLOW_ITERATOR_ENABLED: false, }, permissionsPerRoleId: {}, } as WorkspaceDataSource; diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts index 4d816645d5..caa96f75a7 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts @@ -80,6 +80,11 @@ export const seedFeatureFlags = async ( workspaceId: workspaceId, value: true, }, + { + key: FeatureFlagKey.IS_WORKFLOW_ITERATOR_ENABLED, + workspaceId: workspaceId, + value: false, + }, ]) .execute(); }; diff --git a/packages/twenty-shared/src/workflow/index.ts b/packages/twenty-shared/src/workflow/index.ts index bb9e08d34f..7befcc6f52 100644 --- a/packages/twenty-shared/src/workflow/index.ts +++ b/packages/twenty-shared/src/workflow/index.ts @@ -23,6 +23,7 @@ export { workflowHttpRequestActionSettingsSchema, workflowAiAgentActionSettingsSchema, workflowFilterActionSettingsSchema, + workflowIteratorActionSettingsSchema, workflowCodeActionSchema, workflowSendEmailActionSchema, workflowCreateRecordActionSchema, @@ -33,6 +34,7 @@ export { workflowHttpRequestActionSchema, workflowAiAgentActionSchema, workflowFilterActionSchema, + workflowIteratorActionSchema, workflowActionSchema, workflowDatabaseEventTriggerSchema, workflowManualTriggerSchema, diff --git a/packages/twenty-shared/src/workflow/schemas/workflow.schema.ts b/packages/twenty-shared/src/workflow/schemas/workflow.schema.ts index c56dffffb8..a78ddbde99 100644 --- a/packages/twenty-shared/src/workflow/schemas/workflow.schema.ts +++ b/packages/twenty-shared/src/workflow/schemas/workflow.schema.ts @@ -219,6 +219,15 @@ export const workflowFilterActionSettingsSchema = }), }); +export const workflowIteratorActionSettingsSchema = + baseWorkflowActionSettingsSchema.extend({ + input: z.object({ + items: z.union([z.array(z.any()), z.string()]).optional(), + // TODO: should never be a string once fix the UI + initialLoopStepIds: z.union([z.array(z.string()), z.string()]).optional(), + }), + }); + // Action schemas export const workflowCodeActionSchema = baseWorkflowActionSchema.extend({ type: z.literal('CODE'), @@ -270,6 +279,11 @@ export const workflowFilterActionSchema = baseWorkflowActionSchema.extend({ settings: workflowFilterActionSettingsSchema, }); +export const workflowIteratorActionSchema = baseWorkflowActionSchema.extend({ + type: z.literal('ITERATOR'), + settings: workflowIteratorActionSettingsSchema, +}); + // Combined action schema export const workflowActionSchema = z.discriminatedUnion('type', [ workflowCodeActionSchema, @@ -282,6 +296,7 @@ export const workflowActionSchema = z.discriminatedUnion('type', [ workflowHttpRequestActionSchema, workflowAiAgentActionSchema, workflowFilterActionSchema, + workflowIteratorActionSchema, ]); // Trigger schemas