diff --git a/package.json b/package.json
index cebb2a269..63a28b895 100644
--- a/package.json
+++ b/package.json
@@ -77,6 +77,7 @@
"bytes": "^3.1.2",
"class-transformer": "^0.5.1",
"clsx": "^2.1.1",
+ "cron-validate": "^1.4.5",
"cross-env": "^7.0.3",
"css-loader": "^7.1.2",
"danger-plugin-todos": "^1.3.1",
diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormFieldInputContainer.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormFieldInputContainer.tsx
index 6de0d78b3..3002a6134 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormFieldInputContainer.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormFieldInputContainer.tsx
@@ -1,4 +1,7 @@
import styled from '@emotion/styled';
+import { ReactNode } from 'react';
+import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
+import { FormFieldInputHotKeyScope } from '@/object-record/record-field/form-types/constants/FormFieldInputHotKeyScope';
const StyledFormFieldInputContainer = styled.div`
display: flex;
@@ -6,4 +9,35 @@ const StyledFormFieldInputContainer = styled.div`
width: 100%;
`;
-export const FormFieldInputContainer = StyledFormFieldInputContainer;
+export const FormFieldInputContainer = ({
+ children,
+ testId,
+}: {
+ children: ReactNode;
+ testId?: string;
+}) => {
+ const {
+ goBackToPreviousHotkeyScope,
+ setHotkeyScopeAndMemorizePreviousScope,
+ } = usePreviousHotkeyScope();
+
+ const onFocus = () => {
+ setHotkeyScopeAndMemorizePreviousScope(
+ FormFieldInputHotKeyScope.FormFieldInput,
+ );
+ };
+
+ const onBlur = () => {
+ goBackToPreviousHotkeyScope();
+ };
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormMultiSelectFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormMultiSelectFieldInput.tsx
index c97d3eee9..a50f4d147 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormMultiSelectFieldInput.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormMultiSelectFieldInput.tsx
@@ -178,7 +178,7 @@ export const FormMultiSelectFieldInput = ({
const placeholderText = placeholder ?? label;
return (
-
+
{label ? {label} : null}
diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormNumberFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormNumberFieldInput.tsx
index 142cc82e7..f19b5ac22 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormNumberFieldInput.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormNumberFieldInput.tsx
@@ -1,4 +1,3 @@
-import { FormFieldHint } from '@/object-record/record-field/form-types/components/FormFieldHint';
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
import { FormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInputContainer';
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
@@ -14,6 +13,8 @@ import {
canBeCastAsNumberOrNull,
castAsNumberOrNull,
} from '~/utils/cast-as-number-or-null';
+import { InputErrorHelper } from '@/ui/input/components/InputErrorHelper';
+import { InputHint } from '@/ui/input/components/InputHint';
const StyledInput = styled(TextInput)`
padding: ${({ theme }) => `${theme.spacing(1)} ${theme.spacing(2)}`};
@@ -21,9 +22,11 @@ const StyledInput = styled(TextInput)`
type FormNumberFieldInputProps = {
label?: string;
+ error?: string;
placeholder: string;
defaultValue: number | string | undefined;
onPersist: (value: number | null | string) => void;
+ onBlur?: () => void;
VariablePicker?: VariablePickerComponent;
hint?: string;
readonly?: boolean;
@@ -31,9 +34,11 @@ type FormNumberFieldInputProps = {
export const FormNumberFieldInput = ({
label,
+ error,
placeholder,
defaultValue,
onPersist,
+ onBlur,
VariablePicker,
hint,
readonly,
@@ -105,6 +110,7 @@ export const FormNumberFieldInput = ({
{draftValue.type === 'static' ? (
- {hint ? {hint} : null}
+ {hint ? {hint} : null}
+ {error && {error}}
);
};
diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormTextFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormTextFieldInput.tsx
index 3f4e998f7..35d531086 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormTextFieldInput.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormTextFieldInput.tsx
@@ -8,12 +8,17 @@ import { InputLabel } from '@/ui/input/components/InputLabel';
import { parseEditorContent } from '@/workflow/workflow-variables/utils/parseEditorContent';
import { useId } from 'react';
import { isDefined } from 'twenty-shared';
+import { InputErrorHelper } from '@/ui/input/components/InputErrorHelper';
+import { InputHint } from '@/ui/input/components/InputHint';
type FormTextFieldInputProps = {
label?: string;
+ error?: string;
+ hint?: string;
defaultValue: string | undefined;
placeholder: string;
onPersist: (value: string) => void;
+ onBlur?: () => void;
multiline?: boolean;
readonly?: boolean;
VariablePicker?: VariablePickerComponent;
@@ -21,9 +26,12 @@ type FormTextFieldInputProps = {
export const FormTextFieldInput = ({
label,
+ error,
+ hint,
defaultValue,
placeholder,
onPersist,
+ onBlur,
multiline,
readonly,
VariablePicker,
@@ -65,6 +73,7 @@ export const FormTextFieldInput = ({
) : null}
+ {hint && {hint}}
+ {error && {error}}
);
};
diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/constants/FormFieldInputHotKeyScope.ts b/packages/twenty-front/src/modules/object-record/record-field/form-types/constants/FormFieldInputHotKeyScope.ts
new file mode 100644
index 000000000..38c1c948c
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/constants/FormFieldInputHotKeyScope.ts
@@ -0,0 +1,3 @@
+export enum FormFieldInputHotKeyScope {
+ FormFieldInput = 'form-field-input',
+}
diff --git a/packages/twenty-front/src/modules/ui/input/components/InputErrorHelper.tsx b/packages/twenty-front/src/modules/ui/input/components/InputErrorHelper.tsx
new file mode 100644
index 000000000..5c0b1deb5
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/input/components/InputErrorHelper.tsx
@@ -0,0 +1,15 @@
+import React from 'react';
+import styled from '@emotion/styled';
+
+const StyledInputErrorHelper = styled.div`
+ color: ${({ theme }) => theme.color.red};
+ font-size: ${({ theme }) => theme.font.size.xs};
+`;
+
+export const InputErrorHelper = ({
+ children,
+}: {
+ children: React.ReactNode;
+}) => (
+ {children}
+);
diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormFieldHint.tsx b/packages/twenty-front/src/modules/ui/input/components/InputHint.tsx
similarity index 59%
rename from packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormFieldHint.tsx
rename to packages/twenty-front/src/modules/ui/input/components/InputHint.tsx
index b172a457b..85633dc95 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormFieldHint.tsx
+++ b/packages/twenty-front/src/modules/ui/input/components/InputHint.tsx
@@ -1,10 +1,10 @@
import styled from '@emotion/styled';
-const StyledFormFieldHint = styled.div`
+const StyledInputHint = styled.div`
color: ${({ theme }) => theme.font.color.light};
font-size: ${({ theme }) => theme.font.size.xs};
font-weight: ${({ theme }) => theme.font.weight.regular};
- margin-top: ${({ theme }) => theme.spacing(1)};
+ margin-top: ${({ theme }) => theme.spacing(0.5)};
`;
-export const FormFieldHint = StyledFormFieldHint;
+export { StyledInputHint as InputHint };
diff --git a/packages/twenty-front/src/modules/ui/input/components/TextInputV2.tsx b/packages/twenty-front/src/modules/ui/input/components/TextInputV2.tsx
index 2f0150589..b4bb09178 100644
--- a/packages/twenty-front/src/modules/ui/input/components/TextInputV2.tsx
+++ b/packages/twenty-front/src/modules/ui/input/components/TextInputV2.tsx
@@ -19,6 +19,7 @@ import {
} from 'twenty-ui';
import { useCombinedRefs } from '~/hooks/useCombinedRefs';
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
+import { InputErrorHelper } from '@/ui/input/components/InputErrorHelper';
const StyledContainer = styled.div<
Pick
@@ -83,9 +84,7 @@ const StyledInput = styled.input<
}
`;
-const StyledErrorHelper = styled.div`
- color: ${({ theme }) => theme.color.red};
- font-size: ${({ theme }) => theme.font.size.xs};
+const StyledErrorHelper = styled(InputErrorHelper)`
padding: ${({ theme }) => theme.spacing(1)};
`;
diff --git a/packages/twenty-front/src/modules/workflow/hooks/useAllActiveWorkflowVersions.ts b/packages/twenty-front/src/modules/workflow/hooks/useAllActiveWorkflowVersions.ts
index 37425e232..b3d6586c5 100644
--- a/packages/twenty-front/src/modules/workflow/hooks/useAllActiveWorkflowVersions.ts
+++ b/packages/twenty-front/src/modules/workflow/hooks/useAllActiveWorkflowVersions.ts
@@ -62,7 +62,9 @@ export const useAllActiveWorkflowVersions = ({
if (!isDefined(objectMetadataItem)) {
return {
records: records.filter(
- (record) => !isDefined(record.trigger?.settings.objectType),
+ (record) =>
+ record.trigger?.type !== 'CRON' &&
+ !isDefined(record.trigger?.settings.objectType),
),
};
}
diff --git a/packages/twenty-front/src/modules/workflow/types/Workflow.ts b/packages/twenty-front/src/modules/workflow/types/Workflow.ts
index e7c2dfa15..691032a70 100644
--- a/packages/twenty-front/src/modules/workflow/types/Workflow.ts
+++ b/packages/twenty-front/src/modules/workflow/types/Workflow.ts
@@ -122,6 +122,24 @@ export type WorkflowManualTrigger = BaseTrigger & {
};
};
+export type WorkflowCronTrigger = BaseTrigger & {
+ type: 'CRON';
+ settings: (
+ | {
+ type: 'HOURS';
+ schedule: { hour: number; minute: number };
+ }
+ | {
+ type: 'MINUTES';
+ schedule: { minute: number };
+ }
+ | {
+ type: 'CUSTOM';
+ pattern: string;
+ }
+ ) & { outputSchema: object };
+};
+
export type WorkflowManualTriggerSettings = WorkflowManualTrigger['settings'];
export type WorkflowManualTriggerAvailability =
@@ -130,7 +148,8 @@ export type WorkflowManualTriggerAvailability =
export type WorkflowTrigger =
| WorkflowDatabaseEventTrigger
- | WorkflowManualTrigger;
+ | WorkflowManualTrigger
+ | WorkflowCronTrigger;
export type WorkflowTriggerType = WorkflowTrigger['type'];
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeIcon.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeIcon.tsx
index e355bd647..bbdd9313b 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeIcon.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeIcon.tsx
@@ -1,3 +1,4 @@
+import React from 'react';
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
import { WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey';
@@ -46,6 +47,16 @@ export const WorkflowDiagramStepNodeIcon = ({
);
}
+ case 'CRON': {
+ return (
+
+
+
+ );
+ }
}
return assertUnreachable(data.triggerType);
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateWorkflowDiagram.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateWorkflowDiagram.ts
index 906d8b0b9..450249786 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateWorkflowDiagram.ts
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateWorkflowDiagram.ts
@@ -73,6 +73,14 @@ export const generateWorkflowDiagram = ({
break;
}
+ case 'CRON': {
+ triggerDefaultLabel = 'On a Schedule';
+ triggerIcon = getTriggerIcon({
+ type: 'CRON',
+ });
+
+ break;
+ }
case 'DATABASE_EVENT': {
const triggerEvent = splitWorkflowTriggerEventName(
trigger.settings.eventName,
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 eee236a82..985b05638 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
@@ -11,6 +11,7 @@ import { WorkflowEditActionFormSendEmail } from '@/workflow/workflow-steps/workf
import { WorkflowEditActionFormUpdateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormUpdateRecord';
import { WorkflowEditTriggerDatabaseEventForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerDatabaseEventForm';
import { WorkflowEditTriggerManualForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerManualForm';
+import { WorkflowEditTriggerCronForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerCronForm';
import { Suspense, lazy } from 'react';
import { isDefined } from 'twenty-shared';
import { RightDrawerSkeletonLoader } from '~/loading/components/RightDrawerSkeletonLoader';
@@ -71,6 +72,14 @@ export const WorkflowStepDetail = ({
/>
);
}
+ case 'CRON': {
+ return (
+
+ );
+ }
}
return assertUnreachable(
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowSingleRecordPicker.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowSingleRecordPicker.tsx
index fb37b76e9..9985c57d0 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowSingleRecordPicker.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowSingleRecordPicker.tsx
@@ -125,7 +125,7 @@ export const WorkflowSingleRecordPicker = ({
};
return (
-
+
{label ? {label} : null}
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionFormCreateRecord.stories.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionFormCreateRecord.stories.tsx
index ba512c0b9..15667a794 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionFormCreateRecord.stories.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionFormCreateRecord.stories.tsx
@@ -9,6 +9,7 @@ import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorato
import { graphqlMocks } from '~/testing/graphqlMocks';
import { getWorkflowNodeIdMock } from '~/testing/mock-data/workflow';
import { WorkflowEditActionFormCreateRecord } from '../WorkflowEditActionFormCreateRecord';
+import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator';
const meta: Meta = {
title: 'Modules/Workflow/WorkflowEditActionFormCreateRecord',
@@ -45,6 +46,7 @@ const meta: Meta = {
ComponentDecorator,
ObjectMetadataItemsDecorator,
SnackBarDecorator,
+ WorkspaceDecorator,
],
};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionFormDeleteRecord.stories.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionFormDeleteRecord.stories.tsx
index 74f435a6d..436bb07df 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionFormDeleteRecord.stories.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionFormDeleteRecord.stories.tsx
@@ -10,6 +10,7 @@ import { graphqlMocks } from '~/testing/graphqlMocks';
import { getPeopleMock } from '~/testing/mock-data/people';
import { getWorkflowNodeIdMock } from '~/testing/mock-data/workflow';
import { WorkflowEditActionFormDeleteRecord } from '../WorkflowEditActionFormDeleteRecord';
+import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator';
const DEFAULT_ACTION = {
id: getWorkflowNodeIdMock(),
@@ -49,6 +50,7 @@ const meta: Meta = {
ObjectMetadataItemsDecorator,
SnackBarDecorator,
RouterDecorator,
+ WorkspaceDecorator,
],
};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionFormUpdateRecord.stories.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionFormUpdateRecord.stories.tsx
index 7a0c9af2a..c422fd47e 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionFormUpdateRecord.stories.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionFormUpdateRecord.stories.tsx
@@ -10,6 +10,7 @@ import { graphqlMocks } from '~/testing/graphqlMocks';
import { getPeopleMock } from '~/testing/mock-data/people';
import { getWorkflowNodeIdMock } from '~/testing/mock-data/workflow';
import { WorkflowEditActionFormUpdateRecord } from '../WorkflowEditActionFormUpdateRecord';
+import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator';
const DEFAULT_ACTION = {
id: getWorkflowNodeIdMock(),
@@ -62,6 +63,7 @@ const meta: Meta = {
ObjectMetadataItemsDecorator,
SnackBarDecorator,
RouterDecorator,
+ WorkspaceDecorator,
],
};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerCronForm.tsx b/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerCronForm.tsx
new file mode 100644
index 000000000..049a49ac9
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerCronForm.tsx
@@ -0,0 +1,291 @@
+import { WorkflowCronTrigger } from '@/workflow/types/Workflow';
+import { useIcons } from 'twenty-ui';
+import { useTheme } from '@emotion/react';
+import { Select } from '@/ui/input/components/Select';
+import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
+import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
+import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
+import { isDefined } from 'twenty-shared';
+import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerLabel';
+import { CRON_TRIGGER_INTERVAL_OPTIONS } from '@/workflow/workflow-trigger/constants/CronTriggerIntervalOptions';
+import cron from 'cron-validate';
+import { useState } from 'react';
+import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
+import { FormNumberFieldInput } from '@/object-record/record-field/form-types/components/FormNumberFieldInput';
+import { isNumber } from '@sniptt/guards';
+import { getCronTriggerDefaultSettings } from '@/workflow/workflow-trigger/utils/getCronTriggerDefaultSettings';
+
+type WorkflowEditTriggerCronFormProps = {
+ trigger: WorkflowCronTrigger;
+ triggerOptions:
+ | {
+ readonly: true;
+ onTriggerUpdate?: undefined;
+ }
+ | {
+ readonly?: false;
+ onTriggerUpdate: (trigger: WorkflowCronTrigger) => void;
+ };
+};
+
+type FormErrorMessages = {
+ CUSTOM?: string | undefined;
+ HOURS_hour?: string | undefined;
+ HOURS_minute?: string | undefined;
+ MINUTES?: string | undefined;
+};
+
+export const WorkflowEditTriggerCronForm = ({
+ trigger,
+ triggerOptions,
+}: WorkflowEditTriggerCronFormProps) => {
+ const theme = useTheme();
+ const [errorMessages, setErrorMessages] = useState({});
+ const [errorMessagesVisible, setErrorMessagesVisible] = useState(false);
+
+ const { getIcon } = useIcons();
+
+ const headerIcon = getTriggerIcon({
+ type: 'CRON',
+ });
+
+ const defaultLabel =
+ getTriggerDefaultLabel({
+ type: 'CRON',
+ }) ?? '';
+
+ const headerTitle = isDefined(trigger.name) ? trigger.name : defaultLabel;
+
+ const headerType = 'Trigger';
+
+ const onBlur = () => {
+ setErrorMessagesVisible(true);
+ };
+
+ return (
+ <>
+ {
+ if (triggerOptions.readonly === true) {
+ return;
+ }
+
+ triggerOptions.onTriggerUpdate({
+ ...trigger,
+ name: newName,
+ });
+ }}
+ Icon={getIcon(headerIcon)}
+ iconColor={theme.font.color.tertiary}
+ initialTitle={headerTitle}
+ headerType={headerType}
+ />
+
+
+ >
+ );
+};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-trigger/constants/CronTriggerIntervalOptions.ts b/packages/twenty-front/src/modules/workflow/workflow-trigger/constants/CronTriggerIntervalOptions.ts
new file mode 100644
index 000000000..1d69ca30c
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/workflow-trigger/constants/CronTriggerIntervalOptions.ts
@@ -0,0 +1,30 @@
+import {
+ IconComponent,
+ Icon24Hours,
+ IconTimeDuration60,
+ IconClockPlay,
+} from 'twenty-ui';
+
+export type CronTriggerInterval = 'HOURS' | 'MINUTES' | 'CUSTOM';
+
+export const CRON_TRIGGER_INTERVAL_OPTIONS: Array<{
+ label: string;
+ value: CronTriggerInterval;
+ Icon: IconComponent;
+}> = [
+ {
+ label: 'Hours',
+ value: 'HOURS',
+ Icon: Icon24Hours,
+ },
+ {
+ label: 'Minutes',
+ value: 'MINUTES',
+ Icon: IconTimeDuration60,
+ },
+ {
+ label: 'Cron (Custom)',
+ value: 'CUSTOM',
+ Icon: IconClockPlay,
+ },
+];
diff --git a/packages/twenty-front/src/modules/workflow/workflow-trigger/constants/OtherTriggerTypes.ts b/packages/twenty-front/src/modules/workflow/workflow-trigger/constants/OtherTriggerTypes.ts
index 925ce7690..87786e2e1 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-trigger/constants/OtherTriggerTypes.ts
+++ b/packages/twenty-front/src/modules/workflow/workflow-trigger/constants/OtherTriggerTypes.ts
@@ -10,4 +10,9 @@ export const OTHER_TRIGGER_TYPES: Array<{
type: 'MANUAL',
icon: 'IconHandMove',
},
+ {
+ defaultLabel: 'On a Schedule',
+ type: 'CRON',
+ icon: 'IconClock',
+ },
];
diff --git a/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/__tests__/getCronTriggerDefaultSettings.test.ts b/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/__tests__/getCronTriggerDefaultSettings.test.ts
new file mode 100644
index 000000000..a9eff47dd
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/__tests__/getCronTriggerDefaultSettings.test.ts
@@ -0,0 +1,38 @@
+import { getCronTriggerDefaultSettings } from '@/workflow/workflow-trigger/utils/getCronTriggerDefaultSettings';
+
+describe('getCronTriggerDefaultSettings', () => {
+ it('returns correct settings for HOURS interval', () => {
+ const result = getCronTriggerDefaultSettings('HOURS');
+ expect(result).toEqual({
+ schedule: { hour: 1, minute: 0 },
+ type: 'HOURS',
+ outputSchema: {},
+ });
+ });
+
+ it('returns correct settings for MINUTES interval', () => {
+ const result = getCronTriggerDefaultSettings('MINUTES');
+ expect(result).toEqual({
+ schedule: { minute: 1 },
+ type: 'MINUTES',
+ outputSchema: {},
+ });
+ });
+
+ it('returns correct settings for CUSTOM interval', () => {
+ const DEFAULT_CRON_PATTERN = '0 */1 * * *';
+ const result = getCronTriggerDefaultSettings('CUSTOM');
+ expect(result).toEqual({
+ pattern: DEFAULT_CRON_PATTERN,
+ type: 'CUSTOM',
+ outputSchema: {},
+ });
+ });
+
+ it('throws an error for an invalid interval', () => {
+ // @ts-expect-error Testing invalid input
+ expect(() => getCronTriggerDefaultSettings('INVALID')).toThrowError(
+ 'Invalid cron trigger interval',
+ );
+ });
+});
diff --git a/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/getCronTriggerDefaultSettings.ts b/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/getCronTriggerDefaultSettings.ts
new file mode 100644
index 000000000..cad9ecf59
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/getCronTriggerDefaultSettings.ts
@@ -0,0 +1,39 @@
+import { CronTriggerInterval } from '@/workflow/workflow-trigger/constants/CronTriggerIntervalOptions';
+import { WorkflowCronTrigger } from '@/workflow/types/Workflow';
+import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
+
+const DEFAULT_CRON_PATTERN = '0 */1 * * *'; // Every hour
+
+export const getCronTriggerDefaultSettings = (
+ cronTriggerInterval: CronTriggerInterval,
+): WorkflowCronTrigger['settings'] => {
+ switch (cronTriggerInterval) {
+ case 'HOURS':
+ return {
+ schedule: {
+ hour: 1,
+ minute: 0,
+ },
+ type: cronTriggerInterval,
+ outputSchema: {},
+ };
+ case 'MINUTES':
+ return {
+ schedule: {
+ minute: 1,
+ },
+ type: cronTriggerInterval,
+ outputSchema: {},
+ };
+ case 'CUSTOM':
+ return {
+ pattern: DEFAULT_CRON_PATTERN,
+ type: cronTriggerInterval,
+ outputSchema: {},
+ };
+ }
+ return assertUnreachable(
+ cronTriggerInterval,
+ 'Invalid cron trigger interval',
+ );
+};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/getTriggerDefaultDefinition.ts b/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/getTriggerDefaultDefinition.ts
index bd81a4658..1f267c5fe 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/getTriggerDefaultDefinition.ts
+++ b/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/getTriggerDefaultDefinition.ts
@@ -45,6 +45,16 @@ export const getTriggerDefaultDefinition = ({
}),
};
}
+ case 'CRON': {
+ return {
+ type,
+ settings: {
+ type: 'HOURS',
+ schedule: { hour: 1, minute: 0 },
+ outputSchema: {},
+ },
+ };
+ }
default: {
return assertUnreachable(type, `Unknown type: ${type}`);
}
diff --git a/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/getTriggerIcon.ts b/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/getTriggerIcon.ts
index 003b464ff..d508b600e 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/getTriggerIcon.ts
+++ b/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/getTriggerIcon.ts
@@ -6,6 +6,9 @@ export const getTriggerIcon = (
| {
type: 'MANUAL';
}
+ | {
+ type: 'CRON';
+ }
| {
type: 'DATABASE_EVENT';
eventName: string;
diff --git a/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/getTriggerLabel.ts b/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/getTriggerLabel.ts
index fb07f7bf3..51eae2fab 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/getTriggerLabel.ts
+++ b/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/getTriggerLabel.ts
@@ -6,6 +6,9 @@ export const getTriggerDefaultLabel = (
| {
type: 'MANUAL';
}
+ | {
+ type: 'CRON';
+ }
| {
type: 'DATABASE_EVENT';
eventName: string;
diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/getTriggerStepName.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/getTriggerStepName.ts
index 192c9c6be..e1769b370 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/getTriggerStepName.ts
+++ b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/getTriggerStepName.ts
@@ -10,6 +10,8 @@ export const getTriggerStepName = (trigger: WorkflowTrigger): string => {
switch (trigger.type) {
case 'DATABASE_EVENT':
return getDatabaseEventTriggerStepName(trigger);
+ case 'CRON':
+ return 'On a Schedule';
case 'MANUAL':
if (!isDefined(trigger.settings.objectType)) {
return 'Manual trigger';
diff --git a/packages/twenty-front/src/testing/decorators/WorkspaceDecorator.tsx b/packages/twenty-front/src/testing/decorators/WorkspaceDecorator.tsx
new file mode 100644
index 000000000..bd5ad52e6
--- /dev/null
+++ b/packages/twenty-front/src/testing/decorators/WorkspaceDecorator.tsx
@@ -0,0 +1,14 @@
+import { useSetRecoilState } from 'recoil';
+import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
+import { mockCurrentWorkspace } from '~/testing/mock-data/users';
+import { useEffect } from 'react';
+import { Decorator } from '@storybook/react';
+
+export const WorkspaceDecorator: Decorator = (Story) => {
+ const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
+
+ useEffect(() => {
+ setCurrentWorkspace(mockCurrentWorkspace);
+ }, [setCurrentWorkspace]);
+ return ;
+};
diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-builder.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-builder.workspace-service.ts
index 0afd026ea..8c2a89731 100644
--- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-builder.workspace-service.ts
+++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-builder.workspace-service.ts
@@ -59,6 +59,9 @@ export class WorkflowBuilderWorkspaceService {
objectMetadataRepository: this.objectMetadataRepository,
});
}
+ case WorkflowTriggerType.CRON: {
+ return {};
+ }
case WorkflowActionType.SEND_EMAIL: {
return this.computeSendEmailActionOutputSchema();
}
diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/types/workflow-trigger.type.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/types/workflow-trigger.type.ts
index c58277a7f..30130a63b 100644
--- a/packages/twenty-server/src/modules/workflow/workflow-trigger/types/workflow-trigger.type.ts
+++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/types/workflow-trigger.type.ts
@@ -38,9 +38,20 @@ export type WorkflowManualTrigger = BaseTrigger & {
export type WorkflowCronTrigger = BaseTrigger & {
type: WorkflowTriggerType.CRON;
- settings: {
- pattern: string;
- };
+ settings: (
+ | {
+ type: 'HOURS';
+ schedule: { hour: number; minute: number };
+ }
+ | {
+ type: 'MINUTES';
+ schedule: { minute: number };
+ }
+ | {
+ type: 'CUSTOM';
+ pattern: string;
+ }
+ ) & { outputSchema: object };
};
export type WorkflowManualTriggerSettings = WorkflowManualTrigger['settings'];
diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/utils/__tests__/compute-cron-pattern-from-schedule.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/utils/__tests__/compute-cron-pattern-from-schedule.spec.ts
new file mode 100644
index 000000000..ff2aebf23
--- /dev/null
+++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/utils/__tests__/compute-cron-pattern-from-schedule.spec.ts
@@ -0,0 +1,88 @@
+import {
+ WorkflowCronTrigger,
+ WorkflowTriggerType,
+} from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
+import { WorkflowTriggerException } from 'src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception';
+import { computeCronPatternFromSchedule } from 'src/modules/workflow/workflow-trigger/utils/compute-cron-pattern-from-schedule';
+
+describe('computeCronPatternFromSchedule', () => {
+ it('should return the pattern for CUSTOM type', () => {
+ const trigger: WorkflowCronTrigger = {
+ name: '',
+ type: WorkflowTriggerType.CRON,
+ settings: {
+ type: 'CUSTOM',
+ pattern: '12 * * * *',
+ outputSchema: {},
+ },
+ };
+
+ expect(computeCronPatternFromSchedule(trigger)).toBe('12 * * * *');
+ });
+
+ it('should throw an exception for unsupported pattern for CUSTOM type', () => {
+ const trigger: WorkflowCronTrigger = {
+ name: '',
+ type: WorkflowTriggerType.CRON,
+ settings: {
+ type: 'CUSTOM',
+ pattern: '0 12 * * * *',
+ outputSchema: {},
+ },
+ };
+
+ expect(() => computeCronPatternFromSchedule(trigger)).toThrow(
+ WorkflowTriggerException,
+ );
+ expect(() => computeCronPatternFromSchedule(trigger)).toThrow(
+ "Cron pattern '0 12 * * * *' is invalid",
+ );
+ });
+
+ it('should return the correct cron pattern for HOURS type', () => {
+ const trigger: WorkflowCronTrigger = {
+ name: '',
+ type: WorkflowTriggerType.CRON,
+ settings: {
+ type: 'HOURS',
+ schedule: { hour: 10, minute: 30 },
+ outputSchema: {},
+ },
+ };
+
+ expect(computeCronPatternFromSchedule(trigger)).toBe('30 */10 * * *');
+ });
+
+ it('should return the correct cron pattern for MINUTES type', () => {
+ const trigger: WorkflowCronTrigger = {
+ name: '',
+ type: WorkflowTriggerType.CRON,
+ settings: {
+ type: 'MINUTES',
+ schedule: { minute: 15 },
+ outputSchema: {},
+ },
+ };
+
+ expect(computeCronPatternFromSchedule(trigger)).toBe('*/15 * * * *');
+ });
+
+ it('should throw an exception for unsupported schedule type', () => {
+ const trigger: WorkflowCronTrigger = {
+ name: '',
+ type: WorkflowTriggerType.CRON,
+ settings: {
+ type: 'INVALID_TYPE' as any,
+ pattern: '',
+ outputSchema: {},
+ },
+ };
+
+ expect(() => computeCronPatternFromSchedule(trigger)).toThrow(
+ WorkflowTriggerException,
+ );
+ expect(() => computeCronPatternFromSchedule(trigger)).toThrow(
+ 'Unsupported cron schedule type',
+ );
+ });
+});
diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts
index ad9612de4..1e8686308 100644
--- a/packages/twenty-server/src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts
+++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts
@@ -70,6 +70,9 @@ function assertTriggerSettingsAreValid(
break;
case WorkflowTriggerType.MANUAL:
break;
+ case WorkflowTriggerType.CRON:
+ assertCronTriggerSettingsAreValid(settings);
+ break;
default:
throw new WorkflowTriggerException(
'Invalid trigger type for enabling workflow trigger',
@@ -78,6 +81,50 @@ function assertTriggerSettingsAreValid(
}
}
+function assertCronTriggerSettingsAreValid(settings: any) {
+ if (!settings?.type) {
+ throw new WorkflowTriggerException(
+ 'No setting type provided in cron trigger',
+ WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
+ );
+ }
+ if (settings.type === 'CUSTOM' && !settings.pattern) {
+ throw new WorkflowTriggerException(
+ 'No pattern provided in CUSTOM cron trigger',
+ WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
+ );
+ }
+ if (!settings.schedule) {
+ throw new WorkflowTriggerException(
+ 'No schedule provided in cron trigger',
+ WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
+ );
+ }
+ if (settings.type === 'HOURS' && settings.schedule.hour <= 0) {
+ throw new WorkflowTriggerException(
+ 'Invalid hour value. Should be integer greater than 1',
+ WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
+ );
+ }
+
+ if (
+ settings.type === 'HOURS' &&
+ (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,
+ );
+ }
+
+ if (settings.type === 'MINUTES' && settings.schedule.minute <= 0) {
+ throw new WorkflowTriggerException(
+ 'Invalid minute value. Should be integer greater than 1',
+ WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
+ );
+ }
+}
+
function assertDatabaseEventTriggerSettingsAreValid(settings: any) {
if (!settings?.eventName) {
throw new WorkflowTriggerException(
diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/utils/compute-cron-pattern-from-schedule.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/utils/compute-cron-pattern-from-schedule.ts
new file mode 100644
index 000000000..376950f88
--- /dev/null
+++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/utils/compute-cron-pattern-from-schedule.ts
@@ -0,0 +1,49 @@
+import cron from 'cron-validate';
+
+import { WorkflowCronTrigger } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
+import {
+ WorkflowTriggerException,
+ WorkflowTriggerExceptionCode,
+} from 'src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception';
+
+const validatePattern = (pattern: string) => {
+ const cronValidator = cron(pattern);
+
+ if (cronValidator.isError()) {
+ throw new WorkflowTriggerException(
+ `Cron pattern '${pattern}' is invalid`,
+ WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
+ );
+ }
+};
+
+export const computeCronPatternFromSchedule = (
+ trigger: WorkflowCronTrigger,
+) => {
+ switch (trigger.settings.type) {
+ case 'CUSTOM': {
+ validatePattern(trigger.settings.pattern);
+
+ return trigger.settings.pattern;
+ }
+ case 'HOURS': {
+ const pattern = `${trigger.settings.schedule.minute} */${trigger.settings.schedule.hour} * * *`;
+
+ validatePattern(pattern);
+
+ return pattern;
+ }
+ case 'MINUTES': {
+ const pattern = `*/${trigger.settings.schedule.minute} * * * *`;
+
+ validatePattern(pattern);
+
+ return pattern;
+ }
+ default:
+ throw new WorkflowTriggerException(
+ 'Unsupported cron schedule type',
+ WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
+ );
+ }
+};
diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts
index 46e397588..e5548d9a3 100644
--- a/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts
+++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts
@@ -36,6 +36,7 @@ import {
WorkflowTriggerJob,
WorkflowTriggerJobData,
} from 'src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job';
+import { computeCronPatternFromSchedule } from 'src/modules/workflow/workflow-trigger/utils/compute-cron-pattern-from-schedule';
@Injectable()
export class WorkflowTriggerWorkspaceService {
@@ -339,7 +340,9 @@ export class WorkflowTriggerWorkspaceService {
return;
case WorkflowTriggerType.MANUAL:
return;
- case WorkflowTriggerType.CRON:
+ case WorkflowTriggerType.CRON: {
+ const pattern = computeCronPatternFromSchedule(workflowVersion.trigger);
+
await this.messageQueueService.addCron({
jobName: WorkflowTriggerJob.name,
jobId: workflowVersion.workflowId,
@@ -350,12 +353,13 @@ export class WorkflowTriggerWorkspaceService {
},
options: {
repeat: {
- pattern: workflowVersion.trigger.settings.pattern,
+ pattern,
},
},
});
return;
+ }
default: {
assertNever(workflowVersion.trigger);
}
diff --git a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts
index 72a6f83f3..4dbe3f7d5 100644
--- a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts
+++ b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts
@@ -1,6 +1,7 @@
/* eslint-disable no-restricted-imports */
export {
Icon123,
+ Icon24Hours,
IconAlertCircle,
IconAlertTriangle,
IconApi,
@@ -48,6 +49,7 @@ export {
IconCircleX,
IconClick,
IconClockHour8,
+ IconClockPlay,
IconClockShare,
IconCode,
IconCodeCircle,
@@ -247,6 +249,8 @@ export {
IconTestPipe,
IconTextSize,
IconTextWrap,
+ IconTimeDuration30,
+ IconTimeDuration60,
IconTimelineEvent,
IconTrash,
IconTrashX,
diff --git a/yarn.lock b/yarn.lock
index 7a19370cf..cd3c65c61 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3328,6 +3328,15 @@ __metadata:
languageName: node
linkType: hard
+"@babel/runtime@npm:^7.10.5":
+ version: 7.26.7
+ resolution: "@babel/runtime@npm:7.26.7"
+ dependencies:
+ regenerator-runtime: "npm:^0.14.0"
+ checksum: 10c0/60199c049f90e5e41c687687430052a370aca60bac7859ff4ee761c5c1739b8ba1604d391d01588c22dc0e93828cbadb8ada742578ad1b1df240746bce98729a
+ languageName: node
+ linkType: hard
+
"@babel/template@npm:^7.18.10, @babel/template@npm:^7.20.7, @babel/template@npm:^7.22.15, @babel/template@npm:^7.22.5, @babel/template@npm:^7.24.7, @babel/template@npm:^7.25.0, @babel/template@npm:^7.3.3":
version: 7.25.0
resolution: "@babel/template@npm:7.25.0"
@@ -17200,6 +17209,13 @@ __metadata:
languageName: node
linkType: hard
+"@types/lodash@npm:^4.14.165":
+ version: 4.17.15
+ resolution: "@types/lodash@npm:4.17.15"
+ checksum: 10c0/2eb2dc6d231f5fb4603d176c08c8d7af688f574d09af47466a179cd7812d9f64144ba74bb32ca014570ffdc544eedc51b7a5657212bad083b6eecbd72223f9bb
+ languageName: node
+ linkType: hard
+
"@types/long@npm:^4.0.0":
version: 4.0.2
resolution: "@types/long@npm:4.0.2"
@@ -24003,6 +24019,15 @@ __metadata:
languageName: node
linkType: hard
+"cron-validate@npm:^1.4.5":
+ version: 1.4.5
+ resolution: "cron-validate@npm:1.4.5"
+ dependencies:
+ yup: "npm:0.32.9"
+ checksum: 10c0/4c210bea21832269fd8e18e4c9e9d750b314da04bc475b8b58414a6421a4c86804a6f1a8b9eda6d357af4c43f627fa3ee372f6fbb99b849d2a9b2aef2917bddf
+ languageName: node
+ linkType: hard
+
"cross-env@npm:^7.0.3":
version: 7.0.3
resolution: "cross-env@npm:7.0.3"
@@ -34147,7 +34172,7 @@ __metadata:
languageName: node
linkType: hard
-"lodash-es@npm:^4.17.21":
+"lodash-es@npm:^4.17.15, lodash-es@npm:^4.17.21":
version: 4.17.21
resolution: "lodash-es@npm:4.17.21"
checksum: 10c0/fb407355f7e6cd523a9383e76e6b455321f0f153a6c9625e21a8827d10c54c2a2341bd2ae8d034358b60e07325e1330c14c224ff582d04612a46a4f0479ff2f2
@@ -46163,6 +46188,7 @@ __metadata:
class-transformer: "npm:^0.5.1"
clsx: "npm:^2.1.1"
concurrently: "npm:^8.2.2"
+ cron-validate: "npm:^1.4.5"
cross-env: "npm:^7.0.3"
cross-var: "npm:^1.1.0"
css-loader: "npm:^7.1.2"
@@ -49286,6 +49312,21 @@ __metadata:
languageName: node
linkType: hard
+"yup@npm:0.32.9":
+ version: 0.32.9
+ resolution: "yup@npm:0.32.9"
+ dependencies:
+ "@babel/runtime": "npm:^7.10.5"
+ "@types/lodash": "npm:^4.14.165"
+ lodash: "npm:^4.17.20"
+ lodash-es: "npm:^4.17.15"
+ nanoclone: "npm:^0.2.1"
+ property-expr: "npm:^2.0.4"
+ toposort: "npm:^2.0.2"
+ checksum: 10c0/b2adff31f4be85aaad338e6db12a26715b9e11270c587afe051d42c423f7f24de2d184f646047cb5c3b8c65163c37611f8309f2ef4eb6bb7a66688158a081d66
+ languageName: node
+ linkType: hard
+
"yup@npm:^0.32.0":
version: 0.32.11
resolution: "yup@npm:0.32.11"