fix: custom-cron-ui (#15786)
Fixes #14723 ### Summary Fixes a UI while creating a custom CRON ### Changes Made - updated `WorkflowEditTriggerCronForm.tsx` to use the predefined `FormSelectFieldInput.tsx` instead of using `Select.tsx` - added `hint` prop to `FormSelectFieldInput.tsx` - updated styling for showing `Upcoming execution time` - added conditional render for `Schedule` and `Upcoming Execution Time` - `Schedule` section is not rendered in Custom Cron ### Steps to Reproduce (Before Fix) 1. Navigate to - /object/workflow/xxxx 2. Try adding a new Trigger 3. Set Trigger Interval to Cron (Custom) ### Before  ### After  --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
+4
@@ -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 = ({
|
||||
/>
|
||||
)}
|
||||
</FormFieldInputRowContainer>
|
||||
{hint && <InputHint>{hint}</InputHint>}
|
||||
</FormFieldInputContainer>
|
||||
);
|
||||
};
|
||||
|
||||
+30
-62
@@ -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 (
|
||||
<StyledContainer>
|
||||
<StyledSection>
|
||||
<InputLabel>{t`Schedule`}</InputLabel>
|
||||
<StyledScheduleDescription>
|
||||
{customDescription}
|
||||
</StyledScheduleDescription>
|
||||
<StyledScheduleSubtext>
|
||||
{t`Schedule runs in UTC timezone.`}
|
||||
</StyledScheduleSubtext>
|
||||
</StyledSection>
|
||||
|
||||
{nextExecutions.length > 0 && (
|
||||
{isScheduleVisible && (
|
||||
<StyledSection>
|
||||
<InputLabel>{t`Upcoming execution times (${timeZone})`}</InputLabel>
|
||||
<StyledScheduleTitle>{t`Schedule`}</StyledScheduleTitle>
|
||||
<StyledScheduleDescription>
|
||||
{customDescription}
|
||||
</StyledScheduleDescription>
|
||||
</StyledSection>
|
||||
)}
|
||||
{nextExecutions.length > 0 && isUpcomingExecutionVisible && (
|
||||
<StyledSection>
|
||||
<StyledScheduleTitle>{t`Upcoming execution time (${timeZone})`}</StyledScheduleTitle>
|
||||
{nextExecutions.slice(0, 3).map((execution, index) => (
|
||||
<StyledExecutionItem key={index}>
|
||||
{formatDateTimeString({
|
||||
|
||||
+10
-1
@@ -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 (
|
||||
<Suspense fallback={null}>
|
||||
<CronExpressionHelperComponent trigger={trigger} isVisible={isVisible} />
|
||||
<CronExpressionHelperComponent
|
||||
trigger={trigger}
|
||||
isVisible={isVisible}
|
||||
isScheduleVisible={isScheduleVisible}
|
||||
isUpcomingExecutionVisible={isUpcomingExecutionVisible}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
+26
-14
@@ -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<FormErrorMessages>({});
|
||||
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)}
|
||||
/>
|
||||
<WorkflowStepBody>
|
||||
<Select
|
||||
dropdownId="workflow-edit-cron-trigger-interval"
|
||||
<FormSelectFieldInput
|
||||
label={t`Trigger interval`}
|
||||
fullWidth
|
||||
disabled={triggerOptions.readonly}
|
||||
value={trigger.settings.type}
|
||||
hint={t`Cron will be triggered at UTC time`}
|
||||
defaultValue={trigger.settings.type}
|
||||
options={CRON_TRIGGER_INTERVAL_OPTIONS}
|
||||
readonly={triggerOptions.readonly}
|
||||
onChange={(newTriggerType) => {
|
||||
if (triggerOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!newTriggerType) {
|
||||
return;
|
||||
}
|
||||
setErrorMessages({});
|
||||
|
||||
setErrorMessagesVisible(false);
|
||||
|
||||
triggerOptions.onTriggerUpdate({
|
||||
...trigger,
|
||||
settings: getCronTriggerDefaultSettings(newTriggerType),
|
||||
settings: getCronTriggerDefaultSettings(
|
||||
newTriggerType as CronTriggerInterval,
|
||||
),
|
||||
});
|
||||
}}
|
||||
withSearchInput
|
||||
dropdownOffset={{ y: parseInt(theme.spacing(1), 10) }}
|
||||
dropdownWidth={GenericDropdownContentWidth.ExtraLarge}
|
||||
/>
|
||||
{trigger.settings.type === 'CUSTOM' && (
|
||||
<>
|
||||
@@ -117,7 +128,7 @@ export const WorkflowEditTriggerCronForm = ({
|
||||
placeholder="0 */1 * * *"
|
||||
error={errorMessagesVisible ? errorMessages.CUSTOM : undefined}
|
||||
onBlur={onBlur}
|
||||
hint={t`Format: [Minute] [Hour] [Day of Month] [Month] [Day of Week]`}
|
||||
hint={customDescription ?? ''}
|
||||
readonly={triggerOptions.readonly}
|
||||
defaultValue={trigger.settings.pattern}
|
||||
onChange={async (newPattern: string) => {
|
||||
@@ -154,6 +165,7 @@ export const WorkflowEditTriggerCronForm = ({
|
||||
<CronExpressionHelperLazy
|
||||
trigger={trigger}
|
||||
isVisible={!!trigger.settings.pattern && !errorMessages.CUSTOM}
|
||||
isScheduleVisible={false}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { type WorkflowCronTrigger } from '@/workflow/types/Workflow';
|
||||
|
||||
export 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;
|
||||
}
|
||||
};
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
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 { t } from '@lingui/core/macro';
|
||||
|
||||
export 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;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user