Date field format display (#11384)

## Introduction

This PR enables functionality discussed in [Layout Date
Formatting](https://github.com/twentyhq/core-team-issues/issues/97).

### TLDR;
It enables greater control of date formatting at the object's field
level by upgrading all DATE and DATE_TIME fields' settings from:

```ts
{
    displayAsRelativeDate: boolean
}
```

to:

```ts

type FieldDateDisplayFormat = 'full_date' | 'relative_date' | 'date' | 'time' | 'year' | 'custom'

{
    displayFormat: FieldDateDisplayFormat
}
```

PR also includes an upgrade command that will update any existing DATE
and DATE_TIME fields to the new settings value

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
oliver
2025-04-18 01:00:02 -06:00
committed by GitHub
parent dd0ea2366f
commit 53042cc9dc
33 changed files with 690 additions and 250 deletions

View File

@ -1,4 +1,5 @@
import { DateFormat } from '@/localization/constants/DateFormat';
import { FieldDateDisplayFormat } from '@/object-record/record-field/types/FieldMetadata';
import { DateTime } from 'luxon';
import { formatDateString } from '~/utils/string/formatDateString';
@ -26,7 +27,7 @@ describe('formatDateString', () => {
expect(result).toBe('');
});
it('should format date as relative when displayAsRelativeDate is true', () => {
it('should format date as relative when displayFormat is set to RELATIVE', () => {
const mockDate = DateTime.now().minus({ months: 2 }).toISO();
const mockRelativeDate = 'about 2 months ago';
@ -39,13 +40,15 @@ describe('formatDateString', () => {
const result = formatDateString({
...defaultParams,
value: mockDate,
displayAsRelativeDate: true,
dateFieldSettings: {
displayFormat: FieldDateDisplayFormat.RELATIVE,
},
});
expect(result).toBe(mockRelativeDate);
});
it('should format date as datetime when displayAsRelativeDate is false', () => {
it('should format date as datetime when displayFormat is set to USER_SETTINGS', () => {
const mockDate = '2023-01-01T12:00:00Z';
const mockFormattedDate = '1 Jan, 2023';
@ -58,13 +61,40 @@ describe('formatDateString', () => {
const result = formatDateString({
...defaultParams,
value: mockDate,
displayAsRelativeDate: false,
dateFieldSettings: {
displayFormat: FieldDateDisplayFormat.USER_SETTINGS,
},
});
expect(result).toBe(mockFormattedDate);
});
it('should format date as datetime by default when displayAsRelativeDate is not provided', () => {
it('should format date with custom format when displayFormat is set to CUSTOM', () => {
const mockDate = '2023-01-01T12:00:00Z';
const mockFormattedDate = '2023';
jest.mock(
'@/localization/utils/formatDateISOStringToCustomUnicodeFormat',
() => ({
formatDateISOStringToCustomUnicodeFormat: jest
.fn()
.mockReturnValue(mockFormattedDate),
}),
);
const result = formatDateString({
...defaultParams,
value: mockDate,
dateFieldSettings: {
displayFormat: FieldDateDisplayFormat.CUSTOM,
customUnicodeDateFormat: 'yyyy',
},
});
expect(result).toBe(mockFormattedDate);
});
it('should format date as datetime by default when displayFormat is not provided', () => {
const mockDate = '2023-01-01T12:00:00Z';
const mockFormattedDate = '1 Jan, 2023';

View File

@ -1,5 +1,6 @@
import { DateFormat } from '@/localization/constants/DateFormat';
import { TimeFormat } from '@/localization/constants/TimeFormat';
import { FieldDateDisplayFormat } from '@/object-record/record-field/types/FieldMetadata';
import { DateTime } from 'luxon';
import { formatDateTimeString } from '~/utils/string/formatDateTimeString';
@ -28,7 +29,7 @@ describe('formatDateTimeString', () => {
expect(result).toBe('');
});
it('should format date as relative when displayAsRelativeDate is true', () => {
it('should format date as relative when displayFormat is RELATIVE', () => {
const mockDate = DateTime.now().minus({ months: 2 }).toISO();
const mockRelativeDate = 'about 2 months ago';
@ -41,13 +42,15 @@ describe('formatDateTimeString', () => {
const result = formatDateTimeString({
...defaultParams,
value: mockDate,
displayAsRelativeDate: true,
dateFieldSettings: {
displayFormat: FieldDateDisplayFormat.RELATIVE,
},
});
expect(result).toBe(mockRelativeDate);
});
it('should format date as datetime when displayAsRelativeDate is false', () => {
it('should format date as datetime when displayFormat is USER_SETTINGS', () => {
const mockDate = '2023-01-01T12:00:00Z';
const mockFormattedDate = '1 Jan, 2023 12:00';
@ -60,13 +63,40 @@ describe('formatDateTimeString', () => {
const result = formatDateTimeString({
...defaultParams,
value: mockDate,
displayAsRelativeDate: false,
dateFieldSettings: {
displayFormat: FieldDateDisplayFormat.USER_SETTINGS,
},
});
expect(result).toBe(mockFormattedDate);
});
it('should format date as datetime by default when displayAsRelativeDate is not provided', () => {
it('should format date as datetime when displayFormat is set to CUSTOM', () => {
const mockDate = '2023-01-01T12:00:00Z';
const mockFormattedDate = '2023';
jest.mock(
'@/localization/utils/formatDateISOStringToCustomUnicodeFormat',
() => ({
formatDateISOStringToCustomUnicodeFormat: jest
.fn()
.mockReturnValue(mockFormattedDate),
}),
);
const result = formatDateTimeString({
...defaultParams,
value: mockDate,
dateFieldSettings: {
displayFormat: FieldDateDisplayFormat.CUSTOM,
customUnicodeDateFormat: 'yyyy',
},
});
expect(result).toBe(mockFormattedDate);
});
it('should format date as datetime by default when displayFormat is not provided', () => {
const mockDate = '2023-01-01T12:00:00Z';
const mockFormattedDate = '1 Jan, 2023 12:00';

View File

@ -1,23 +1,40 @@
import { DateFormat } from '@/localization/constants/DateFormat';
import { formatDateISOStringToCustomUnicodeFormat } from '@/localization/utils/formatDateISOStringToCustomUnicodeFormat';
import { formatDateISOStringToDate } from '@/localization/utils/formatDateISOStringToDate';
import { formatDateISOStringToRelativeDate } from '@/localization/utils/formatDateISOStringToRelativeDate';
import {
FieldDateDisplayFormat,
FieldDateMetadataSettings,
} from '@/object-record/record-field/types/FieldMetadata';
import { isDefined } from 'twenty-shared/utils';
export const formatDateString = ({
value,
timeZone,
dateFormat,
displayAsRelativeDate,
dateFieldSettings,
}: {
timeZone: string;
dateFormat: DateFormat;
value?: string | null;
displayAsRelativeDate?: boolean;
}) => {
const formattedDate = value
? displayAsRelativeDate
? formatDateISOStringToRelativeDate(value)
: formatDateISOStringToDate(value, timeZone, dateFormat)
: '';
dateFieldSettings?: FieldDateMetadataSettings;
}): string => {
if (!isDefined(value)) {
return '';
}
return formattedDate;
switch (dateFieldSettings?.displayFormat) {
case FieldDateDisplayFormat.RELATIVE:
return formatDateISOStringToRelativeDate(value);
case FieldDateDisplayFormat.USER_SETTINGS:
return formatDateISOStringToDate(value, timeZone, dateFormat);
case FieldDateDisplayFormat.CUSTOM:
return formatDateISOStringToCustomUnicodeFormat(
value,
timeZone,
dateFieldSettings.customUnicodeDateFormat,
);
default:
return formatDateISOStringToDate(value, timeZone, dateFormat);
}
};

View File

@ -1,26 +1,52 @@
import { DateFormat } from '@/localization/constants/DateFormat';
import { 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,
FieldDateMetadataSettings,
} from '@/object-record/record-field/types/FieldMetadata';
export const formatDateTimeString = ({
value,
timeZone,
dateFormat,
timeFormat,
displayAsRelativeDate,
dateFieldSettings,
}: {
timeZone: string;
dateFormat: DateFormat;
timeFormat: TimeFormat;
value?: string | null;
displayAsRelativeDate?: boolean;
dateFieldSettings?: FieldDateMetadataSettings;
}) => {
const formattedDate = value
? displayAsRelativeDate
? formatDateISOStringToRelativeDate(value)
: formatDateISOStringToDateTime(value, timeZone, dateFormat, timeFormat)
: '';
if (!value) {
return '';
}
return formattedDate;
switch (dateFieldSettings?.displayFormat) {
case FieldDateDisplayFormat.RELATIVE:
return formatDateISOStringToRelativeDate(value);
case FieldDateDisplayFormat.USER_SETTINGS:
return formatDateISOStringToDateTime(
value,
timeZone,
dateFormat,
timeFormat,
);
case FieldDateDisplayFormat.CUSTOM:
return formatDateISOStringToCustomUnicodeFormat(
value,
timeZone,
dateFieldSettings.customUnicodeDateFormat,
);
default:
return formatDateISOStringToDateTime(
value,
timeZone,
dateFormat,
timeFormat,
);
}
};