Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
@@ -1,6 +1,8 @@
import { useRecoilValue } from 'recoil';

import { workspaceMemberFormatPreferencesState } from '@/localization/states/workspaceMemberFormatPreferencesState';
import { resolveDateFormat } from '@/localization/utils/resolveDateFormat';
import { resolveTimeFormat } from '@/localization/utils/resolveTimeFormat';

export const useDateTimeFormat = () => {
const workspaceMemberFormatPreferences = useRecoilValue(
Expand All @@ -9,8 +11,8 @@ export const useDateTimeFormat = () => {

return {
timeZone: workspaceMemberFormatPreferences.timeZone,
dateFormat: workspaceMemberFormatPreferences.dateFormat,
timeFormat: workspaceMemberFormatPreferences.timeFormat,
dateFormat: resolveDateFormat(workspaceMemberFormatPreferences.dateFormat),
timeFormat: resolveTimeFormat(workspaceMemberFormatPreferences.timeFormat),
calendarStartDay: workspaceMemberFormatPreferences.calendarStartDay,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { DateFormat } from '@/localization/constants/DateFormat';
import { detectDateFormat } from '@/localization/utils/detection/detectDateFormat';

export const resolveDateFormat = (dateFormat: DateFormat): DateFormat => {
if (dateFormat === DateFormat.SYSTEM) {
const detectedFormat = detectDateFormat();
return DateFormat[detectedFormat];
}

return dateFormat;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { TimeFormat } from '@/localization/constants/TimeFormat';
import { detectTimeFormat } from '@/localization/utils/detection/detectTimeFormat';

export const resolveTimeFormat = (timeFormat: TimeFormat): TimeFormat => {
if (timeFormat === TimeFormat.SYSTEM) {
const detectedFormat = detectTimeFormat();
return TimeFormat[detectedFormat];
}

return timeFormat;
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import styled from '@emotion/styled';
import { useIMask } from 'react-imask';

import { useDateTimeFormat } from '@/localization/hooks/useDateTimeFormat';
import { DATE_TIME_BLOCKS } from '@/ui/input/components/internal/date/constants/DateTimeBlocks';
import { DATE_BLOCKS } from '@/ui/input/components/internal/date/constants/DateBlocks';
import { MAX_DATE } from '@/ui/input/components/internal/date/constants/MaxDate';
import { MIN_DATE } from '@/ui/input/components/internal/date/constants/MinDate';
import { getDateTimeMask } from '@/ui/input/components/internal/date/utils/getDateTimeMask';
import { getTimeBlocks } from '@/ui/input/components/internal/date/utils/getTimeBlocks';

import { TimeZoneAbbreviation } from '@/ui/input/components/internal/date/components/TimeZoneAbbreviation';
import { useGetShiftedDateToCustomTimeZone } from '@/ui/input/components/internal/date/hooks/useGetShiftedDateToCustomTimeZone';
Expand Down Expand Up @@ -36,7 +37,7 @@ const StyledInput = styled.input<{ hasError?: boolean }>`
padding-left: ${({ theme }) => theme.spacing(2)};
font-weight: 500;
font-size: ${({ theme }) => theme.font.size.md};
width: 105px;
width: 140px;
`;

type DateTimePickerInputProps = {
Expand All @@ -58,7 +59,7 @@ export const DateTimePickerInput = ({

const { userTimezone } = useUserTimezone();

const { dateFormat } = useDateTimeFormat();
const { dateFormat, timeFormat } = useDateTimeFormat();

const { getShiftedDateToSystemTimeZone } =
useGetShiftedDateToSystemTimeZone();
Expand All @@ -77,9 +78,9 @@ export const DateTimePickerInput = ({
return date;
};

const pattern = getDateTimeMask(dateFormat);
const pattern = getDateTimeMask({ dateFormat, timeFormat });

const blocks = DATE_TIME_BLOCKS;
const blocks = { ...DATE_BLOCKS, ...getTimeBlocks(timeFormat) };

const defaultValueForIMask = isDefined(internalDate)
? new Date(internalDate?.toInstant().toString())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { isValid, parse } from 'date-fns';
import { getDateTimeFormatStringFoDatePickerInputMask } from '~/utils/date-utils';

export const useParseDateTimeInputStringToJSDate = () => {
const { dateFormat } = useDateTimeFormat();
const { dateFormat, timeFormat } = useDateTimeFormat();

const parseDateTimeInputStringToJSDate = (dateAsString: string) => {
const parsingFormat =
getDateTimeFormatStringFoDatePickerInputMask(dateFormat);
const parsingFormat = getDateTimeFormatStringFoDatePickerInputMask({
dateFormat,
timeFormat,
});
const referenceDate = new Date();

const parsedDate = parse(dateAsString, parsingFormat, referenceDate);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { useDateTimeFormat } from '@/localization/hooks/useDateTimeFormat';
import { format } from 'date-fns';
import { format, isValid } from 'date-fns';
import { getDateTimeFormatStringFoDatePickerInputMask } from '~/utils/date-utils';

export const useParseJSDateToIMaskDateTimeInputString = () => {
const { dateFormat } = useDateTimeFormat();
const { dateFormat, timeFormat } = useDateTimeFormat();

const parseJSDateToDateTimeInputString = (date: Date) => {
const parsingFormat =
getDateTimeFormatStringFoDatePickerInputMask(dateFormat);
if (!date || !isValid(date)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I wouldn't bring validation with the Date object, as we're trying to avoid its usage. Prefer using Temporal.

return '';
}

const parsingFormat = getDateTimeFormatStringFoDatePickerInputMask({
dateFormat,
timeFormat,
});

return format(date, parsingFormat);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { TIME_MASK } from '@/ui/input/components/internal/date/constants/TimeMask';

import { type DateFormat } from '@/localization/constants/DateFormat';
import { type TimeFormat } from '@/localization/constants/TimeFormat';
import { getDateMask } from './getDateMask';
import { getTimeMask } from './getTimeMask';

export const getDateTimeMask = (dateFormat: DateFormat): string => {
return `${getDateMask(dateFormat)} ${TIME_MASK}`;
export const getDateTimeMask = ({
dateFormat,
timeFormat,
}: {
dateFormat: DateFormat;
timeFormat: TimeFormat;
}): string => {
return `${getDateMask(dateFormat)} ${getTimeMask(timeFormat)}`;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { TimeFormat } from '@/localization/constants/TimeFormat';
import { IMask } from 'react-imask';

export const getTimeBlocks = (timeFormat: TimeFormat) => {
const isHour12 = timeFormat === TimeFormat.HOUR_12;

return {
HH: {
mask: IMask.MaskedRange,
from: isHour12 ? 1 : 0,
to: isHour12 ? 12 : 23,
maxLength: 2,
},
Copy link

Choose a reason for hiding this comment

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

Bug: The time input's AM/PM validation is case-sensitive, expecting AM/PM. However, date-fns formats time with lowercase am/pm, causing a mismatch and validation failure.
Severity: MEDIUM

Suggested Fix

To fix the case-sensitivity issue, either update the IMask.MaskedEnum to include lowercase variants ['AM', 'PM', 'am', 'pm'], or add a prepare hook to the 'aa' block definition in getTimeBlocks.ts to convert the input value to uppercase before validation, like prepare: (str) => str.toUpperCase().

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location:
packages/twenty-front/src/modules/ui/input/components/internal/date/utils/getTimeBlocks.ts#L13

Potential issue: In 12-hour time format, the `IMask.MaskedEnum` for the AM/PM selector
is configured with `['AM', 'PM']`, making it case-sensitive. However, the `date-fns`
formatting pattern `'hh:mm a'` produces lowercase "am" and "pm". When a date is
programmatically set, the formatted value (e.g., "03:30 pm") will not match the mask's
enum, causing validation to fail. This also affects users who manually type "am" or "pm"
in lowercase. The component lacks a `prepare` hook or other normalization to handle case
differences.

Did we get this right? 👍 / 👎 to inform future reviews.

mm: {
mask: IMask.MaskedRange,
from: 0,
to: 59,
maxLength: 2,
},
...(isHour12 && {
aa: {
mask: IMask.MaskedEnum,
enum: ['AM', 'PM'],
},
}),
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { TimeFormat } from '@/localization/constants/TimeFormat';

export const getTimeMask = (timeFormat: TimeFormat): string => {
return timeFormat === TimeFormat.HOUR_12 ? 'HH`:`mm` `aa' : 'HH`:`mm';
};
22 changes: 16 additions & 6 deletions packages/twenty-front/src/utils/date-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from 'date-fns';

import { DateFormat } from '@/localization/constants/DateFormat';
import { TimeFormat } from '@/localization/constants/TimeFormat';
import { CustomError, isDefined } from 'twenty-shared/utils';

import { i18n } from '@lingui/core';
Expand Down Expand Up @@ -182,17 +183,26 @@ export const formatToHumanReadableDate = (date: Date | string) => {
return i18n.date(parsedJSDate, { dateStyle: 'medium' });
};

export const getDateTimeFormatStringFoDatePickerInputMask = (
dateFormat: DateFormat,
): string => {
const getTimePattern = (timeFormat: TimeFormat) => {
return timeFormat === TimeFormat.HOUR_12 ? 'hh:mm a' : 'HH:mm';
};

export const getDateTimeFormatStringFoDatePickerInputMask = ({
dateFormat,
timeFormat,
}: {
dateFormat: DateFormat;
timeFormat: TimeFormat;
}): string => {
const timePattern = getTimePattern(timeFormat);
switch (dateFormat) {
case DateFormat.DAY_FIRST:
return `dd/MM/yyyy HH:mm`;
return `dd/MM/yyyy ${timePattern}`;
case DateFormat.YEAR_FIRST:
return `yyyy-MM-dd HH:mm`;
return `yyyy-MM-dd ${timePattern}`;
case DateFormat.MONTH_FIRST:
default:
return `MM/dd/yyyy HH:mm`;
return `MM/dd/yyyy ${timePattern}`;
}
};

Expand Down