Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/twenty-front/src/modules/workflow/types/Workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
type workflowTriggerSchema,
type workflowUpdateRecordActionSchema,
type workflowWebhookTriggerSchema,
type workflowDelayActionSchema,
} from 'twenty-shared/workflow';
import { type z } from 'zod';

Expand All @@ -42,6 +43,7 @@ export type WorkflowDeleteRecordAction = z.infer<
export type WorkflowFindRecordsAction = z.infer<
typeof workflowFindRecordsActionSchema
>;
export type WorkflowDelayAction = z.infer<typeof workflowDelayActionSchema>;
export type WorkflowFilterAction = z.infer<typeof workflowFilterActionSchema>;
export type WorkflowFormAction = z.infer<typeof workflowFormActionSchema>;
export type WorkflowHttpRequestAction = z.infer<
Expand All @@ -65,6 +67,7 @@ export type WorkflowAction =
| WorkflowHttpRequestAction
| WorkflowAiAgentAction
| WorkflowIteratorAction
| WorkflowDelayAction
| WorkflowEmptyAction;

export type WorkflowActionType = WorkflowAction['type'];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ export const WorkflowDiagramStepNodeIcon = ({
case 'AI_AGENT': {
return <Icon size={theme.icon.size.md} color={theme.color.pink} />;
}
case 'DELAY': {
return <Icon size={theme.icon.size.md} color={theme.color.green60} />;
}
default: {
return (
<Icon
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,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 { WorkflowEditActionFormFiller } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormFiller';
Expand Down Expand Up @@ -257,6 +258,17 @@ export const WorkflowRunStepNodeDetail = ({
/>
);
}
case 'DELAY': {
return (
<WorkflowEditActionDelay
key={stepId}
action={stepDefinition.definition}
actionOptions={{
readonly: true,
}}
/>
);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -232,6 +233,15 @@ export const WorkflowStepDetail = ({
/>
);
}
case 'DELAY': {
return (
<WorkflowEditActionDelay
key={stepId}
action={stepDefinition.definition}
actionOptions={props}
/>
);
}
default:
return assertUnreachable(
stepDefinition.definition,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { type WorkflowActionType } from '@/workflow/types/Workflow';

export const FLOW_ACTIONS: Array<{
label: string;
type: Extract<WorkflowActionType, 'ITERATOR' | 'FILTER'>;
type: Extract<WorkflowActionType, 'ITERATOR' | 'FILTER' | 'DELAY'>;
icon: string;
}> = [
{
Expand All @@ -15,4 +15,9 @@ export const FLOW_ACTIONS: Array<{
type: 'FILTER',
icon: 'IconFilter',
},
{
label: 'Delay',
type: 'DELAY',
icon: 'IconPlayerPause',
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
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 { useEffect, useState } from 'react';
import {
HorizontalSeparator,
IconCalendar,
IconClockHour8,
} from 'twenty-ui/display';
import { type SelectOption } from 'twenty-ui/input';
import { useDebouncedCallback } from 'use-debounce';
type WorkflowEditActionDelayProps = {
action: WorkflowDelayAction;
actionOptions:
| {
readonly: true;
}
| {
readonly?: false;
onActionUpdate: (action: WorkflowDelayAction) => void;
};
};

type DelayFormData = {
delayType: 'schedule_date' | 'duration';
scheduledDateTime: string | null;
duration: {
days: number;
hours: number;
minutes: number;
seconds: number;
};
};

export const WorkflowEditActionDelay = ({
action,
actionOptions,
}: WorkflowEditActionDelayProps) => {
const { headerTitle, headerIcon, headerIconColor, headerType, getIcon } =
useWorkflowActionHeader({
action,
defaultTitle: 'Delay',
});

const [formData, setFormData] = useState<DelayFormData>(() => {
const input = action.settings.input;
return {
delayType: input.delayType ?? 'duration',
scheduledDateTime: input.scheduledDateTime ?? null,
duration: input.duration ?? {
days: 0,
hours: 0,
minutes: 0,
seconds: 0,
},
};
});

const delayOptions: Array<SelectOption<string>> = [
{
label: t`At a specific date or time`,
value: 'schedule_date',
Icon: IconCalendar,
},
{
label: t`After a set amount of time`,
value: 'duration',
Icon: IconClockHour8,
},
];

const saveAction = useDebouncedCallback((formData: DelayFormData) => {
if (actionOptions.readonly === true) {
return;
}
actionOptions.onActionUpdate({
...action,
settings: {
...action.settings,
input: {
delayType: formData.delayType,
scheduledDateTime:
formData.delayType === 'schedule_date'
? formData.scheduledDateTime
: null,
duration:
formData.delayType === 'duration' ? formData.duration : undefined,
},
},
});
}, 1_000);

useEffect(() => {
return () => {
saveAction.flush();
};
}, [saveAction]);

const handleDelayTypeChange = (value: string) => {
const delayType = value === 'schedule_date' ? 'schedule_date' : 'duration';
const newFormData: DelayFormData = {
...formData,
delayType,
};

setFormData(newFormData);
saveAction(newFormData);
};

const handleDateTimeChange = (value: string | null) => {
const newFormData: DelayFormData = {
...formData,
scheduledDateTime: value,
};

setFormData(newFormData);
saveAction(newFormData);
};

const handleDurationChange = (
field: keyof DelayFormData['duration'],
value: number | null | string,
) => {
const numberValue = value === null || value === '' ? 0 : Number(value);
const newFormData: DelayFormData = {
...formData,
duration: {
...formData.duration,
[field]: numberValue,
},
};

setFormData(newFormData);
saveAction(newFormData);
};

const HeaderIcon = getIcon(headerIcon ?? 'IconPlayerPause');

return (
<>
<SidePanelHeader
initialTitle={headerTitle}
Icon={HeaderIcon}
iconColor={headerIconColor}
headerType={headerType}
onTitleChange={(newTitle: string) => {
if (actionOptions.readonly === true) {
return;
}

actionOptions.onActionUpdate({
...action,
name: newTitle,
});
}}
/>

<WorkflowStepBody>
<Select
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's drop this? Also it's not at a specific date it's the opposite!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I told you to do duration only and I think you did fixed data only 😅. Since we have the wrong one let's go with both then!

dropdownId="workflow-edit-action-delay-type"
label={t`Resume`}
options={delayOptions}
dropdownWidth={GenericDropdownContentWidth.Large}
value={formData.delayType}
onChange={handleDelayTypeChange}
disabled={actionOptions.readonly}
/>
<HorizontalSeparator noMargin />

{formData.delayType === 'schedule_date' ? (
<FormDateTimeFieldInput
label={t`Delay Until Date`}
defaultValue={formData.scheduledDateTime ?? ''}
onChange={handleDateTimeChange}
readonly={actionOptions.readonly}
VariablePicker={WorkflowVariablePicker}
placeholder="Select a date"
/>
) : (
<>
<FormNumberFieldInput
label={t`Days`}
defaultValue={formData.duration.days}
onChange={(value) => handleDurationChange('days', value)}
readonly={actionOptions.readonly}
VariablePicker={WorkflowVariablePicker}
placeholder="0"
/>
<FormNumberFieldInput
label={t`Hours`}
defaultValue={formData.duration.hours}
onChange={(value) => handleDurationChange('hours', value)}
readonly={actionOptions.readonly}
VariablePicker={WorkflowVariablePicker}
placeholder="0"
/>
<FormNumberFieldInput
label={t`Minutes`}
defaultValue={formData.duration.minutes}
onChange={(value) => handleDurationChange('minutes', value)}
readonly={actionOptions.readonly}
VariablePicker={WorkflowVariablePicker}
placeholder="0"
/>
<FormNumberFieldInput
label={t`Seconds`}
defaultValue={formData.duration.seconds}
onChange={(value) => handleDurationChange('seconds', value)}
readonly={actionOptions.readonly}
VariablePicker={WorkflowVariablePicker}
placeholder="0"
/>
</>
)}
</WorkflowStepBody>

<WorkflowActionFooter stepId={action.id} />
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,13 @@ describe('getActionIconColorOrThrow', () => {
});

describe('FILTER action type', () => {
it('should throw an error for FILTER action type', () => {
it('should return green color for FILTER action type', () => {
const result = getActionIconColorOrThrow({
theme: mockTheme,
actionType: 'FILTER',
});

expect(result).toBe(mockTheme.font.color.tertiary);
expect(result).toBe(mockTheme.color.green60);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ export const getActionHeaderTypeOrThrow = (actionType: WorkflowActionType) => {
case 'EMPTY': {
return msg`Empty Node`;
}
case 'DELAY': {
return msg`Delay`;
}
default:
assertUnreachable(actionType, `Unsupported action type: ${actionType}`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export const getActionIcon = (actionType: WorkflowActionType) => {
return HUMAN_INPUT_ACTIONS.find((item) => item.type === actionType)?.icon;
case 'ITERATOR':
return 'IconRepeat';
case 'DELAY':
return 'IconPlayerPause';
default:
return 'IconDefault';
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ export const getActionIconColorOrThrow = ({
case 'FORM':
return theme.color.orange;
case 'ITERATOR':
case 'FILTER':
case 'EMPTY':
return theme.font.color.tertiary;
case 'FILTER':
case 'DELAY':
return theme.color.green60;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image This looks odd? Please refer to Figma or check with @Bonapara

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldnt find a color for filter on Figma. Maybe I might have missed it looking there. I'll push to make sure it uses the same green color.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's use the green tag color like other nodes from the Flow section

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the filter also become green @Bonapara? (also in the flow section)

case 'AI_AGENT':
return theme.color.pink;
default:
Expand Down
Loading
Loading