diff --git a/packages/twenty-front/src/modules/workflow/types/Workflow.ts b/packages/twenty-front/src/modules/workflow/types/Workflow.ts index 1d615254e31..4f82c724c60 100644 --- a/packages/twenty-front/src/modules/workflow/types/Workflow.ts +++ b/packages/twenty-front/src/modules/workflow/types/Workflow.ts @@ -23,6 +23,7 @@ import { type workflowTriggerSchema, type workflowUpdateRecordActionSchema, type workflowWebhookTriggerSchema, + type workflowDelayActionSchema, } from 'twenty-shared/workflow'; import { type z } from 'zod'; @@ -42,6 +43,7 @@ export type WorkflowDeleteRecordAction = z.infer< export type WorkflowFindRecordsAction = z.infer< typeof workflowFindRecordsActionSchema >; +export type WorkflowDelayAction = z.infer; export type WorkflowFilterAction = z.infer; export type WorkflowFormAction = z.infer; export type WorkflowHttpRequestAction = z.infer< @@ -65,6 +67,7 @@ export type WorkflowAction = | WorkflowHttpRequestAction | WorkflowAiAgentAction | WorkflowIteratorAction + | WorkflowDelayAction | WorkflowEmptyAction; export type WorkflowActionType = WorkflowAction['type']; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeIcon.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeIcon.tsx index 5d7936d242a..ed95dac5948 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeIcon.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeIcon.tsx @@ -47,9 +47,12 @@ export const WorkflowDiagramStepNodeIcon = ({ case 'AI_AGENT': { return ; } - case 'EMPTY': { + case 'EMPTY': return null; - } + case 'DELAY': + case 'FILTER': + case 'ITERATOR': + return ; default: { return ( ); } + case 'DELAY': { + 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 e4110387ca5..0d9a5433f18 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 @@ -10,6 +10,7 @@ import { WorkflowEditActionDeleteRecord } from '@/workflow/workflow-steps/workfl import { WorkflowEditActionEmpty } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionEmpty'; import { WorkflowEditActionSendEmail } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionSendEmail'; import { WorkflowEditActionUpdateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord'; +import { WorkflowEditActionDelay } from '@/workflow/workflow-steps/workflow-actions/delay-actions/components/WorkflowEditActionDelay'; import { WorkflowEditActionFilter } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter'; 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'; @@ -210,6 +211,15 @@ export const WorkflowStepDetail = ({ case 'EMPTY': { return ; } + case 'DELAY': { + return ( + + ); + } default: return assertUnreachable( stepDefinition.definition, diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/constants/FlowActions.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/constants/FlowActions.ts index b13f8f591b4..1c57657c8bd 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/constants/FlowActions.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/constants/FlowActions.ts @@ -2,7 +2,7 @@ import { type WorkflowActionType } from '@/workflow/types/Workflow'; export const FLOW_ACTIONS: Array<{ label: string; - type: Extract; + type: Extract; icon: string; }> = [ { @@ -15,4 +15,9 @@ export const FLOW_ACTIONS: Array<{ type: 'FILTER', icon: 'IconFilter', }, + { + label: 'Delay', + type: 'DELAY', + icon: 'IconPlayerPause', + }, ]; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/delay-actions/components/WorkflowEditActionDelay.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/delay-actions/components/WorkflowEditActionDelay.tsx new file mode 100644 index 00000000000..08a119b4ed8 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/delay-actions/components/WorkflowEditActionDelay.tsx @@ -0,0 +1,225 @@ +import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader'; +import { FormDateTimeFieldInput } from '@/object-record/record-field/ui/form-types/components/FormDateTimeFieldInput'; +import { FormNumberFieldInput } from '@/object-record/record-field/ui/form-types/components/FormNumberFieldInput'; +import { Select } from '@/ui/input/components/Select'; +import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; +import { type WorkflowDelayAction } from '@/workflow/types/Workflow'; +import { WorkflowActionFooter } from '@/workflow/workflow-steps/components/WorkflowActionFooter'; +import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody'; +import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader'; +import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker'; +import { t } from '@lingui/core/macro'; +import { + HorizontalSeparator, + IconCalendar, + IconHourglassHigh, +} from 'twenty-ui/display'; +import { type SelectOption } from 'twenty-ui/input'; +type WorkflowEditActionDelayProps = { + action: WorkflowDelayAction; + actionOptions: + | { + readonly: true; + } + | { + readonly?: false; + onActionUpdate: (action: WorkflowDelayAction) => void; + }; +}; + +export const WorkflowEditActionDelay = ({ + action, + actionOptions, +}: WorkflowEditActionDelayProps) => { + const { headerTitle, headerIcon, headerIconColor, headerType, getIcon } = + useWorkflowActionHeader({ + action, + defaultTitle: 'Delay', + }); + + const delayOptions: Array> = [ + { + label: t`At a specific date or time`, + value: 'SCHEDULED_DATE', + Icon: IconCalendar, + }, + { + label: t`After a set amount of time`, + value: 'DURATION', + Icon: IconHourglassHigh, + }, + ]; + + const handleDelayTypeChange = ( + newDelayType: 'SCHEDULED_DATE' | 'DURATION', + ) => { + if ( + actionOptions.readonly === true || + newDelayType === action.settings.input.delayType + ) { + return; + } + + if (newDelayType === 'SCHEDULED_DATE') { + actionOptions.onActionUpdate({ + ...action, + settings: { + ...action.settings, + input: { + delayType: 'SCHEDULED_DATE', + }, + }, + }); + } else { + actionOptions.onActionUpdate({ + ...action, + settings: { + ...action.settings, + input: { + delayType: 'DURATION', + duration: undefined, + }, + }, + }); + } + }; + + const handleDateTimeChange = (value: string | null) => { + if (actionOptions.readonly === true) { + return; + } + + actionOptions.onActionUpdate({ + ...action, + settings: { + ...action.settings, + input: { + delayType: 'SCHEDULED_DATE', + scheduledDateTime: value ?? '', + }, + }, + }); + }; + + const handleDurationChange = ( + field: 'days' | 'hours' | 'minutes' | 'seconds', + value: number | string | null, + ) => { + if (actionOptions.readonly === true) { + return; + } + + actionOptions.onActionUpdate({ + ...action, + settings: { + ...action.settings, + input: { + delayType: 'DURATION', + duration: { + days: + field === 'days' + ? (value ?? undefined) + : action.settings.input.duration?.days, + hours: + field === 'hours' + ? (value ?? undefined) + : action.settings.input.duration?.hours, + minutes: + field === 'minutes' + ? (value ?? undefined) + : action.settings.input.duration?.minutes, + seconds: + field === 'seconds' + ? (value ?? undefined) + : action.settings.input.duration?.seconds, + }, + }, + }, + }); + }; + + const HeaderIcon = getIcon(headerIcon ?? 'IconPlayerPause'); + + return ( + <> + { + if (actionOptions.readonly === true) { + return; + } + + actionOptions.onActionUpdate({ + ...action, + name: newTitle, + }); + }} + /> + + +