Fix timezone display + translate dates (#12147)

Fixes https://github.com/twentyhq/twenty/issues/11566 + translates dates

- Date display bug: We had an issue with date (not date time) display
depending on the timezone the user had selected. The date is stored in
the db as yyyy-mm-dd, eg. 2025-05-01 for **May 1st, 2025**. When
returned this date is formatted in a UTC DateTime at midnight, so
2025-05-1 00:00:00. Then when displaying the date we were converting
this date using the timeZone, so 2025-04-30 17:00:00, thus displaying
**April 30th, 2025**. The fix chosen is that we should not take into
account the timezone for date (not date time!) displays as we always
want to show the same date.
- Date translation: dates were not translated, not in their default
display (_May 1st, 2025_) nor in their relative display (_about a month
ago_). The lib we use for date formatting, date-fns, offers a
translation option with pre-built `Locale`s from their lib.
Unfortunately and surprisingly we cannot just use directly string locale
codes (like `fr-FR`), cf [open issue on
date-fns](https://github.com/date-fns/date-fns/issues/3660). A util was
introduced to offset this by dynamically importing the right date-fns
Locale based on the locale code.
This commit is contained in:
Marie
2025-05-22 14:04:44 +02:00
committed by GitHub
parent 0ac4cc6899
commit b5544b6853
25 changed files with 352 additions and 68 deletions

View File

@ -6,6 +6,7 @@ import { AggregateRecordsData } from '@/object-record/hooks/useAggregateRecords'
import { computeAggregateValueAndLabel } from '@/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel';
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { DATE_AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/DateAggregateOperations';
import { enUS } from 'date-fns/locale';
import { FieldMetadataType } from '~/generated/graphql';
const MOCK_FIELD_ID = '7d2d7b5e-7b3e-4b4a-8b0a-7b3e4b4a8b0a';
@ -35,6 +36,7 @@ describe('computeAggregateValueAndLabel', () => {
objectMetadataItem: mockObjectMetadata,
fieldMetadataId: MOCK_FIELD_ID,
aggregateOperation: AGGREGATE_OPERATIONS.sum,
localeCatalog: enUS,
...defaultParams,
});
@ -53,6 +55,7 @@ describe('computeAggregateValueAndLabel', () => {
objectMetadataItem: mockObjectMetadata,
fieldMetadataId: MOCK_FIELD_ID,
aggregateOperation: AGGREGATE_OPERATIONS.sum,
localeCatalog: enUS,
...defaultParams,
});
@ -90,6 +93,7 @@ describe('computeAggregateValueAndLabel', () => {
objectMetadataItem: mockObjectMetadataWithPercentageField,
fieldMetadataId: MOCK_FIELD_ID,
aggregateOperation: AGGREGATE_OPERATIONS.avg,
localeCatalog: enUS,
...defaultParams,
});
@ -127,6 +131,7 @@ describe('computeAggregateValueAndLabel', () => {
objectMetadataItem: mockObjectMetadataWithDecimalsField,
fieldMetadataId: MOCK_FIELD_ID,
aggregateOperation: AGGREGATE_OPERATIONS.sum,
localeCatalog: enUS,
...defaultParams,
});
@ -161,6 +166,7 @@ describe('computeAggregateValueAndLabel', () => {
objectMetadataItem: mockObjectMetadataWithDatetimeField,
fieldMetadataId: MOCK_FIELD_ID,
aggregateOperation: DATE_AGGREGATE_OPERATIONS.earliest,
localeCatalog: enUS,
...defaultParams,
});
@ -195,6 +201,7 @@ describe('computeAggregateValueAndLabel', () => {
objectMetadataItem: mockObjectMetadataWithDatetimeField,
fieldMetadataId: MOCK_FIELD_ID,
aggregateOperation: DATE_AGGREGATE_OPERATIONS.latest,
localeCatalog: enUS,
...defaultParams,
});
@ -215,6 +222,7 @@ describe('computeAggregateValueAndLabel', () => {
const result = computeAggregateValueAndLabel({
data: mockData,
objectMetadataItem: mockObjectMetadata,
localeCatalog: enUS,
...defaultParams,
});
@ -237,6 +245,7 @@ describe('computeAggregateValueAndLabel', () => {
objectMetadataItem: mockObjectMetadata,
fieldMetadataId: MOCK_FIELD_ID,
aggregateOperation: AGGREGATE_OPERATIONS.sum,
localeCatalog: enUS,
...defaultParams,
});

View File

@ -26,6 +26,7 @@ export const computeAggregateValueAndLabel = ({
dateFormat,
timeFormat,
timeZone,
localeCatalog,
}: {
data: AggregateRecordsData;
objectMetadataItem: ObjectMetadataItem;
@ -34,6 +35,7 @@ export const computeAggregateValueAndLabel = ({
dateFormat: DateFormat;
timeFormat: TimeFormat;
timeZone: string;
localeCatalog: Locale;
}) => {
if (isEmpty(data)) {
return {};
@ -105,6 +107,7 @@ export const computeAggregateValueAndLabel = ({
dateFormat,
timeFormat,
dateFieldSettings,
localeCatalog,
});
break;
}
@ -116,6 +119,7 @@ export const computeAggregateValueAndLabel = ({
timeZone,
dateFormat,
dateFieldSettings,
localeCatalog,
});
break;
}

View File

@ -4,10 +4,10 @@ import { DateFormat } from '@/localization/constants/DateFormat';
import { TimeFormat } from '@/localization/constants/TimeFormat';
import { DateFieldDisplay } from '@/object-record/record-field/meta-types/display/components/DateFieldDisplay';
import { UserContext } from '@/users/contexts/UserContext';
import { ComponentDecorator } from 'twenty-ui/testing';
import { getFieldDecorator } from '~/testing/decorators/getFieldDecorator';
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
import { ComponentDecorator } from 'twenty-ui/testing';
const meta: Meta = {
title: 'UI/Data/Field/Display/DateFieldDisplay',

View File

@ -4,10 +4,10 @@ import { DateFormat } from '@/localization/constants/DateFormat';
import { TimeFormat } from '@/localization/constants/TimeFormat';
import { DateTimeFieldDisplay } from '@/object-record/record-field/meta-types/display/components/DateTimeFieldDisplay';
import { UserContext } from '@/users/contexts/UserContext';
import { ComponentDecorator } from 'twenty-ui/testing';
import { getFieldDecorator } from '~/testing/decorators/getFieldDecorator';
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
import { ComponentDecorator } from 'twenty-ui/testing';
const meta: Meta = {
title: 'UI/Data/Field/Display/DateTimeFieldDisplay',

View File

@ -1,6 +1,7 @@
/* eslint-disable @nx/workspace-no-navigate-prefer-link */
import { RecordTableEmptyStateDisplay } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay';
import { SettingsPath } from '@/types/SettingsPath';
import { t } from '@lingui/core/macro';
import { IconSettings } from 'twenty-ui/display';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
@ -13,9 +14,9 @@ export const RecordTableEmptyStateRemote = () => {
return (
<RecordTableEmptyStateDisplay
buttonTitle={'Go to Settings'}
subTitle={'If this is unexpected, please verify your settings.'}
title={'No Data Available for Remote Table'}
buttonTitle={t`Go to Settings`}
subTitle={t`If this is unexpected, please verify your settings.`}
title={t`No Data Available for Remote Table`}
ButtonIcon={IconSettings}
animatedPlaceholderType="noRecord"
onClick={handleButtonClick}

View File

@ -12,6 +12,7 @@ import { UserContext } from '@/users/contexts/UserContext';
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { dateLocaleState } from '~/localization/states/dateLocaleState';
type UseAggregateRecordsProps = {
objectMetadataItem: ObjectMetadataItem;
@ -35,6 +36,8 @@ export const useAggregateRecordsForHeader = ({
recordIndexKanbanAggregateOperationState,
);
const dateLocale = useRecoilValue(dateLocaleState);
const { filterValueDependencies } = useFilterValueDependencies();
const { dateFormat, timeFormat, timeZone } = useContext(UserContext);
@ -65,6 +68,7 @@ export const useAggregateRecordsForHeader = ({
dateFormat,
timeFormat,
timeZone,
localeCatalog: dateLocale.localeCatalog,
});
return {

View File

@ -16,6 +16,7 @@ import { UserContext } from '@/users/contexts/UserContext';
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils';
import { dateLocaleState } from '~/localization/states/dateLocaleState';
export const useAggregateRecordsForRecordTableColumnFooter = (
fieldMetadataId: string,
@ -31,6 +32,8 @@ export const useAggregateRecordsForRecordTableColumnFooter = (
currentRecordFiltersComponentState,
);
const dateLocale = useRecoilValue(dateLocaleState);
const { filterValueDependencies } = useFilterValueDependencies();
const requestFilters = computeRecordGqlOperationFilter({
@ -99,6 +102,7 @@ export const useAggregateRecordsForRecordTableColumnFooter = (
dateFormat,
timeFormat,
timeZone,
localeCatalog: dateLocale.localeCatalog,
});
return {