f613886511
## Summary Fixes #19634 ### Root Cause The ECMAScript spec treats date-only strings (`YYYY-MM-DD`) as **UTC midnight** when passed to `new Date()`. But `date-fns` comparison functions (`isToday`, `isYesterday`, `isTomorrow`) operate in **local time**. For users in UTC-negative timezones, UTC midnight April 14 is April 13 evening locally — so the label shows "Yesterday" instead of "Today". ### Fix In `formatDateISOStringToRelativeDate.ts`, detect date-only strings (length === 10) and append `T00:00:00` (no `Z`) to force local-time parsing: ```ts // Before const targetDate = new Date(isoDate); // After const targetDate = isoDate.length === 10 ? new Date(isoDate + 'T00:00:00') : new Date(isoDate); ``` Full datetime strings (with time component) are left unchanged — they already carry timezone information. ### Tests Added `formatDateISOStringToRelativeDate.test.ts` covering: - `Today` / `Yesterday` / `Tomorrow` labels for date-only strings - Regression case: date-only string parsed at local midnight (not UTC midnight) - Full datetime strings continue to work as before ## Before / After | Scenario | Before | After | |---|---|---| | `"2026-04-14"` viewed at UTC-5 on April 14 | Yesterday ❌ | Today ✓ | | `"2026-04-14"` viewed at UTC+0 on April 14 | Today ✓ | Today ✓ | | `"2026-04-14T12:00:00Z"` | Today ✓ | Today ✓ | --------- Co-authored-by: Marie Stoppa <marie@twenty.com>
63 lines
1.8 KiB
TypeScript
63 lines
1.8 KiB
TypeScript
import { type DateFormat } from '@/localization/constants/DateFormat';
|
|
import { type TimeFormat } from '@/localization/constants/TimeFormat';
|
|
import { formatDateISOStringToCustomUnicodeFormat } from '@/localization/utils/formatDateISOStringToCustomUnicodeFormat';
|
|
import { formatDateISOStringToDateTime } from '@/localization/utils/formatDateISOStringToDateTime';
|
|
import { formatDateISOStringToRelativeDate } from '@/localization/utils/formatDateISOStringToRelativeDate';
|
|
import {
|
|
FieldDateDisplayFormat,
|
|
type FieldDateMetadataSettings,
|
|
} from '@/object-record/record-field/ui/types/FieldMetadata';
|
|
import { type Locale } from 'date-fns';
|
|
|
|
export const formatDateTimeString = ({
|
|
value,
|
|
timeZone,
|
|
dateFormat,
|
|
timeFormat,
|
|
dateFieldSettings,
|
|
localeCatalog,
|
|
}: {
|
|
timeZone: string;
|
|
dateFormat: DateFormat;
|
|
timeFormat: TimeFormat;
|
|
value?: string | null;
|
|
dateFieldSettings?: FieldDateMetadataSettings;
|
|
localeCatalog: Locale;
|
|
}) => {
|
|
if (!value) {
|
|
return '';
|
|
}
|
|
|
|
switch (dateFieldSettings?.displayFormat) {
|
|
case FieldDateDisplayFormat.RELATIVE:
|
|
return formatDateISOStringToRelativeDate({
|
|
isoDate: value,
|
|
localeCatalog,
|
|
timeZone,
|
|
});
|
|
case FieldDateDisplayFormat.USER_SETTINGS:
|
|
return formatDateISOStringToDateTime({
|
|
date: value,
|
|
timeZone,
|
|
dateFormat,
|
|
timeFormat,
|
|
localeCatalog,
|
|
});
|
|
case FieldDateDisplayFormat.CUSTOM:
|
|
return formatDateISOStringToCustomUnicodeFormat({
|
|
date: value,
|
|
timeZone,
|
|
dateFormat: dateFieldSettings.customUnicodeDateFormat,
|
|
localeCatalog,
|
|
});
|
|
default:
|
|
return formatDateISOStringToDateTime({
|
|
date: value,
|
|
timeZone,
|
|
dateFormat,
|
|
timeFormat,
|
|
localeCatalog,
|
|
});
|
|
}
|
|
};
|