Add days schedule trigger (#10800)
<img width="1470" alt="image" src="https://github.com/user-attachments/assets/660d79ba-60c7-4874-aa82-80a7575366ba" />
This commit is contained in:
@ -150,14 +150,26 @@ export const workflowManualTriggerSchema = baseTriggerSchema.extend({
|
||||
export const workflowCronTriggerSchema = baseTriggerSchema.extend({
|
||||
type: z.literal('CRON'),
|
||||
settings: z.discriminatedUnion('type', [
|
||||
z.object({
|
||||
type: z.literal('DAYS'),
|
||||
schedule: z.object({
|
||||
day: z.number().min(1),
|
||||
hour: z.number().min(0).max(23),
|
||||
minute: z.number().min(0).max(59),
|
||||
}),
|
||||
outputSchema: z.object({}).passthrough(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('HOURS'),
|
||||
schedule: z.object({ hour: z.number(), minute: z.number() }),
|
||||
schedule: z.object({
|
||||
hour: z.number().min(1),
|
||||
minute: z.number().min(0).max(59),
|
||||
}),
|
||||
outputSchema: z.object({}).passthrough(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('MINUTES'),
|
||||
schedule: z.object({ minute: z.number() }),
|
||||
schedule: z.object({ minute: z.number().min(1) }),
|
||||
outputSchema: z.object({}).passthrough(),
|
||||
}),
|
||||
z.object({
|
||||
|
||||
@ -30,6 +30,9 @@ type WorkflowEditTriggerCronFormProps = {
|
||||
|
||||
type FormErrorMessages = {
|
||||
CUSTOM?: string | undefined;
|
||||
DAYS_day?: string | undefined;
|
||||
DAYS_hour?: string | undefined;
|
||||
DAYS_minute?: string | undefined;
|
||||
HOURS_hour?: string | undefined;
|
||||
HOURS_minute?: string | undefined;
|
||||
MINUTES?: string | undefined;
|
||||
@ -146,6 +149,159 @@ export const WorkflowEditTriggerCronForm = ({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{trigger.settings.type === 'DAYS' && (
|
||||
<>
|
||||
<FormNumberFieldInput
|
||||
label="Days Between Triggers"
|
||||
error={errorMessagesVisible ? errorMessages.DAYS_day : undefined}
|
||||
onBlur={onBlur}
|
||||
defaultValue={trigger.settings.schedule.day}
|
||||
onPersist={(newDay) => {
|
||||
if (triggerOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDefined(newDay)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isNumber(newDay) || newDay <= 0) {
|
||||
setErrorMessages((prev) => ({
|
||||
...prev,
|
||||
DAYS_day: `Invalid day value '${newDay}'. Should be integer greater than 1`,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
setErrorMessages((prev) => ({
|
||||
...prev,
|
||||
DAYS_day: undefined,
|
||||
}));
|
||||
|
||||
triggerOptions.onTriggerUpdate({
|
||||
...trigger,
|
||||
settings: {
|
||||
...trigger.settings,
|
||||
type: 'DAYS',
|
||||
schedule: {
|
||||
day: newDay,
|
||||
hour:
|
||||
trigger.settings.type === 'DAYS'
|
||||
? trigger.settings.schedule.hour
|
||||
: 0,
|
||||
minute:
|
||||
trigger.settings.type === 'DAYS'
|
||||
? trigger.settings.schedule.minute
|
||||
: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
placeholder="Enter number greater than 1"
|
||||
readonly={triggerOptions.readonly}
|
||||
/>
|
||||
<FormNumberFieldInput
|
||||
label="Trigger at Hour"
|
||||
error={errorMessagesVisible ? errorMessages.DAYS_hour : undefined}
|
||||
onBlur={onBlur}
|
||||
defaultValue={trigger.settings.schedule.hour}
|
||||
onPersist={(newHour) => {
|
||||
if (triggerOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDefined(newHour)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isNumber(newHour) || newHour < 0 || newHour > 23) {
|
||||
setErrorMessages((prev) => ({
|
||||
...prev,
|
||||
DAYS_hour: `Invalid hour value '${newHour}'. Should be integer between 0 and 23`,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
setErrorMessages((prev) => ({
|
||||
...prev,
|
||||
DAYS_hour: undefined,
|
||||
}));
|
||||
|
||||
triggerOptions.onTriggerUpdate({
|
||||
...trigger,
|
||||
settings: {
|
||||
...trigger.settings,
|
||||
type: 'DAYS',
|
||||
schedule: {
|
||||
day:
|
||||
trigger.settings.type === 'DAYS'
|
||||
? trigger.settings.schedule.day
|
||||
: 1,
|
||||
hour: newHour,
|
||||
minute:
|
||||
trigger.settings.type === 'DAYS'
|
||||
? trigger.settings.schedule.minute
|
||||
: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
placeholder="Enter number between 0 and 23"
|
||||
readonly={triggerOptions.readonly}
|
||||
/>
|
||||
<FormNumberFieldInput
|
||||
label="Trigger at Minute"
|
||||
error={
|
||||
errorMessagesVisible ? errorMessages.DAYS_minute : undefined
|
||||
}
|
||||
onBlur={onBlur}
|
||||
defaultValue={trigger.settings.schedule.minute}
|
||||
onPersist={(newMinute) => {
|
||||
if (triggerOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDefined(newMinute)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isNumber(newMinute) || newMinute < 0 || newMinute > 59) {
|
||||
setErrorMessages((prev) => ({
|
||||
...prev,
|
||||
DAYS_minute: `Invalid minute value '${newMinute}'. Should be integer between 0 and 59`,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
setErrorMessages((prev) => ({
|
||||
...prev,
|
||||
DAYS_minute: undefined,
|
||||
}));
|
||||
|
||||
triggerOptions.onTriggerUpdate({
|
||||
...trigger,
|
||||
settings: {
|
||||
...trigger.settings,
|
||||
type: 'DAYS',
|
||||
schedule: {
|
||||
day:
|
||||
trigger.settings.type === 'DAYS'
|
||||
? trigger.settings.schedule.day
|
||||
: 1,
|
||||
hour:
|
||||
trigger.settings.type === 'DAYS'
|
||||
? trigger.settings.schedule.hour
|
||||
: 0,
|
||||
minute: newMinute,
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
placeholder="Enter number between 0 and 59"
|
||||
readonly={triggerOptions.readonly}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{trigger.settings.type === 'HOURS' && (
|
||||
<>
|
||||
<FormNumberFieldInput
|
||||
|
||||
@ -3,15 +3,21 @@ import {
|
||||
IconComponent,
|
||||
IconHours24,
|
||||
IconTimeDuration60,
|
||||
IconBrandDaysCounter,
|
||||
} from 'twenty-ui';
|
||||
|
||||
export type CronTriggerInterval = 'HOURS' | 'MINUTES' | 'CUSTOM';
|
||||
export type CronTriggerInterval = 'DAYS' | 'HOURS' | 'MINUTES' | 'CUSTOM';
|
||||
|
||||
export const CRON_TRIGGER_INTERVAL_OPTIONS: Array<{
|
||||
label: string;
|
||||
value: CronTriggerInterval;
|
||||
Icon: IconComponent;
|
||||
}> = [
|
||||
{
|
||||
label: 'Days',
|
||||
value: 'DAYS',
|
||||
Icon: IconBrandDaysCounter,
|
||||
},
|
||||
{
|
||||
label: 'Hours',
|
||||
value: 'HOURS',
|
||||
|
||||
@ -8,6 +8,16 @@ export const getCronTriggerDefaultSettings = (
|
||||
cronTriggerInterval: CronTriggerInterval,
|
||||
): WorkflowCronTrigger['settings'] => {
|
||||
switch (cronTriggerInterval) {
|
||||
case 'DAYS':
|
||||
return {
|
||||
schedule: {
|
||||
day: 1,
|
||||
hour: 0,
|
||||
minute: 0,
|
||||
},
|
||||
type: cronTriggerInterval,
|
||||
outputSchema: {},
|
||||
};
|
||||
case 'HOURS':
|
||||
return {
|
||||
schedule: {
|
||||
|
||||
@ -52,8 +52,8 @@ export const getTriggerDefaultDefinition = ({
|
||||
type,
|
||||
name: defaultLabel,
|
||||
settings: {
|
||||
type: 'HOURS',
|
||||
schedule: { hour: 1, minute: 0 },
|
||||
type: 'DAYS',
|
||||
schedule: { day: 1, hour: 0, minute: 0 },
|
||||
outputSchema: {},
|
||||
},
|
||||
};
|
||||
|
||||
@ -39,6 +39,10 @@ export type WorkflowManualTrigger = BaseTrigger & {
|
||||
export type WorkflowCronTrigger = BaseTrigger & {
|
||||
type: WorkflowTriggerType.CRON;
|
||||
settings: (
|
||||
| {
|
||||
type: 'DAYS';
|
||||
schedule: { day: number; hour: number; minute: number };
|
||||
}
|
||||
| {
|
||||
type: 'HOURS';
|
||||
schedule: { hour: number; minute: number };
|
||||
|
||||
@ -39,6 +39,20 @@ describe('computeCronPatternFromSchedule', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should return the correct cron pattern for DAYS type', () => {
|
||||
const trigger: WorkflowCronTrigger = {
|
||||
name: '',
|
||||
type: WorkflowTriggerType.CRON,
|
||||
settings: {
|
||||
type: 'DAYS',
|
||||
schedule: { day: 31, hour: 10, minute: 30 },
|
||||
outputSchema: {},
|
||||
},
|
||||
};
|
||||
|
||||
expect(computeCronPatternFromSchedule(trigger)).toBe('30 10 */31 * *');
|
||||
});
|
||||
|
||||
it('should return the correct cron pattern for HOURS type', () => {
|
||||
const trigger: WorkflowCronTrigger = {
|
||||
name: '',
|
||||
|
||||
@ -100,6 +100,35 @@ function assertCronTriggerSettingsAreValid(settings: any) {
|
||||
return;
|
||||
}
|
||||
|
||||
case 'DAYS': {
|
||||
if (!settings.schedule) {
|
||||
throw new WorkflowTriggerException(
|
||||
'No schedule provided in cron trigger',
|
||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
||||
);
|
||||
}
|
||||
if (settings.schedule.day <= 0) {
|
||||
throw new WorkflowTriggerException(
|
||||
'Invalid day value. Should be integer greater than 1',
|
||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
||||
);
|
||||
}
|
||||
if (settings.schedule.hour < 0 || settings.schedule.hour > 23) {
|
||||
throw new WorkflowTriggerException(
|
||||
'Invalid hour value. Should be integer between 0 and 23',
|
||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
||||
);
|
||||
}
|
||||
if (settings.schedule.minute < 0 || settings.schedule.minute > 59) {
|
||||
throw new WorkflowTriggerException(
|
||||
'Invalid minute value. Should be integer between 0 and 59',
|
||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
case 'HOURS': {
|
||||
if (!settings.schedule) {
|
||||
throw new WorkflowTriggerException(
|
||||
|
||||
@ -26,6 +26,13 @@ export const computeCronPatternFromSchedule = (
|
||||
|
||||
return trigger.settings.pattern;
|
||||
}
|
||||
case 'DAYS': {
|
||||
const pattern = `${trigger.settings.schedule.minute} ${trigger.settings.schedule.hour} */${trigger.settings.schedule.day} * *`;
|
||||
|
||||
validatePattern(pattern);
|
||||
|
||||
return pattern;
|
||||
}
|
||||
case 'HOURS': {
|
||||
const pattern = `${trigger.settings.schedule.minute} */${trigger.settings.schedule.hour} * * *`;
|
||||
|
||||
|
||||
@ -27,6 +27,7 @@ export {
|
||||
IconBrackets,
|
||||
IconBracketsAngle,
|
||||
IconBracketsContain,
|
||||
IconBrandDaysCounter,
|
||||
IconBrandGithub,
|
||||
IconBrandGoogle,
|
||||
IconBrandGraphql,
|
||||
|
||||
Reference in New Issue
Block a user