diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSelectFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSelectFieldInput.tsx index cac2cb6beb..738d8e558e 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSelectFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSelectFieldInput.tsx @@ -3,6 +3,7 @@ import { FormFieldInputInnerContainer } from '@/object-record/record-field/ui/fo import { FormFieldInputRowContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputRowContainer'; import { VariableChipStandalone } from '@/object-record/record-field/ui/form-types/components/VariableChipStandalone'; import { type VariablePickerComponent } from '@/object-record/record-field/ui/form-types/types/VariablePickerComponent'; +import { InputHint } from '@/ui/input/components/InputHint'; import { InputLabel } from '@/ui/input/components/InputLabel'; import { Select } from '@/ui/input/components/Select'; import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; @@ -18,6 +19,7 @@ import { type SelectOption } from 'twenty-ui/input'; type FormSelectFieldInputProps = { label?: string; + hint?: string; defaultValue: string | undefined; onChange: (value: string | null) => void; VariablePicker?: VariablePickerComponent; @@ -27,6 +29,7 @@ type FormSelectFieldInputProps = { export const FormSelectFieldInput = ({ label, + hint, defaultValue, onChange, VariablePicker, @@ -162,6 +165,7 @@ export const FormSelectFieldInput = ({ /> )} + {hint && {hint}} ); }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-trigger/components/CronExpressionHelper.tsx b/packages/twenty-front/src/modules/workflow/workflow-trigger/components/CronExpressionHelper.tsx index 5d6471076c..0816fbf1bf 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-trigger/components/CronExpressionHelper.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-trigger/components/CronExpressionHelper.tsx @@ -1,56 +1,15 @@ import { useDateTimeFormat } from '@/localization/hooks/useDateTimeFormat'; import { InputHint } from '@/ui/input/components/InputHint'; -import { InputLabel } from '@/ui/input/components/InputLabel'; import type { WorkflowCronTrigger } from '@/workflow/types/Workflow'; -import { describeCronExpression } from '@/workflow/workflow-trigger/utils/cron-to-human/describeCronExpression'; +import { convertScheduleToCronExpression } from '@/workflow/workflow-trigger/utils/cron-to-human/utils/convertScheduleToCronExpression'; +import { getTriggerScheduleDescription } from '@/workflow/workflow-trigger/utils/getTriggerScheduleDescription'; import styled from '@emotion/styled'; import { t } from '@lingui/core/macro'; import { CronExpressionParser } from 'cron-parser'; -import { type Locale } from 'date-fns'; import { useRecoilValue } from 'recoil'; import { dateLocaleState } from '~/localization/states/dateLocaleState'; import { formatDateTimeString } from '~/utils/string/formatDateTimeString'; -const convertScheduleToCronExpression = ( - trigger: WorkflowCronTrigger, -): string | null => { - switch (trigger.settings.type) { - case 'CUSTOM': - return trigger.settings.pattern; - case 'DAYS': - return `${trigger.settings.schedule.minute} ${trigger.settings.schedule.hour} */${trigger.settings.schedule.day} * *`; - case 'HOURS': - return `${trigger.settings.schedule.minute} */${trigger.settings.schedule.hour} * * *`; - case 'MINUTES': - return `*/${trigger.settings.schedule.minute} * * * *`; - default: - return null; - } -}; - -const getTriggerScheduleDescription = ( - trigger: WorkflowCronTrigger, - localeCatalog?: Locale, -): string | null => { - const cronExpression = convertScheduleToCronExpression(trigger); - - if (!cronExpression) { - return null; - } - - try { - return describeCronExpression( - cronExpression, - { use24HourTimeFormat: true }, - localeCatalog, - ); - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : t`Invalid cron expression`; - return errorMessage; - } -}; - const getNextExecutions = (cronExpression: string): Date[] => { try { const interval = CronExpressionParser.parse(cronExpression, { @@ -70,37 +29,48 @@ const StyledContainer = styled.div` `; const StyledSection = styled.div` + background-color: ${({ theme }) => theme.background.transparent.lighter}; + border: 1px solid ${({ theme }) => theme.border.color.medium}; + border-radius: ${({ theme }) => theme.border.radius.sm}; + box-sizing: border-box; display: flex; flex-direction: column; gap: ${({ theme }) => theme.spacing(1)}; + padding: ${({ theme }) => theme.spacing(3)} ${({ theme }) => theme.spacing(4)}; `; const StyledScheduleDescription = styled.div` - color: ${({ theme }) => theme.font.color.primary}; + color: ${({ theme }) => theme.font.color.secondary}; font-size: ${({ theme }) => theme.font.size.sm}; font-weight: ${({ theme }) => theme.font.weight.medium}; `; -const StyledScheduleSubtext = styled.div` - color: ${({ theme }) => theme.font.color.tertiary}; - font-size: ${({ theme }) => theme.font.size.xs}; +const StyledScheduleTitle = styled.div` + color: ${({ theme }) => theme.font.color.primary}; + font-size: ${({ theme }) => theme.font.size.sm}; + font-weight: ${({ theme }) => theme.font.weight.medium}; + margin-bottom: ${({ theme }) => theme.spacing(1)}; `; const StyledExecutionItem = styled.div` color: ${({ theme }) => theme.font.color.secondary}; - font-family: monospace; - font-size: ${({ theme }) => theme.font.size.xs}; - margin-bottom: ${({ theme }) => theme.spacing(0.5)}; + font-size: ${({ theme }) => theme.font.size.sm}; + font-weight: ${({ theme }) => theme.font.weight.medium}; + margin-top: ${({ theme }) => theme.spacing(0.5)}; `; type CronExpressionHelperProps = { trigger: WorkflowCronTrigger; isVisible?: boolean; + isScheduleVisible?: boolean; + isUpcomingExecutionVisible?: boolean; }; export const CronExpressionHelper = ({ trigger, isVisible = true, + isScheduleVisible = true, + isUpcomingExecutionVisible = true, }: CronExpressionHelperProps) => { const { timeZone, dateFormat, timeFormat } = useDateTimeFormat(); const dateLocale = useRecoilValue(dateLocaleState); @@ -143,19 +113,17 @@ export const CronExpressionHelper = ({ return ( - - {t`Schedule`} - - {customDescription} - - - {t`Schedule runs in UTC timezone.`} - - - - {nextExecutions.length > 0 && ( + {isScheduleVisible && ( - {t`Upcoming execution times (${timeZone})`} + {t`Schedule`} + + {customDescription} + + + )} + {nextExecutions.length > 0 && isUpcomingExecutionVisible && ( + + {t`Upcoming execution time (${timeZone})`} {nextExecutions.slice(0, 3).map((execution, index) => ( {formatDateTimeString({ diff --git a/packages/twenty-front/src/modules/workflow/workflow-trigger/components/CronExpressionHelperLazy.tsx b/packages/twenty-front/src/modules/workflow/workflow-trigger/components/CronExpressionHelperLazy.tsx index eec2c9d2d7..37a0cc44d1 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-trigger/components/CronExpressionHelperLazy.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-trigger/components/CronExpressionHelperLazy.tsx @@ -10,11 +10,15 @@ const CronExpressionHelperComponent = lazy(() => type CronExpressionHelperLazyProps = { trigger: WorkflowCronTrigger; isVisible?: boolean; + isScheduleVisible?: boolean; + isUpcomingExecutionVisible?: boolean; }; export const CronExpressionHelperLazy = ({ trigger, isVisible = true, + isScheduleVisible = true, + isUpcomingExecutionVisible = true, }: CronExpressionHelperLazyProps) => { if (!isVisible) { return null; @@ -22,7 +26,12 @@ export const CronExpressionHelperLazy = ({ return ( - + ); }; 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 index 97a479251b..9c1fae2f88 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerCronForm.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerCronForm.tsx @@ -1,25 +1,30 @@ import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader'; import { FormNumberFieldInput } from '@/object-record/record-field/ui/form-types/components/FormNumberFieldInput'; +import { FormSelectFieldInput } from '@/object-record/record-field/ui/form-types/components/FormSelectFieldInput'; import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput'; -import { Select } from '@/ui/input/components/Select'; -import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; import { type WorkflowCronTrigger } from '@/workflow/types/Workflow'; import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody'; import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter'; import { CronExpressionHelperLazy } from '@/workflow/workflow-trigger/components/CronExpressionHelperLazy'; -import { CRON_TRIGGER_INTERVAL_OPTIONS } from '@/workflow/workflow-trigger/constants/CronTriggerIntervalOptions'; +import { + CRON_TRIGGER_INTERVAL_OPTIONS, + type CronTriggerInterval, +} from '@/workflow/workflow-trigger/constants/CronTriggerIntervalOptions'; import { getCronTriggerDefaultSettings } from '@/workflow/workflow-trigger/utils/getCronTriggerDefaultSettings'; import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerDefaultLabel'; import { getTriggerHeaderType } from '@/workflow/workflow-trigger/utils/getTriggerHeaderType'; import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon'; import { getTriggerIconColor } from '@/workflow/workflow-trigger/utils/getTriggerIconColor'; +import { getTriggerScheduleDescription } from '@/workflow/workflow-trigger/utils/getTriggerScheduleDescription'; import { useTheme } from '@emotion/react'; import { t } from '@lingui/core/macro'; import { isNumber } from '@sniptt/guards'; import { useState } from 'react'; +import { useRecoilValue } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; import { TRIGGER_STEP_ID } from 'twenty-shared/workflow'; import { useIcons } from 'twenty-ui/display'; +import { dateLocaleState } from '~/localization/states/dateLocaleState'; type WorkflowEditTriggerCronFormProps = { trigger: WorkflowCronTrigger; @@ -51,6 +56,7 @@ export const WorkflowEditTriggerCronForm = ({ const theme = useTheme(); const [errorMessages, setErrorMessages] = useState({}); const [errorMessagesVisible, setErrorMessagesVisible] = useState(false); + const dateLocale = useRecoilValue(dateLocaleState); const { getIcon } = useIcons(); @@ -60,6 +66,11 @@ export const WorkflowEditTriggerCronForm = ({ const headerTitle = trigger.name ?? defaultLabel; const headerType = getTriggerHeaderType(trigger); + const customDescription = getTriggerScheduleDescription( + trigger, + dateLocale.localeCatalog, + ); + const onBlur = () => { setErrorMessagesVisible(true); }; @@ -85,30 +96,30 @@ export const WorkflowEditTriggerCronForm = ({ iconTooltip={getTriggerDefaultLabel(trigger)} /> -