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:
@ -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);
|
||||
};
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@ -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 (
|
||||
<StyledInputContainer>
|
||||
<StyledInput
|
||||
type="text"
|
||||
ref={ref as any}
|
||||
placeholder={`Type date${
|
||||
isDateTimeInput ? ' and time' : ' (mm/dd/yyyy)'
|
||||
}`}
|
||||
value={value}
|
||||
onChange={() => {}} // Prevent React warning
|
||||
hasError={hasError}
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
)
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export const DATE_MASK = 'm`/d`/Y`'; // See https://imask.js.org/guide.html#masked-date
|
||||
@ -1 +0,0 @@
|
||||
export const DATE_PARSER_FORMAT = 'MM/dd/yyyy';
|
||||
@ -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}`;
|
||||
@ -1 +0,0 @@
|
||||
export const DATE_TIME_PARSER_FORMAT = 'MM/dd/yyyy HH:mm';
|
||||
@ -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`';
|
||||
}
|
||||
};
|
||||
@ -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}`;
|
||||
};
|
||||
@ -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 });
|
||||
|
||||
|
||||
@ -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 })
|
||||
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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}`;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user