diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateTimeFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateTimeFieldInput.tsx index 1705df730..b93e545dd 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateTimeFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateTimeFieldInput.tsx @@ -11,23 +11,14 @@ import { } from '@/ui/input/components/internal/date/components/InternalDatePicker'; import { MAX_DATE } from '@/ui/input/components/internal/date/constants/MaxDate'; import { MIN_DATE } from '@/ui/input/components/internal/date/constants/MinDate'; -import { parseDateToString } from '@/ui/input/components/internal/date/utils/parseDateToString'; -import { parseStringToDate } from '@/ui/input/components/internal/date/utils/parseStringToDate'; +import { useDateParser } from '@/ui/input/components/internal/hooks/useDateParser'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; -import { UserContext } from '@/users/contexts/UserContext'; import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString'; import { css } from '@emotion/react'; import styled from '@emotion/styled'; -import { - ChangeEvent, - KeyboardEvent, - useContext, - useId, - useRef, - useState, -} from 'react'; +import { ChangeEvent, KeyboardEvent, useId, useRef, useState } from 'react'; import { isDefined } from 'twenty-shared/utils'; import { TEXT_INPUT_STYLE } from 'twenty-ui/theme'; import { Nullable } from 'twenty-ui/utilities'; @@ -93,7 +84,9 @@ export const FormDateTimeFieldInput = ({ readonly, placeholder, }: FormDateTimeFieldInputProps) => { - const { timeZone } = useContext(UserContext); + const { parseToString, parseToDate } = useDateParser({ + isDateTimeInput: !dateOnly, + }); const inputId = useId(); @@ -121,11 +114,7 @@ export const FormDateTimeFieldInput = ({ const [inputDateTime, setInputDateTime] = useState( isDefined(draftValueAsDate) && !isStandaloneVariableString(defaultValue) - ? parseDateToString({ - date: draftValueAsDate, - isDateTimeInput: !dateOnly, - userTimezone: timeZone, - }) + ? parseToString(draftValueAsDate) : '', ); @@ -172,15 +161,7 @@ export const FormDateTimeFieldInput = ({ value: newDate?.toDateString() ?? null, }); - setInputDateTime( - isDefined(newDate) - ? parseDateToString({ - date: newDate, - isDateTimeInput: !dateOnly, - userTimezone: timeZone, - }) - : '', - ); + setInputDateTime(isDefined(newDate) ? parseToString(newDate) : ''); setPickerDate(newDate); @@ -230,15 +211,7 @@ export const FormDateTimeFieldInput = ({ setPickerDate(newDate); - setInputDateTime( - isDefined(newDate) - ? parseDateToString({ - date: newDate, - isDateTimeInput: !dateOnly, - userTimezone: timeZone, - }) - : '', - ); + setInputDateTime(isDefined(newDate) ? parseToString(newDate) : ''); persistDate(newDate); }; @@ -264,15 +237,10 @@ export const FormDateTimeFieldInput = ({ if (inputDateTimeTrimmed === '') { handlePickerClear(); - return; } - const parsedInputDateTime = parseStringToDate({ - dateAsString: inputDateTimeTrimmed, - isDateTimeInput: !dateOnly, - userTimezone: timeZone, - }); + const parsedInputDateTime = parseToDate(inputDateTimeTrimmed); if (!isDefined(parsedInputDateTime)) { return; @@ -293,13 +261,7 @@ export const FormDateTimeFieldInput = ({ setPickerDate(validatedDate); - setInputDateTime( - parseDateToString({ - date: validatedDate, - isDateTimeInput: !dateOnly, - userTimezone: timeZone, - }), - ); + setInputDateTime(parseToString(validatedDate)); persistDate(validatedDate); }; diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/AbsoluteDatePickerHeader.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/AbsoluteDatePickerHeader.tsx index 46d1f9ea6..716a53bcb 100644 --- a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/AbsoluteDatePickerHeader.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/AbsoluteDatePickerHeader.tsx @@ -5,12 +5,12 @@ import { Select } from '@/ui/input/components/Select'; import { DateTimeInput } from '@/ui/input/components/internal/date/components/DateTimeInput'; import { getMonthSelectOptions } from '@/ui/input/components/internal/date/utils/getMonthSelectOptions'; +import { IconChevronLeft, IconChevronRight } from 'twenty-ui/display'; +import { LightIconButton } from 'twenty-ui/input'; import { MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID, MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID, } from './InternalDatePicker'; -import { IconChevronLeft, IconChevronRight } from 'twenty-ui/display'; -import { LightIconButton } from 'twenty-ui/input'; const StyledCustomDatePickerHeader = styled.div` align-items: center; @@ -38,7 +38,6 @@ type AbsoluteDatePickerHeaderProps = { prevMonthButtonDisabled: boolean; nextMonthButtonDisabled: boolean; isDateTimeInput?: boolean; - timeZone: string; hideInput?: boolean; }; @@ -52,7 +51,6 @@ export const AbsoluteDatePickerHeader = ({ prevMonthButtonDisabled, nextMonthButtonDisabled, isDateTimeInput, - timeZone, hideInput = false, }: AbsoluteDatePickerHeaderProps) => { const endOfDayDateTimeInLocalTimezone = DateTime.now().set({ @@ -74,7 +72,6 @@ export const AbsoluteDatePickerHeader = ({ date={date} isDateTimeInput={isDateTimeInput} onChange={onChange} - userTimezone={timeZone} /> )} diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/DateTimeInput.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/DateTimeInput.tsx index 6ee480858..af25a771a 100644 --- a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/DateTimeInput.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/DateTimeInput.tsx @@ -1,18 +1,19 @@ import { css } from '@emotion/react'; import styled from '@emotion/styled'; -import { useCallback, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; 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_MASK } from '@/ui/input/components/internal/date/constants/DateMask'; 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 { MIN_DATE } from '@/ui/input/components/internal/date/constants/MinDate'; -import { parseDateToString } from '@/ui/input/components/internal/date/utils/parseDateToString'; -import { parseStringToDate } from '@/ui/input/components/internal/date/utils/parseStringToDate'; +import { getDateMask } from '@/ui/input/components/internal/date/utils/getDateMask'; +import { getDateTimeMask } from '@/ui/input/components/internal/date/utils/getDateTimeMask'; import { isNull } from '@sniptt/guards'; import { isDefined } from 'twenty-shared/utils'; +import { useDateParser } from '../../hooks/useDateParser'; const StyledInputContainer = styled.div` align-items: center; @@ -44,42 +45,30 @@ type DateTimeInputProps = { onChange?: (date: Date | null) => void; date: Date | null; isDateTimeInput?: boolean; - userTimezone?: string; - onError?: (error: Error) => void; }; export const DateTimeInput = ({ date, onChange, isDateTimeInput, - userTimezone, }: DateTimeInputProps) => { const [hasError, setHasError] = useState(false); - - const handleParseDateToString = useCallback( - (date: any) => { - return parseDateToString({ - date, - isDateTimeInput: isDateTimeInput === true, - userTimezone, - }); - }, - [isDateTimeInput, userTimezone], - ); + const { dateFormat } = useRecoilValue(dateTimeFormatState); + const { parseToString, parseToDate } = useDateParser({ + isDateTimeInput: isDateTimeInput === true, + }); const handleParseStringToDate = (str: string) => { - const date = parseStringToDate({ - dateAsString: str, - isDateTimeInput: isDateTimeInput === true, - userTimezone, - }); + const date = parseToDate(str); setHasError(isNull(date) === true); 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 { ref, setValue, value } = useIMask( @@ -89,18 +78,14 @@ export const DateTimeInput = ({ blocks, min: MIN_DATE, max: MAX_DATE, - format: handleParseDateToString, + format: (date: any) => parseToString(date), parse: handleParseStringToDate, lazy: false, autofix: true, }, { onComplete: (value) => { - const parsedDate = parseStringToDate({ - dateAsString: value, - isDateTimeInput: isDateTimeInput === true, - userTimezone, - }); + const parsedDate = parseToDate(value); onChange?.(parsedDate); }, @@ -115,23 +100,14 @@ export const DateTimeInput = ({ return; } - setValue( - parseDateToString({ - date: date, - isDateTimeInput: isDateTimeInput === true, - userTimezone, - }), - ); - }, [date, setValue, isDateTimeInput, userTimezone]); + setValue(parseToString(date)); + }, [date, setValue, parseToString]); return ( {}} // Prevent React warning hasError={hasError} diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/InternalDatePicker.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/InternalDatePicker.tsx index c4f70f158..c4c0f02bf 100644 --- a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/InternalDatePicker.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/InternalDatePicker.tsx @@ -511,7 +511,6 @@ export const DateTimePicker = ({ date={internalDate} isDateTimeInput={isDateTimeInput} onChange={onChange} - userTimezone={timeZone} /> } renderCustomHeader={({ @@ -536,7 +535,6 @@ export const DateTimePicker = ({ prevMonthButtonDisabled={prevMonthButtonDisabled} nextMonthButtonDisabled={nextMonthButtonDisabled} isDateTimeInput={isDateTimeInput} - timeZone={timeZone} hideInput={hideHeaderInput} /> ) diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/DateMask.ts b/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/DateMask.ts deleted file mode 100644 index c4cd30a3a..000000000 --- a/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/DateMask.ts +++ /dev/null @@ -1 +0,0 @@ -export const DATE_MASK = 'm`/d`/Y`'; // See https://imask.js.org/guide.html#masked-date diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/DateParserFormat.ts b/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/DateParserFormat.ts deleted file mode 100644 index a75f42564..000000000 --- a/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/DateParserFormat.ts +++ /dev/null @@ -1 +0,0 @@ -export const DATE_PARSER_FORMAT = 'MM/dd/yyyy'; diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/DateTimeMask.ts b/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/DateTimeMask.ts deleted file mode 100644 index c7297a0ee..000000000 --- a/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/DateTimeMask.ts +++ /dev/null @@ -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}`; diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/DateTimeParserFormat.ts b/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/DateTimeParserFormat.ts deleted file mode 100644 index 59fa44c42..000000000 --- a/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/DateTimeParserFormat.ts +++ /dev/null @@ -1 +0,0 @@ -export const DATE_TIME_PARSER_FORMAT = 'MM/dd/yyyy HH:mm'; diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/utils/getDateMask.ts b/packages/twenty-front/src/modules/ui/input/components/internal/date/utils/getDateMask.ts new file mode 100644 index 000000000..6f1fa5f85 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/utils/getDateMask.ts @@ -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`'; + } +}; diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/utils/getDateTimeMask.ts b/packages/twenty-front/src/modules/ui/input/components/internal/date/utils/getDateTimeMask.ts new file mode 100644 index 000000000..bb14169fe --- /dev/null +++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/utils/getDateTimeMask.ts @@ -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}`; +}; diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/utils/parseDateToString.ts b/packages/twenty-front/src/modules/ui/input/components/internal/date/utils/parseDateToString.ts index c8e1625e0..3a6ebd926 100644 --- a/packages/twenty-front/src/modules/ui/input/components/internal/date/utils/parseDateToString.ts +++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/utils/parseDateToString.ts @@ -1,21 +1,21 @@ -import { DATE_PARSER_FORMAT } from '@/ui/input/components/internal/date/constants/DateParserFormat'; -import { DATE_TIME_PARSER_FORMAT } from '@/ui/input/components/internal/date/constants/DateTimeParserFormat'; +import { DateFormat } from '@/localization/constants/DateFormat'; import { DateTime } from 'luxon'; +import { getDateFormatString } from '~/utils/date-utils'; type ParseDateToStringArgs = { date: Date; isDateTimeInput: boolean; userTimezone: string | undefined; + dateFormat?: DateFormat; }; export const parseDateToString = ({ date, isDateTimeInput, userTimezone, + dateFormat = DateFormat.MONTH_FIRST, }: ParseDateToStringArgs) => { - const parsingFormat = isDateTimeInput - ? DATE_TIME_PARSER_FORMAT - : DATE_PARSER_FORMAT; + const parsingFormat = getDateFormatString(dateFormat, isDateTimeInput); const dateParsed = DateTime.fromJSDate(date, { zone: userTimezone }); diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/utils/parseStringToDate.ts b/packages/twenty-front/src/modules/ui/input/components/internal/date/utils/parseStringToDate.ts index b678aa364..28ca88335 100644 --- a/packages/twenty-front/src/modules/ui/input/components/internal/date/utils/parseStringToDate.ts +++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/utils/parseStringToDate.ts @@ -1,21 +1,21 @@ -import { DATE_PARSER_FORMAT } from '@/ui/input/components/internal/date/constants/DateParserFormat'; -import { DATE_TIME_PARSER_FORMAT } from '@/ui/input/components/internal/date/constants/DateTimeParserFormat'; +import { DateFormat } from '@/localization/constants/DateFormat'; import { DateTime } from 'luxon'; +import { getDateFormatString } from '~/utils/date-utils'; type ParseStringToDateArgs = { dateAsString: string; isDateTimeInput: boolean; userTimezone: string | undefined; + dateFormat: DateFormat; }; export const parseStringToDate = ({ dateAsString, isDateTimeInput, userTimezone, + dateFormat, }: ParseStringToDateArgs) => { - const parsingFormat = isDateTimeInput - ? DATE_TIME_PARSER_FORMAT - : DATE_PARSER_FORMAT; + const parsingFormat = getDateFormatString(dateFormat, isDateTimeInput); const parsedDate = isDateTimeInput ? DateTime.fromFormat(dateAsString, parsingFormat, { zone: userTimezone }) diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/hooks/useDateParser.ts b/packages/twenty-front/src/modules/ui/input/components/internal/hooks/useDateParser.ts new file mode 100644 index 000000000..cbf06db1a --- /dev/null +++ b/packages/twenty-front/src/modules/ui/input/components/internal/hooks/useDateParser.ts @@ -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, + }; +}; diff --git a/packages/twenty-front/src/utils/date-utils.ts b/packages/twenty-front/src/utils/date-utils.ts index 621c6c784..365d0cc2e 100644 --- a/packages/twenty-front/src/utils/date-utils.ts +++ b/packages/twenty-front/src/utils/date-utils.ts @@ -4,9 +4,11 @@ import { differenceInCalendarDays, formatDistanceToNow } from 'date-fns'; import { DateTime } from 'luxon'; import moize from 'moize'; -import { logError } from './logError'; +import { DateFormat } from '@/localization/constants/DateFormat'; import { isDefined } from 'twenty-shared/utils'; +import { logError } from './logError'; + export const DEFAULT_DATE_LOCALE = 'en-EN'; export const parseDate = (dateToParse: Date | string | number) => { @@ -212,3 +214,20 @@ export const formatToHumanReadableDateTime = (date: Date | string) => { minute: 'numeric', }).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}`; + } +};