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:
@ -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,
|
||||
});
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user