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({
|
export const workflowCronTriggerSchema = baseTriggerSchema.extend({
|
||||||
type: z.literal('CRON'),
|
type: z.literal('CRON'),
|
||||||
settings: z.discriminatedUnion('type', [
|
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({
|
z.object({
|
||||||
type: z.literal('HOURS'),
|
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(),
|
outputSchema: z.object({}).passthrough(),
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
type: z.literal('MINUTES'),
|
type: z.literal('MINUTES'),
|
||||||
schedule: z.object({ minute: z.number() }),
|
schedule: z.object({ minute: z.number().min(1) }),
|
||||||
outputSchema: z.object({}).passthrough(),
|
outputSchema: z.object({}).passthrough(),
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
|
|||||||
@ -30,6 +30,9 @@ type WorkflowEditTriggerCronFormProps = {
|
|||||||
|
|
||||||
type FormErrorMessages = {
|
type FormErrorMessages = {
|
||||||
CUSTOM?: string | undefined;
|
CUSTOM?: string | undefined;
|
||||||
|
DAYS_day?: string | undefined;
|
||||||
|
DAYS_hour?: string | undefined;
|
||||||
|
DAYS_minute?: string | undefined;
|
||||||
HOURS_hour?: string | undefined;
|
HOURS_hour?: string | undefined;
|
||||||
HOURS_minute?: string | undefined;
|
HOURS_minute?: string | undefined;
|
||||||
MINUTES?: 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' && (
|
{trigger.settings.type === 'HOURS' && (
|
||||||
<>
|
<>
|
||||||
<FormNumberFieldInput
|
<FormNumberFieldInput
|
||||||
|
|||||||
@ -3,15 +3,21 @@ import {
|
|||||||
IconComponent,
|
IconComponent,
|
||||||
IconHours24,
|
IconHours24,
|
||||||
IconTimeDuration60,
|
IconTimeDuration60,
|
||||||
|
IconBrandDaysCounter,
|
||||||
} from 'twenty-ui';
|
} from 'twenty-ui';
|
||||||
|
|
||||||
export type CronTriggerInterval = 'HOURS' | 'MINUTES' | 'CUSTOM';
|
export type CronTriggerInterval = 'DAYS' | 'HOURS' | 'MINUTES' | 'CUSTOM';
|
||||||
|
|
||||||
export const CRON_TRIGGER_INTERVAL_OPTIONS: Array<{
|
export const CRON_TRIGGER_INTERVAL_OPTIONS: Array<{
|
||||||
label: string;
|
label: string;
|
||||||
value: CronTriggerInterval;
|
value: CronTriggerInterval;
|
||||||
Icon: IconComponent;
|
Icon: IconComponent;
|
||||||
}> = [
|
}> = [
|
||||||
|
{
|
||||||
|
label: 'Days',
|
||||||
|
value: 'DAYS',
|
||||||
|
Icon: IconBrandDaysCounter,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Hours',
|
label: 'Hours',
|
||||||
value: 'HOURS',
|
value: 'HOURS',
|
||||||
|
|||||||
@ -8,6 +8,16 @@ export const getCronTriggerDefaultSettings = (
|
|||||||
cronTriggerInterval: CronTriggerInterval,
|
cronTriggerInterval: CronTriggerInterval,
|
||||||
): WorkflowCronTrigger['settings'] => {
|
): WorkflowCronTrigger['settings'] => {
|
||||||
switch (cronTriggerInterval) {
|
switch (cronTriggerInterval) {
|
||||||
|
case 'DAYS':
|
||||||
|
return {
|
||||||
|
schedule: {
|
||||||
|
day: 1,
|
||||||
|
hour: 0,
|
||||||
|
minute: 0,
|
||||||
|
},
|
||||||
|
type: cronTriggerInterval,
|
||||||
|
outputSchema: {},
|
||||||
|
};
|
||||||
case 'HOURS':
|
case 'HOURS':
|
||||||
return {
|
return {
|
||||||
schedule: {
|
schedule: {
|
||||||
|
|||||||
@ -52,8 +52,8 @@ export const getTriggerDefaultDefinition = ({
|
|||||||
type,
|
type,
|
||||||
name: defaultLabel,
|
name: defaultLabel,
|
||||||
settings: {
|
settings: {
|
||||||
type: 'HOURS',
|
type: 'DAYS',
|
||||||
schedule: { hour: 1, minute: 0 },
|
schedule: { day: 1, hour: 0, minute: 0 },
|
||||||
outputSchema: {},
|
outputSchema: {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -39,6 +39,10 @@ export type WorkflowManualTrigger = BaseTrigger & {
|
|||||||
export type WorkflowCronTrigger = BaseTrigger & {
|
export type WorkflowCronTrigger = BaseTrigger & {
|
||||||
type: WorkflowTriggerType.CRON;
|
type: WorkflowTriggerType.CRON;
|
||||||
settings: (
|
settings: (
|
||||||
|
| {
|
||||||
|
type: 'DAYS';
|
||||||
|
schedule: { day: number; hour: number; minute: number };
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
type: 'HOURS';
|
type: 'HOURS';
|
||||||
schedule: { hour: number; minute: number };
|
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', () => {
|
it('should return the correct cron pattern for HOURS type', () => {
|
||||||
const trigger: WorkflowCronTrigger = {
|
const trigger: WorkflowCronTrigger = {
|
||||||
name: '',
|
name: '',
|
||||||
|
|||||||
@ -100,6 +100,35 @@ function assertCronTriggerSettingsAreValid(settings: any) {
|
|||||||
return;
|
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': {
|
case 'HOURS': {
|
||||||
if (!settings.schedule) {
|
if (!settings.schedule) {
|
||||||
throw new WorkflowTriggerException(
|
throw new WorkflowTriggerException(
|
||||||
|
|||||||
@ -26,6 +26,13 @@ export const computeCronPatternFromSchedule = (
|
|||||||
|
|
||||||
return trigger.settings.pattern;
|
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': {
|
case 'HOURS': {
|
||||||
const pattern = `${trigger.settings.schedule.minute} */${trigger.settings.schedule.hour} * * *`;
|
const pattern = `${trigger.settings.schedule.minute} */${trigger.settings.schedule.hour} * * *`;
|
||||||
|
|
||||||
|
|||||||
@ -27,6 +27,7 @@ export {
|
|||||||
IconBrackets,
|
IconBrackets,
|
||||||
IconBracketsAngle,
|
IconBracketsAngle,
|
||||||
IconBracketsContain,
|
IconBracketsContain,
|
||||||
|
IconBrandDaysCounter,
|
||||||
IconBrandGithub,
|
IconBrandGithub,
|
||||||
IconBrandGoogle,
|
IconBrandGoogle,
|
||||||
IconBrandGraphql,
|
IconBrandGraphql,
|
||||||
|
|||||||
Reference in New Issue
Block a user