-
Notifications
You must be signed in to change notification settings - Fork 4.3k
feat: workflow delay action (Pause - Wait/Sleep/Delay) #14915
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
84f8d05
77972eb
dd581bb
af18c0c
153c5b8
b5589d0
956198b
e6a878f
9ddc06e
479be48
2e1302f
cc1ed7c
2ae8a5e
51ac6a8
cb3b0b4
fa7174f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
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 |
---|---|---|
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||
|
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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!