Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Jun 3, 2025
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c6a6250
add support for YYYY-MM-DD and MM/DD/YYYY date formats
abdulrahmancodes May 24, 2025
81815cb
Refactor date parsing in FormDateTimeFieldInput and DateTimeInput com…
abdulrahmancodes May 26, 2025
db3f7a8
Remove deprecated date parser format constants for consistency in dat…
abdulrahmancodes May 26, 2025
99903b7
Update date format in getDateFormatString to use slashes instead of d…
abdulrahmancodes May 26, 2025
110b3d1
Refactor date mask handling in DateTimeInput component
abdulrahmancodes May 26, 2025
329614d
update parseDateToString to make dateFormat optional
abdulrahmancodes May 27, 2025
92ccd17
Refactor date formatting in DateTimeInput component by inlining handl…
abdulrahmancodes May 27, 2025
262edcb
Refactor getPlaceholder function in DateTimeInput component to remove…
abdulrahmancodes May 27, 2025
ad708c4
Refactor getPlaceholder function in DateTimeInput component to use a …
abdulrahmancodes May 27, 2025
e2fd07f
Update date format in getDateFormatString to use hyphens for YEAR_FIR…
abdulrahmancodes May 27, 2025
8ae30f3
Refactor parseDateToString to improve date parsing logic and handle t…
abdulrahmancodes May 27, 2025
ee56638
Enhance parseDateToString function to return an empty string for unde…
abdulrahmancodes May 27, 2025
5ea73b0
Remove timeZone prop from AbsoluteDatePickerHeader and InternalDatePi…
abdulrahmancodes May 28, 2025
da570cc
Update import path for DateFormat in DateTimeMask constants
abdulrahmancodes May 28, 2025
2f47514
Remove unused onError prop from DateTimeInput component
abdulrahmancodes May 28, 2025
d88b10a
Update date mask format for YEAR_FIRST to use hyphens instead of slashes
abdulrahmancodes May 28, 2025
727b692
Fix eslint warning in DateTimeInput by adjusting dependency array in …
abdulrahmancodes May 28, 2025
2919335
move getDateTimeMask to date/utils folder
abdulrahmancodes May 28, 2025
b691749
remove unused dateFormat from useDateParser hook
abdulrahmancodes May 28, 2025
cea73d6
Refactor useDateParser hook to utilize useCallback for parseToString …
abdulrahmancodes May 28, 2025
a919c06
Remove early return for undefined dates in parseDateToString function…
abdulrahmancodes May 28, 2025
9d81232
Add getDateMask utility function for date format handling
abdulrahmancodes Jun 2, 2025
0dcc412
Refactor DateTimeInput and getDateTimeMask to use updated getDateMask…
abdulrahmancodes Jun 2, 2025
b6ea032
Merge branch 'main' into feat/new-date-formats
abdulrahmancodes Jun 2, 2025
156ae94
Remove unused timeZone prop from InternalDatePicker component
abdulrahmancodes Jun 2, 2025
d2f6dac
remove unused placeholder
etiennejouan Jun 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -93,7 +84,9 @@ export const FormDateTimeFieldInput = ({
readonly,
placeholder,
}: FormDateTimeFieldInputProps) => {
const { timeZone } = useContext(UserContext);
const { parseToString, parseToDate } = useDateParser({
isDateTimeInput: !dateOnly,
});

const inputId = useId();

Expand Down Expand Up @@ -121,11 +114,7 @@ export const FormDateTimeFieldInput = ({

const [inputDateTime, setInputDateTime] = useState(
isDefined(draftValueAsDate) && !isStandaloneVariableString(defaultValue)
? parseDateToString({
date: draftValueAsDate,
isDateTimeInput: !dateOnly,
userTimezone: timeZone,
})
? parseToString(draftValueAsDate)
: '',
);

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);
};
Expand All @@ -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;
Expand All @@ -293,13 +261,7 @@ export const FormDateTimeFieldInput = ({

setPickerDate(validatedDate);

setInputDateTime(
parseDateToString({
date: validatedDate,
isDateTimeInput: !dateOnly,
userTimezone: timeZone,
}),
);
setInputDateTime(parseToString(validatedDate));

persistDate(validatedDate);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -38,7 +38,6 @@ type AbsoluteDatePickerHeaderProps = {
prevMonthButtonDisabled: boolean;
nextMonthButtonDisabled: boolean;
isDateTimeInput?: boolean;
timeZone: string;
hideInput?: boolean;
};

Expand All @@ -52,7 +51,6 @@ export const AbsoluteDatePickerHeader = ({
prevMonthButtonDisabled,
nextMonthButtonDisabled,
isDateTimeInput,
timeZone,
hideInput = false,
}: AbsoluteDatePickerHeaderProps) => {
const endOfDayDateTimeInLocalTimezone = DateTime.now().set({
Expand All @@ -74,7 +72,6 @@ export const AbsoluteDatePickerHeader = ({
date={date}
isDateTimeInput={isDateTimeInput}
onChange={onChange}
userTimezone={timeZone}
/>
)}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
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 { DateFormat } from '@/localization/constants/DateFormat';
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 { getDateMask } 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 { getDateTimeMask } 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 { isNull } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { useDateParser } from '../../hooks/useDateParser';

const StyledInputContainer = styled.div`
align-items: center;
Expand Down Expand Up @@ -44,42 +46,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(
Expand All @@ -89,18 +79,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);
},
Expand All @@ -115,23 +101,28 @@ export const DateTimeInput = ({
return;
}

setValue(
parseDateToString({
date: date,
isDateTimeInput: isDateTimeInput === true,
userTimezone,
}),
);
}, [date, setValue, isDateTimeInput, userTimezone]);
setValue(parseToString(date));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [date, setValue]);

const getPlaceholder = () => {
const formatMap: Record<DateFormat, string> = {
[DateFormat.SYSTEM]: 'mm/dd/yyyy',
[DateFormat.MONTH_FIRST]: 'mm/dd/yyyy',
[DateFormat.DAY_FIRST]: 'dd/mm/yyyy',
[DateFormat.YEAR_FIRST]: 'yyyy-mm-dd',
};

const dateFormatStr = formatMap[dateFormat];
return `Type date${isDateTimeInput ? ' and time' : ` (${dateFormatStr})`}`;
};

return (
<StyledInputContainer>
<StyledInput
type="text"
ref={ref as any}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Type assertion to 'any' could be replaced with proper typing of the ref

placeholder={`Type date${
isDateTimeInput ? ' and time' : ' (mm/dd/yyyy)'
}`}
placeholder={getPlaceholder()}
value={value}
onChange={() => {}} // Prevent React warning
hasError={hasError}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,6 @@ export const DateTimePicker = ({
date={internalDate}
isDateTimeInput={isDateTimeInput}
onChange={onChange}
userTimezone={timeZone}
/>
}
renderCustomHeader={({
Expand All @@ -489,7 +488,6 @@ export const DateTimePicker = ({
prevMonthButtonDisabled={prevMonthButtonDisabled}
nextMonthButtonDisabled={nextMonthButtonDisabled}
isDateTimeInput={isDateTimeInput}
timeZone={timeZone}
hideInput={hideHeaderInput}
/>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,13 @@
export const DATE_MASK = 'm`/d`/Y`'; // See https://imask.js.org/guide.html#masked-date
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`';
}
};

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import { TIME_MASK } from '@/ui/input/components/internal/date/constants/TimeMask';

export const DATE_TIME_MASK = `m\`/d\`/Y\` ${TIME_MASK}`;
import { DateFormat } from '@/localization/constants/DateFormat';
import { getDateMask } from './DateMask';

export const getDateTimeMask = (dateFormat: DateFormat): string => {
return `${getDateMask(dateFormat)} ${TIME_MASK}`;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider keeping this file but deprecating the constant with a warning message directing users to getDateFormatString for smoother migration

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
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;
if (!date) {
return '';
}

const parsingFormat = getDateFormatString(dateFormat, isDateTimeInput);

const dateParsed = DateTime.fromJSDate(date, { zone: userTimezone });

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Comment on lines 8 to +9
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: userTimezone is marked as optional but the code doesn't handle undefined case, which could cause runtime errors

Suggested change
userTimezone: string | undefined;
dateFormat: DateFormat;
userTimezone: string;
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 })
Expand Down
Loading