Skip to content

Commit 59f7582

Browse files
Fix: time format issue in datepicker mask (#16922)
Fixes: #16872 ## Summary Fixes the DateTimePicker to respect user's 12H/24H time format preference. Previously, the time display at the top of the DateTimePicker always showed 24-hour format (e.g., "14:30") regardless of the user's time format setting. ## Screenshots _After:_ Time respects user preference, showing 12H with AM/PM (e.g., "03:28 PM") or 24H format <img width="357" height="491" alt="image" src="https://github.com/user-attachments/assets/4a03f195-a52b-4f74-84ba-c5eb2fc6c8c1" /> ## Implementation Details ### Changes Made: 1. **TimeMask.ts** - Added `getTimeMask()` to return appropriate mask pattern based on time format 2. **TimeBlocks.ts** - Restored as constant file per linting requirements 3. **getTimeBlocks.ts** (new) - Dynamic block generator supporting 12H (1-12 + AM/PM) and 24H (0-23) 4. **DateTimeBlocks.ts** - Simplified to static constant 5. **getDateTimeMask.ts** - Updated to accept and use `timeFormat` parameter 6. **DateTimePickerInput.tsx** - Dynamically generates mask and blocks based on user's time format preference, increased input width to accommodate AM/PM 7. **useParseJSDateToIMaskDateTimeInputString.ts** - Formats dates with correct time pattern 8. **useParseDateTimeInputStringToJSDate.ts** - Parses dates with correct time pattern 9. **date-utils.ts** - Added `getTimePattern()` helper (returns 'hh:mm a' for 12H, 'HH:mm' for 24H) 10. **parseDateTimeToString.ts** - Added optional `timeFormat` parameter for consistency ### Technical Approach: - Leverages existing `useDateTimeFormat()` hook to get user's `timeFormat` preference - Supports `TimeFormat.SYSTEM`, `TimeFormat.HOUR_12`, and `TimeFormat.HOUR_24` - Uses IMask blocks with appropriate ranges: 1-12 for 12H, 0-23 for 24H - Adds 'aa' (AM/PM) block for 12-hour format with enum validation --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
1 parent 9672ca0 commit 59f7582

File tree

10 files changed

+102
-24
lines changed

10 files changed

+102
-24
lines changed
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { useRecoilValue } from 'recoil';
22

33
import { workspaceMemberFormatPreferencesState } from '@/localization/states/workspaceMemberFormatPreferencesState';
4+
import { resolveDateFormat } from '@/localization/utils/resolveDateFormat';
5+
import { resolveTimeFormat } from '@/localization/utils/resolveTimeFormat';
46

57
export const useDateTimeFormat = () => {
68
const workspaceMemberFormatPreferences = useRecoilValue(
@@ -9,8 +11,8 @@ export const useDateTimeFormat = () => {
911

1012
return {
1113
timeZone: workspaceMemberFormatPreferences.timeZone,
12-
dateFormat: workspaceMemberFormatPreferences.dateFormat,
13-
timeFormat: workspaceMemberFormatPreferences.timeFormat,
14+
dateFormat: resolveDateFormat(workspaceMemberFormatPreferences.dateFormat),
15+
timeFormat: resolveTimeFormat(workspaceMemberFormatPreferences.timeFormat),
1416
calendarStartDay: workspaceMemberFormatPreferences.calendarStartDay,
1517
};
1618
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { DateFormat } from '@/localization/constants/DateFormat';
2+
import { detectDateFormat } from '@/localization/utils/detection/detectDateFormat';
3+
4+
export const resolveDateFormat = (dateFormat: DateFormat): DateFormat => {
5+
if (dateFormat === DateFormat.SYSTEM) {
6+
const detectedFormat = detectDateFormat();
7+
return DateFormat[detectedFormat];
8+
}
9+
10+
return dateFormat;
11+
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { TimeFormat } from '@/localization/constants/TimeFormat';
2+
import { detectTimeFormat } from '@/localization/utils/detection/detectTimeFormat';
3+
4+
export const resolveTimeFormat = (timeFormat: TimeFormat): TimeFormat => {
5+
if (timeFormat === TimeFormat.SYSTEM) {
6+
const detectedFormat = detectTimeFormat();
7+
return TimeFormat[detectedFormat];
8+
}
9+
10+
return timeFormat;
11+
};

packages/twenty-front/src/modules/ui/input/components/internal/date/components/DateTimePickerInput.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ import styled from '@emotion/styled';
22
import { useIMask } from 'react-imask';
33

44
import { useDateTimeFormat } from '@/localization/hooks/useDateTimeFormat';
5-
import { DATE_TIME_BLOCKS } from '@/ui/input/components/internal/date/constants/DateTimeBlocks';
5+
import { DATE_BLOCKS } from '@/ui/input/components/internal/date/constants/DateBlocks';
66
import { MAX_DATE } from '@/ui/input/components/internal/date/constants/MaxDate';
77
import { MIN_DATE } from '@/ui/input/components/internal/date/constants/MinDate';
88
import { getDateTimeMask } from '@/ui/input/components/internal/date/utils/getDateTimeMask';
9+
import { getTimeBlocks } from '@/ui/input/components/internal/date/utils/getTimeBlocks';
910

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

4243
type DateTimePickerInputProps = {
@@ -58,7 +59,7 @@ export const DateTimePickerInput = ({
5859

5960
const { userTimezone } = useUserTimezone();
6061

61-
const { dateFormat } = useDateTimeFormat();
62+
const { dateFormat, timeFormat } = useDateTimeFormat();
6263

6364
const { getShiftedDateToSystemTimeZone } =
6465
useGetShiftedDateToSystemTimeZone();
@@ -77,9 +78,9 @@ export const DateTimePickerInput = ({
7778
return date;
7879
};
7980

80-
const pattern = getDateTimeMask(dateFormat);
81+
const pattern = getDateTimeMask({ dateFormat, timeFormat });
8182

82-
const blocks = DATE_TIME_BLOCKS;
83+
const blocks = { ...DATE_BLOCKS, ...getTimeBlocks(timeFormat) };
8384

8485
const defaultValueForIMask = isDefined(internalDate)
8586
? new Date(internalDate?.toInstant().toString())

packages/twenty-front/src/modules/ui/input/components/internal/date/hooks/useParseDateTimeInputStringToJSDate.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import { isValid, parse } from 'date-fns';
33
import { getDateTimeFormatStringFoDatePickerInputMask } from '~/utils/date-utils';
44

55
export const useParseDateTimeInputStringToJSDate = () => {
6-
const { dateFormat } = useDateTimeFormat();
6+
const { dateFormat, timeFormat } = useDateTimeFormat();
77

88
const parseDateTimeInputStringToJSDate = (dateAsString: string) => {
9-
const parsingFormat =
10-
getDateTimeFormatStringFoDatePickerInputMask(dateFormat);
9+
const parsingFormat = getDateTimeFormatStringFoDatePickerInputMask({
10+
dateFormat,
11+
timeFormat,
12+
});
1113
const referenceDate = new Date();
1214

1315
const parsedDate = parse(dateAsString, parsingFormat, referenceDate);

packages/twenty-front/src/modules/ui/input/components/internal/date/hooks/useParseJSDateToIMaskDateTimeInputString.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
import { useDateTimeFormat } from '@/localization/hooks/useDateTimeFormat';
2-
import { format } from 'date-fns';
2+
import { format, isValid } from 'date-fns';
33
import { getDateTimeFormatStringFoDatePickerInputMask } from '~/utils/date-utils';
44

55
export const useParseJSDateToIMaskDateTimeInputString = () => {
6-
const { dateFormat } = useDateTimeFormat();
6+
const { dateFormat, timeFormat } = useDateTimeFormat();
77

88
const parseJSDateToDateTimeInputString = (date: Date) => {
9-
const parsingFormat =
10-
getDateTimeFormatStringFoDatePickerInputMask(dateFormat);
9+
if (!date || !isValid(date)) {
10+
return '';
11+
}
12+
13+
const parsingFormat = getDateTimeFormatStringFoDatePickerInputMask({
14+
dateFormat,
15+
timeFormat,
16+
});
1117

1218
return format(date, parsingFormat);
1319
};
Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1-
import { TIME_MASK } from '@/ui/input/components/internal/date/constants/TimeMask';
2-
31
import { type DateFormat } from '@/localization/constants/DateFormat';
2+
import { type TimeFormat } from '@/localization/constants/TimeFormat';
43
import { getDateMask } from './getDateMask';
4+
import { getTimeMask } from './getTimeMask';
55

6-
export const getDateTimeMask = (dateFormat: DateFormat): string => {
7-
return `${getDateMask(dateFormat)} ${TIME_MASK}`;
6+
export const getDateTimeMask = ({
7+
dateFormat,
8+
timeFormat,
9+
}: {
10+
dateFormat: DateFormat;
11+
timeFormat: TimeFormat;
12+
}): string => {
13+
return `${getDateMask(dateFormat)} ${getTimeMask(timeFormat)}`;
814
};
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { TimeFormat } from '@/localization/constants/TimeFormat';
2+
import { IMask } from 'react-imask';
3+
4+
export const getTimeBlocks = (timeFormat: TimeFormat) => {
5+
const isHour12 = timeFormat === TimeFormat.HOUR_12;
6+
7+
return {
8+
HH: {
9+
mask: IMask.MaskedRange,
10+
from: isHour12 ? 1 : 0,
11+
to: isHour12 ? 12 : 23,
12+
maxLength: 2,
13+
},
14+
mm: {
15+
mask: IMask.MaskedRange,
16+
from: 0,
17+
to: 59,
18+
maxLength: 2,
19+
},
20+
aa: {
21+
mask: '**',
22+
},
23+
};
24+
};
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { TimeFormat } from '@/localization/constants/TimeFormat';
2+
3+
export const getTimeMask = (timeFormat: TimeFormat): string => {
4+
return timeFormat === TimeFormat.HOUR_12 ? 'HH`:mm` `aa' : 'HH`:mm`';
5+
};

packages/twenty-front/src/utils/date-utils.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from 'date-fns';
1414

1515
import { DateFormat } from '@/localization/constants/DateFormat';
16+
import { TimeFormat } from '@/localization/constants/TimeFormat';
1617
import { CustomError, isDefined } from 'twenty-shared/utils';
1718

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

185-
export const getDateTimeFormatStringFoDatePickerInputMask = (
186-
dateFormat: DateFormat,
187-
): string => {
186+
const getTimePattern = (timeFormat: TimeFormat) => {
187+
return timeFormat === TimeFormat.HOUR_12 ? 'hh:mm a' : 'HH:mm';
188+
};
189+
190+
export const getDateTimeFormatStringFoDatePickerInputMask = ({
191+
dateFormat,
192+
timeFormat,
193+
}: {
194+
dateFormat: DateFormat;
195+
timeFormat: TimeFormat;
196+
}): string => {
197+
const timePattern = getTimePattern(timeFormat);
188198
switch (dateFormat) {
189199
case DateFormat.DAY_FIRST:
190-
return `dd/MM/yyyy HH:mm`;
200+
return `dd/MM/yyyy ${timePattern}`;
191201
case DateFormat.YEAR_FIRST:
192-
return `yyyy-MM-dd HH:mm`;
202+
return `yyyy-MM-dd ${timePattern}`;
193203
case DateFormat.MONTH_FIRST:
194204
default:
195-
return `MM/dd/yyyy HH:mm`;
205+
return `MM/dd/yyyy ${timePattern}`;
196206
}
197207
};
198208

0 commit comments

Comments
 (0)