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>
This commit is contained in:
@@ -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(
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
+6
-5
@@ -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';
|
||||
@@ -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 = {
|
||||
@@ -58,7 +59,7 @@ export const DateTimePickerInput = ({
|
||||
|
||||
const { userTimezone } = useUserTimezone();
|
||||
|
||||
const { dateFormat } = useDateTimeFormat();
|
||||
const { dateFormat, timeFormat } = useDateTimeFormat();
|
||||
|
||||
const { getShiftedDateToSystemTimeZone } =
|
||||
useGetShiftedDateToSystemTimeZone();
|
||||
@@ -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())
|
||||
|
||||
+5
-3
@@ -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);
|
||||
|
||||
+10
-4
@@ -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)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const parsingFormat = getDateTimeFormatStringFoDatePickerInputMask({
|
||||
dateFormat,
|
||||
timeFormat,
|
||||
});
|
||||
|
||||
return format(date, parsingFormat);
|
||||
};
|
||||
|
||||
+10
-4
@@ -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)}`;
|
||||
};
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
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,
|
||||
},
|
||||
mm: {
|
||||
mask: IMask.MaskedRange,
|
||||
from: 0,
|
||||
to: 59,
|
||||
maxLength: 2,
|
||||
},
|
||||
aa: {
|
||||
mask: '**',
|
||||
},
|
||||
};
|
||||
};
|
||||
+5
@@ -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`';
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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}`;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user