Feat: add support for day-first and year-first date formats (DD/MM/YYYY, YYYY/MM/DD) (#12333)

Closes #12152 



https://github.com/user-attachments/assets/53640777-578f-4de8-a1f8-52d409a7582d

---------

Co-authored-by: etiennejouan <jouan.etienne@gmail.com>
This commit is contained in:
Abdul Rahman
2025-06-03 17:42:01 +05:30
committed by GitHub
parent 70cc3e75fe
commit 278a7baf5e
14 changed files with 125 additions and 114 deletions

View File

@ -11,23 +11,14 @@ import {
} from '@/ui/input/components/internal/date/components/InternalDatePicker'; } from '@/ui/input/components/internal/date/components/InternalDatePicker';
import { MAX_DATE } from '@/ui/input/components/internal/date/constants/MaxDate'; import { MAX_DATE } from '@/ui/input/components/internal/date/constants/MaxDate';
import { MIN_DATE } from '@/ui/input/components/internal/date/constants/MinDate'; import { MIN_DATE } from '@/ui/input/components/internal/date/constants/MinDate';
import { parseDateToString } from '@/ui/input/components/internal/date/utils/parseDateToString'; import { useDateParser } from '@/ui/input/components/internal/hooks/useDateParser';
import { parseStringToDate } from '@/ui/input/components/internal/date/utils/parseStringToDate';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer'; import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { UserContext } from '@/users/contexts/UserContext';
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString'; import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
import { css } from '@emotion/react'; import { css } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { import { ChangeEvent, KeyboardEvent, useId, useRef, useState } from 'react';
ChangeEvent,
KeyboardEvent,
useContext,
useId,
useRef,
useState,
} from 'react';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { TEXT_INPUT_STYLE } from 'twenty-ui/theme'; import { TEXT_INPUT_STYLE } from 'twenty-ui/theme';
import { Nullable } from 'twenty-ui/utilities'; import { Nullable } from 'twenty-ui/utilities';
@ -93,7 +84,9 @@ export const FormDateTimeFieldInput = ({
readonly, readonly,
placeholder, placeholder,
}: FormDateTimeFieldInputProps) => { }: FormDateTimeFieldInputProps) => {
const { timeZone } = useContext(UserContext); const { parseToString, parseToDate } = useDateParser({
isDateTimeInput: !dateOnly,
});
const inputId = useId(); const inputId = useId();
@ -121,11 +114,7 @@ export const FormDateTimeFieldInput = ({
const [inputDateTime, setInputDateTime] = useState( const [inputDateTime, setInputDateTime] = useState(
isDefined(draftValueAsDate) && !isStandaloneVariableString(defaultValue) isDefined(draftValueAsDate) && !isStandaloneVariableString(defaultValue)
? parseDateToString({ ? parseToString(draftValueAsDate)
date: draftValueAsDate,
isDateTimeInput: !dateOnly,
userTimezone: timeZone,
})
: '', : '',
); );
@ -172,15 +161,7 @@ export const FormDateTimeFieldInput = ({
value: newDate?.toDateString() ?? null, value: newDate?.toDateString() ?? null,
}); });
setInputDateTime( setInputDateTime(isDefined(newDate) ? parseToString(newDate) : '');
isDefined(newDate)
? parseDateToString({
date: newDate,
isDateTimeInput: !dateOnly,
userTimezone: timeZone,
})
: '',
);
setPickerDate(newDate); setPickerDate(newDate);
@ -230,15 +211,7 @@ export const FormDateTimeFieldInput = ({
setPickerDate(newDate); setPickerDate(newDate);
setInputDateTime( setInputDateTime(isDefined(newDate) ? parseToString(newDate) : '');
isDefined(newDate)
? parseDateToString({
date: newDate,
isDateTimeInput: !dateOnly,
userTimezone: timeZone,
})
: '',
);
persistDate(newDate); persistDate(newDate);
}; };
@ -264,15 +237,10 @@ export const FormDateTimeFieldInput = ({
if (inputDateTimeTrimmed === '') { if (inputDateTimeTrimmed === '') {
handlePickerClear(); handlePickerClear();
return; return;
} }
const parsedInputDateTime = parseStringToDate({ const parsedInputDateTime = parseToDate(inputDateTimeTrimmed);
dateAsString: inputDateTimeTrimmed,
isDateTimeInput: !dateOnly,
userTimezone: timeZone,
});
if (!isDefined(parsedInputDateTime)) { if (!isDefined(parsedInputDateTime)) {
return; return;
@ -293,13 +261,7 @@ export const FormDateTimeFieldInput = ({
setPickerDate(validatedDate); setPickerDate(validatedDate);
setInputDateTime( setInputDateTime(parseToString(validatedDate));
parseDateToString({
date: validatedDate,
isDateTimeInput: !dateOnly,
userTimezone: timeZone,
}),
);
persistDate(validatedDate); persistDate(validatedDate);
}; };

View File

@ -5,12 +5,12 @@ import { Select } from '@/ui/input/components/Select';
import { DateTimeInput } from '@/ui/input/components/internal/date/components/DateTimeInput'; import { DateTimeInput } from '@/ui/input/components/internal/date/components/DateTimeInput';
import { getMonthSelectOptions } from '@/ui/input/components/internal/date/utils/getMonthSelectOptions'; import { getMonthSelectOptions } from '@/ui/input/components/internal/date/utils/getMonthSelectOptions';
import { IconChevronLeft, IconChevronRight } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { import {
MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID, MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID,
MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID, MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID,
} from './InternalDatePicker'; } from './InternalDatePicker';
import { IconChevronLeft, IconChevronRight } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
const StyledCustomDatePickerHeader = styled.div` const StyledCustomDatePickerHeader = styled.div`
align-items: center; align-items: center;
@ -38,7 +38,6 @@ type AbsoluteDatePickerHeaderProps = {
prevMonthButtonDisabled: boolean; prevMonthButtonDisabled: boolean;
nextMonthButtonDisabled: boolean; nextMonthButtonDisabled: boolean;
isDateTimeInput?: boolean; isDateTimeInput?: boolean;
timeZone: string;
hideInput?: boolean; hideInput?: boolean;
}; };
@ -52,7 +51,6 @@ export const AbsoluteDatePickerHeader = ({
prevMonthButtonDisabled, prevMonthButtonDisabled,
nextMonthButtonDisabled, nextMonthButtonDisabled,
isDateTimeInput, isDateTimeInput,
timeZone,
hideInput = false, hideInput = false,
}: AbsoluteDatePickerHeaderProps) => { }: AbsoluteDatePickerHeaderProps) => {
const endOfDayDateTimeInLocalTimezone = DateTime.now().set({ const endOfDayDateTimeInLocalTimezone = DateTime.now().set({
@ -74,7 +72,6 @@ export const AbsoluteDatePickerHeader = ({
date={date} date={date}
isDateTimeInput={isDateTimeInput} isDateTimeInput={isDateTimeInput}
onChange={onChange} onChange={onChange}
userTimezone={timeZone}
/> />
)} )}

View File

@ -1,18 +1,19 @@
import { css } from '@emotion/react'; import { css } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useCallback, useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useIMask } from 'react-imask'; import { useIMask } from 'react-imask';
import { useRecoilValue } from 'recoil';
import { dateTimeFormatState } from '@/localization/states/dateTimeFormatState';
import { DATE_BLOCKS } from '@/ui/input/components/internal/date/constants/DateBlocks'; import { DATE_BLOCKS } from '@/ui/input/components/internal/date/constants/DateBlocks';
import { DATE_MASK } from '@/ui/input/components/internal/date/constants/DateMask';
import { DATE_TIME_BLOCKS } from '@/ui/input/components/internal/date/constants/DateTimeBlocks'; import { DATE_TIME_BLOCKS } from '@/ui/input/components/internal/date/constants/DateTimeBlocks';
import { DATE_TIME_MASK } from '@/ui/input/components/internal/date/constants/DateTimeMask';
import { MAX_DATE } from '@/ui/input/components/internal/date/constants/MaxDate'; import { MAX_DATE } from '@/ui/input/components/internal/date/constants/MaxDate';
import { MIN_DATE } from '@/ui/input/components/internal/date/constants/MinDate'; import { MIN_DATE } from '@/ui/input/components/internal/date/constants/MinDate';
import { parseDateToString } from '@/ui/input/components/internal/date/utils/parseDateToString'; import { getDateMask } from '@/ui/input/components/internal/date/utils/getDateMask';
import { parseStringToDate } from '@/ui/input/components/internal/date/utils/parseStringToDate'; import { getDateTimeMask } from '@/ui/input/components/internal/date/utils/getDateTimeMask';
import { isNull } from '@sniptt/guards'; import { isNull } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { useDateParser } from '../../hooks/useDateParser';
const StyledInputContainer = styled.div` const StyledInputContainer = styled.div`
align-items: center; align-items: center;
@ -44,42 +45,30 @@ type DateTimeInputProps = {
onChange?: (date: Date | null) => void; onChange?: (date: Date | null) => void;
date: Date | null; date: Date | null;
isDateTimeInput?: boolean; isDateTimeInput?: boolean;
userTimezone?: string;
onError?: (error: Error) => void;
}; };
export const DateTimeInput = ({ export const DateTimeInput = ({
date, date,
onChange, onChange,
isDateTimeInput, isDateTimeInput,
userTimezone,
}: DateTimeInputProps) => { }: DateTimeInputProps) => {
const [hasError, setHasError] = useState(false); const [hasError, setHasError] = useState(false);
const { dateFormat } = useRecoilValue(dateTimeFormatState);
const handleParseDateToString = useCallback( const { parseToString, parseToDate } = useDateParser({
(date: any) => { isDateTimeInput: isDateTimeInput === true,
return parseDateToString({ });
date,
isDateTimeInput: isDateTimeInput === true,
userTimezone,
});
},
[isDateTimeInput, userTimezone],
);
const handleParseStringToDate = (str: string) => { const handleParseStringToDate = (str: string) => {
const date = parseStringToDate({ const date = parseToDate(str);
dateAsString: str,
isDateTimeInput: isDateTimeInput === true,
userTimezone,
});
setHasError(isNull(date) === true); setHasError(isNull(date) === true);
return date; return date;
}; };
const pattern = isDateTimeInput ? DATE_TIME_MASK : DATE_MASK; const pattern = isDateTimeInput
? getDateTimeMask(dateFormat)
: getDateMask(dateFormat);
const blocks = isDateTimeInput ? DATE_TIME_BLOCKS : DATE_BLOCKS; const blocks = isDateTimeInput ? DATE_TIME_BLOCKS : DATE_BLOCKS;
const { ref, setValue, value } = useIMask( const { ref, setValue, value } = useIMask(
@ -89,18 +78,14 @@ export const DateTimeInput = ({
blocks, blocks,
min: MIN_DATE, min: MIN_DATE,
max: MAX_DATE, max: MAX_DATE,
format: handleParseDateToString, format: (date: any) => parseToString(date),
parse: handleParseStringToDate, parse: handleParseStringToDate,
lazy: false, lazy: false,
autofix: true, autofix: true,
}, },
{ {
onComplete: (value) => { onComplete: (value) => {
const parsedDate = parseStringToDate({ const parsedDate = parseToDate(value);
dateAsString: value,
isDateTimeInput: isDateTimeInput === true,
userTimezone,
});
onChange?.(parsedDate); onChange?.(parsedDate);
}, },
@ -115,23 +100,14 @@ export const DateTimeInput = ({
return; return;
} }
setValue( setValue(parseToString(date));
parseDateToString({ }, [date, setValue, parseToString]);
date: date,
isDateTimeInput: isDateTimeInput === true,
userTimezone,
}),
);
}, [date, setValue, isDateTimeInput, userTimezone]);
return ( return (
<StyledInputContainer> <StyledInputContainer>
<StyledInput <StyledInput
type="text" type="text"
ref={ref as any} ref={ref as any}
placeholder={`Type date${
isDateTimeInput ? ' and time' : ' (mm/dd/yyyy)'
}`}
value={value} value={value}
onChange={() => {}} // Prevent React warning onChange={() => {}} // Prevent React warning
hasError={hasError} hasError={hasError}

View File

@ -511,7 +511,6 @@ export const DateTimePicker = ({
date={internalDate} date={internalDate}
isDateTimeInput={isDateTimeInput} isDateTimeInput={isDateTimeInput}
onChange={onChange} onChange={onChange}
userTimezone={timeZone}
/> />
} }
renderCustomHeader={({ renderCustomHeader={({
@ -536,7 +535,6 @@ export const DateTimePicker = ({
prevMonthButtonDisabled={prevMonthButtonDisabled} prevMonthButtonDisabled={prevMonthButtonDisabled}
nextMonthButtonDisabled={nextMonthButtonDisabled} nextMonthButtonDisabled={nextMonthButtonDisabled}
isDateTimeInput={isDateTimeInput} isDateTimeInput={isDateTimeInput}
timeZone={timeZone}
hideInput={hideHeaderInput} hideInput={hideHeaderInput}
/> />
) )

View File

@ -1 +0,0 @@
export const DATE_MASK = 'm`/d`/Y`'; // See https://imask.js.org/guide.html#masked-date

View File

@ -1 +0,0 @@
export const DATE_PARSER_FORMAT = 'MM/dd/yyyy';

View File

@ -1,3 +0,0 @@
import { TIME_MASK } from '@/ui/input/components/internal/date/constants/TimeMask';
export const DATE_TIME_MASK = `m\`/d\`/Y\` ${TIME_MASK}`;

View File

@ -1 +0,0 @@
export const DATE_TIME_PARSER_FORMAT = 'MM/dd/yyyy HH:mm';

View File

@ -0,0 +1,13 @@
import { DateFormat } from '~/modules/localization/constants/DateFormat';
export const getDateMask = (dateFormat: DateFormat): string => {
switch (dateFormat) {
case DateFormat.DAY_FIRST:
return 'd`/m`/Y`';
case DateFormat.YEAR_FIRST:
return 'Y`-m`-d`';
case DateFormat.MONTH_FIRST:
default:
return 'm`/d`/Y`';
}
};

View File

@ -0,0 +1,8 @@
import { TIME_MASK } from '@/ui/input/components/internal/date/constants/TimeMask';
import { DateFormat } from '@/localization/constants/DateFormat';
import { getDateMask } from './getDateMask';
export const getDateTimeMask = (dateFormat: DateFormat): string => {
return `${getDateMask(dateFormat)} ${TIME_MASK}`;
};

View File

@ -1,21 +1,21 @@
import { DATE_PARSER_FORMAT } from '@/ui/input/components/internal/date/constants/DateParserFormat'; import { DateFormat } from '@/localization/constants/DateFormat';
import { DATE_TIME_PARSER_FORMAT } from '@/ui/input/components/internal/date/constants/DateTimeParserFormat';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { getDateFormatString } from '~/utils/date-utils';
type ParseDateToStringArgs = { type ParseDateToStringArgs = {
date: Date; date: Date;
isDateTimeInput: boolean; isDateTimeInput: boolean;
userTimezone: string | undefined; userTimezone: string | undefined;
dateFormat?: DateFormat;
}; };
export const parseDateToString = ({ export const parseDateToString = ({
date, date,
isDateTimeInput, isDateTimeInput,
userTimezone, userTimezone,
dateFormat = DateFormat.MONTH_FIRST,
}: ParseDateToStringArgs) => { }: ParseDateToStringArgs) => {
const parsingFormat = isDateTimeInput const parsingFormat = getDateFormatString(dateFormat, isDateTimeInput);
? DATE_TIME_PARSER_FORMAT
: DATE_PARSER_FORMAT;
const dateParsed = DateTime.fromJSDate(date, { zone: userTimezone }); const dateParsed = DateTime.fromJSDate(date, { zone: userTimezone });

View File

@ -1,21 +1,21 @@
import { DATE_PARSER_FORMAT } from '@/ui/input/components/internal/date/constants/DateParserFormat'; import { DateFormat } from '@/localization/constants/DateFormat';
import { DATE_TIME_PARSER_FORMAT } from '@/ui/input/components/internal/date/constants/DateTimeParserFormat';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { getDateFormatString } from '~/utils/date-utils';
type ParseStringToDateArgs = { type ParseStringToDateArgs = {
dateAsString: string; dateAsString: string;
isDateTimeInput: boolean; isDateTimeInput: boolean;
userTimezone: string | undefined; userTimezone: string | undefined;
dateFormat: DateFormat;
}; };
export const parseStringToDate = ({ export const parseStringToDate = ({
dateAsString, dateAsString,
isDateTimeInput, isDateTimeInput,
userTimezone, userTimezone,
dateFormat,
}: ParseStringToDateArgs) => { }: ParseStringToDateArgs) => {
const parsingFormat = isDateTimeInput const parsingFormat = getDateFormatString(dateFormat, isDateTimeInput);
? DATE_TIME_PARSER_FORMAT
: DATE_PARSER_FORMAT;
const parsedDate = isDateTimeInput const parsedDate = isDateTimeInput
? DateTime.fromFormat(dateAsString, parsingFormat, { zone: userTimezone }) ? DateTime.fromFormat(dateAsString, parsingFormat, { zone: userTimezone })

View File

@ -0,0 +1,44 @@
import { dateTimeFormatState } from '@/localization/states/dateTimeFormatState';
import { UserContext } from '@/users/contexts/UserContext';
import { useCallback, useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { parseDateToString } from '../date/utils/parseDateToString';
import { parseStringToDate } from '../date/utils/parseStringToDate';
type UseDateParserProps = {
isDateTimeInput: boolean;
};
export const useDateParser = ({ isDateTimeInput }: UseDateParserProps) => {
const { dateFormat } = useRecoilValue(dateTimeFormatState);
const { timeZone } = useContext(UserContext);
const parseToString = useCallback(
(date: Date) => {
return parseDateToString({
date,
isDateTimeInput,
userTimezone: timeZone,
dateFormat,
});
},
[dateFormat, isDateTimeInput, timeZone],
);
const parseToDate = useCallback(
(dateAsString: string) => {
return parseStringToDate({
dateAsString,
isDateTimeInput,
userTimezone: timeZone,
dateFormat,
});
},
[dateFormat, isDateTimeInput, timeZone],
);
return {
parseToString,
parseToDate,
};
};

View File

@ -4,9 +4,11 @@ import { differenceInCalendarDays, formatDistanceToNow } from 'date-fns';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import moize from 'moize'; import moize from 'moize';
import { logError } from './logError'; import { DateFormat } from '@/localization/constants/DateFormat';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { logError } from './logError';
export const DEFAULT_DATE_LOCALE = 'en-EN'; export const DEFAULT_DATE_LOCALE = 'en-EN';
export const parseDate = (dateToParse: Date | string | number) => { export const parseDate = (dateToParse: Date | string | number) => {
@ -212,3 +214,20 @@ export const formatToHumanReadableDateTime = (date: Date | string) => {
minute: 'numeric', minute: 'numeric',
}).format(parsedJSDate); }).format(parsedJSDate);
}; };
export const getDateFormatString = (
dateFormat: DateFormat,
isDateTimeInput: boolean,
): string => {
const timePart = isDateTimeInput ? ' HH:mm' : '';
switch (dateFormat) {
case DateFormat.DAY_FIRST:
return `dd/MM/yyyy${timePart}`;
case DateFormat.YEAR_FIRST:
return `yyyy-MM-dd${timePart}`;
case DateFormat.MONTH_FIRST:
default:
return `MM/dd/yyyy${timePart}`;
}
};