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:
@ -0,0 +1,17 @@
|
|||||||
|
import { Locale } from 'date-fns';
|
||||||
|
import { enUS } from 'date-fns/locale';
|
||||||
|
import { APP_LOCALES } from 'twenty-shared/translations';
|
||||||
|
import { createState } from 'twenty-ui/utilities';
|
||||||
|
|
||||||
|
type DateLocaleState = {
|
||||||
|
locale?: keyof typeof APP_LOCALES;
|
||||||
|
localeCatalog: Locale;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dateLocaleState = createState<DateLocaleState>({
|
||||||
|
key: 'dateLocaleState',
|
||||||
|
defaultValue: {
|
||||||
|
locale: undefined,
|
||||||
|
localeCatalog: enUS,
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import { formatDateISOStringToCustomUnicodeFormat } from '@/localization/utils/formatDateISOStringToCustomUnicodeFormat';
|
import { formatDateISOStringToCustomUnicodeFormat } from '@/localization/utils/formatDateISOStringToCustomUnicodeFormat';
|
||||||
import { formatInTimeZone } from 'date-fns-tz';
|
import { formatInTimeZone } from 'date-fns-tz';
|
||||||
|
import { enUS } from 'date-fns/locale';
|
||||||
|
|
||||||
jest.mock('date-fns-tz');
|
jest.mock('date-fns-tz');
|
||||||
|
|
||||||
@ -15,16 +16,18 @@ describe('formatDateISOStringToCustomUnicodeFormat', () => {
|
|||||||
it('should use provided timezone', () => {
|
it('should use provided timezone', () => {
|
||||||
formatInTimeZone.mockReturnValue('06:30');
|
formatInTimeZone.mockReturnValue('06:30');
|
||||||
|
|
||||||
const result = formatDateISOStringToCustomUnicodeFormat(
|
const result = formatDateISOStringToCustomUnicodeFormat({
|
||||||
mockDate,
|
date: mockDate,
|
||||||
mockTimeZone,
|
timeZone: mockTimeZone,
|
||||||
mockTimeFormat,
|
dateFormat: mockTimeFormat,
|
||||||
);
|
localeCatalog: enUS,
|
||||||
|
});
|
||||||
|
|
||||||
expect(formatInTimeZone).toHaveBeenCalledWith(
|
expect(formatInTimeZone).toHaveBeenCalledWith(
|
||||||
new Date(mockDate),
|
new Date(mockDate),
|
||||||
mockTimeZone,
|
mockTimeZone,
|
||||||
mockTimeFormat,
|
mockTimeFormat,
|
||||||
|
{ locale: enUS },
|
||||||
);
|
);
|
||||||
expect(result).toBe('06:30');
|
expect(result).toBe('06:30');
|
||||||
});
|
});
|
||||||
@ -34,16 +37,18 @@ describe('formatDateISOStringToCustomUnicodeFormat', () => {
|
|||||||
throw new Error();
|
throw new Error();
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = formatDateISOStringToCustomUnicodeFormat(
|
const result = formatDateISOStringToCustomUnicodeFormat({
|
||||||
mockDate,
|
date: mockDate,
|
||||||
mockTimeZone,
|
timeZone: mockTimeZone,
|
||||||
'f',
|
dateFormat: 'f',
|
||||||
);
|
localeCatalog: enUS,
|
||||||
|
});
|
||||||
|
|
||||||
expect(formatInTimeZone).toHaveBeenCalledWith(
|
expect(formatInTimeZone).toHaveBeenCalledWith(
|
||||||
new Date(mockDate),
|
new Date(mockDate),
|
||||||
mockTimeZone,
|
mockTimeZone,
|
||||||
'f',
|
'f',
|
||||||
|
{ locale: enUS },
|
||||||
);
|
);
|
||||||
expect(result).toBe('Invalid format string');
|
expect(result).toBe('Invalid format string');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,12 +1,20 @@
|
|||||||
import { formatInTimeZone } from 'date-fns-tz';
|
import { formatInTimeZone } from 'date-fns-tz';
|
||||||
|
|
||||||
export const formatDateISOStringToCustomUnicodeFormat = (
|
export const formatDateISOStringToCustomUnicodeFormat = ({
|
||||||
date: string,
|
date,
|
||||||
timeZone: string,
|
timeZone,
|
||||||
dateFormat: string,
|
dateFormat,
|
||||||
) => {
|
localeCatalog,
|
||||||
|
}: {
|
||||||
|
date: string;
|
||||||
|
timeZone: string;
|
||||||
|
dateFormat: string;
|
||||||
|
localeCatalog: Locale;
|
||||||
|
}) => {
|
||||||
try {
|
try {
|
||||||
return formatInTimeZone(new Date(date), timeZone, dateFormat);
|
return formatInTimeZone(new Date(date), timeZone, dateFormat, {
|
||||||
|
locale: localeCatalog,
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return 'Invalid format string';
|
return 'Invalid format string';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,18 @@
|
|||||||
import { DateFormat } from '@/localization/constants/DateFormat';
|
import { DateFormat } from '@/localization/constants/DateFormat';
|
||||||
import { formatInTimeZone } from 'date-fns-tz';
|
import { formatInTimeZone } from 'date-fns-tz';
|
||||||
|
|
||||||
export const formatDateISOStringToDate = (
|
export const formatDateISOStringToDate = ({
|
||||||
date: string,
|
date,
|
||||||
timeZone: string,
|
timeZone,
|
||||||
dateFormat: DateFormat,
|
dateFormat,
|
||||||
) => {
|
localeCatalog,
|
||||||
return formatInTimeZone(new Date(date), timeZone, dateFormat);
|
}: {
|
||||||
|
date: string;
|
||||||
|
timeZone: string;
|
||||||
|
dateFormat: DateFormat;
|
||||||
|
localeCatalog?: Locale;
|
||||||
|
}) => {
|
||||||
|
return formatInTimeZone(new Date(date), timeZone, dateFormat, {
|
||||||
|
locale: localeCatalog,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,15 +2,25 @@ import { DateFormat } from '@/localization/constants/DateFormat';
|
|||||||
import { TimeFormat } from '@/localization/constants/TimeFormat';
|
import { TimeFormat } from '@/localization/constants/TimeFormat';
|
||||||
import { formatInTimeZone } from 'date-fns-tz';
|
import { formatInTimeZone } from 'date-fns-tz';
|
||||||
|
|
||||||
export const formatDateISOStringToDateTime = (
|
export const formatDateISOStringToDateTime = ({
|
||||||
date: string,
|
date,
|
||||||
timeZone: string,
|
timeZone,
|
||||||
dateFormat: DateFormat,
|
dateFormat,
|
||||||
timeFormat: TimeFormat,
|
timeFormat,
|
||||||
) => {
|
localeCatalog,
|
||||||
|
}: {
|
||||||
|
date: string;
|
||||||
|
timeZone: string;
|
||||||
|
dateFormat: DateFormat;
|
||||||
|
timeFormat: TimeFormat;
|
||||||
|
localeCatalog: Locale;
|
||||||
|
}) => {
|
||||||
return formatInTimeZone(
|
return formatInTimeZone(
|
||||||
new Date(date),
|
new Date(date),
|
||||||
timeZone,
|
timeZone,
|
||||||
`${dateFormat} ${timeFormat}`,
|
`${dateFormat} ${timeFormat}`,
|
||||||
|
{
|
||||||
|
locale: localeCatalog,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,25 +1,40 @@
|
|||||||
|
import { t } from '@lingui/core/macro';
|
||||||
import {
|
import {
|
||||||
differenceInDays,
|
differenceInDays,
|
||||||
formatDistance,
|
formatDistance,
|
||||||
isToday,
|
isToday,
|
||||||
|
isTomorrow,
|
||||||
|
isYesterday,
|
||||||
|
Locale,
|
||||||
startOfDay,
|
startOfDay,
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
|
|
||||||
export const formatDateISOStringToRelativeDate = (
|
export const formatDateISOStringToRelativeDate = ({
|
||||||
isoDate: string,
|
isoDate,
|
||||||
isDayMaximumPrecision = false,
|
isDayMaximumPrecision = false,
|
||||||
) => {
|
localeCatalog,
|
||||||
|
}: {
|
||||||
|
isoDate: string;
|
||||||
|
isDayMaximumPrecision?: boolean;
|
||||||
|
localeCatalog: Locale;
|
||||||
|
}) => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const targetDate = new Date(isoDate);
|
const targetDate = new Date(isoDate);
|
||||||
|
|
||||||
if (isDayMaximumPrecision && isToday(targetDate)) return 'Today';
|
if (isDayMaximumPrecision && isToday(targetDate)) return t`Today`;
|
||||||
|
if (isDayMaximumPrecision && isYesterday(targetDate)) return t`Yesterday`;
|
||||||
|
if (isDayMaximumPrecision && isTomorrow(targetDate)) return t`Tomorrow`;
|
||||||
|
|
||||||
const isWithin24h = Math.abs(differenceInDays(targetDate, now)) < 1;
|
const isWithin24h = Math.abs(differenceInDays(targetDate, now)) < 1;
|
||||||
|
|
||||||
if (isDayMaximumPrecision || !isWithin24h)
|
if (isDayMaximumPrecision || !isWithin24h)
|
||||||
return formatDistance(startOfDay(targetDate), startOfDay(now), {
|
return formatDistance(startOfDay(targetDate), startOfDay(now), {
|
||||||
addSuffix: true,
|
addSuffix: true,
|
||||||
|
locale: localeCatalog,
|
||||||
});
|
});
|
||||||
|
|
||||||
return formatDistance(targetDate, now, { addSuffix: true });
|
return formatDistance(targetDate, now, {
|
||||||
|
addSuffix: true,
|
||||||
|
locale: localeCatalog,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { AggregateRecordsData } from '@/object-record/hooks/useAggregateRecords'
|
|||||||
import { computeAggregateValueAndLabel } from '@/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel';
|
import { computeAggregateValueAndLabel } from '@/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel';
|
||||||
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
||||||
import { DATE_AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/DateAggregateOperations';
|
import { DATE_AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/DateAggregateOperations';
|
||||||
|
import { enUS } from 'date-fns/locale';
|
||||||
import { FieldMetadataType } from '~/generated/graphql';
|
import { FieldMetadataType } from '~/generated/graphql';
|
||||||
|
|
||||||
const MOCK_FIELD_ID = '7d2d7b5e-7b3e-4b4a-8b0a-7b3e4b4a8b0a';
|
const MOCK_FIELD_ID = '7d2d7b5e-7b3e-4b4a-8b0a-7b3e4b4a8b0a';
|
||||||
@ -35,6 +36,7 @@ describe('computeAggregateValueAndLabel', () => {
|
|||||||
objectMetadataItem: mockObjectMetadata,
|
objectMetadataItem: mockObjectMetadata,
|
||||||
fieldMetadataId: MOCK_FIELD_ID,
|
fieldMetadataId: MOCK_FIELD_ID,
|
||||||
aggregateOperation: AGGREGATE_OPERATIONS.sum,
|
aggregateOperation: AGGREGATE_OPERATIONS.sum,
|
||||||
|
localeCatalog: enUS,
|
||||||
...defaultParams,
|
...defaultParams,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -53,6 +55,7 @@ describe('computeAggregateValueAndLabel', () => {
|
|||||||
objectMetadataItem: mockObjectMetadata,
|
objectMetadataItem: mockObjectMetadata,
|
||||||
fieldMetadataId: MOCK_FIELD_ID,
|
fieldMetadataId: MOCK_FIELD_ID,
|
||||||
aggregateOperation: AGGREGATE_OPERATIONS.sum,
|
aggregateOperation: AGGREGATE_OPERATIONS.sum,
|
||||||
|
localeCatalog: enUS,
|
||||||
...defaultParams,
|
...defaultParams,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -90,6 +93,7 @@ describe('computeAggregateValueAndLabel', () => {
|
|||||||
objectMetadataItem: mockObjectMetadataWithPercentageField,
|
objectMetadataItem: mockObjectMetadataWithPercentageField,
|
||||||
fieldMetadataId: MOCK_FIELD_ID,
|
fieldMetadataId: MOCK_FIELD_ID,
|
||||||
aggregateOperation: AGGREGATE_OPERATIONS.avg,
|
aggregateOperation: AGGREGATE_OPERATIONS.avg,
|
||||||
|
localeCatalog: enUS,
|
||||||
...defaultParams,
|
...defaultParams,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -127,6 +131,7 @@ describe('computeAggregateValueAndLabel', () => {
|
|||||||
objectMetadataItem: mockObjectMetadataWithDecimalsField,
|
objectMetadataItem: mockObjectMetadataWithDecimalsField,
|
||||||
fieldMetadataId: MOCK_FIELD_ID,
|
fieldMetadataId: MOCK_FIELD_ID,
|
||||||
aggregateOperation: AGGREGATE_OPERATIONS.sum,
|
aggregateOperation: AGGREGATE_OPERATIONS.sum,
|
||||||
|
localeCatalog: enUS,
|
||||||
...defaultParams,
|
...defaultParams,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -161,6 +166,7 @@ describe('computeAggregateValueAndLabel', () => {
|
|||||||
objectMetadataItem: mockObjectMetadataWithDatetimeField,
|
objectMetadataItem: mockObjectMetadataWithDatetimeField,
|
||||||
fieldMetadataId: MOCK_FIELD_ID,
|
fieldMetadataId: MOCK_FIELD_ID,
|
||||||
aggregateOperation: DATE_AGGREGATE_OPERATIONS.earliest,
|
aggregateOperation: DATE_AGGREGATE_OPERATIONS.earliest,
|
||||||
|
localeCatalog: enUS,
|
||||||
...defaultParams,
|
...defaultParams,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -195,6 +201,7 @@ describe('computeAggregateValueAndLabel', () => {
|
|||||||
objectMetadataItem: mockObjectMetadataWithDatetimeField,
|
objectMetadataItem: mockObjectMetadataWithDatetimeField,
|
||||||
fieldMetadataId: MOCK_FIELD_ID,
|
fieldMetadataId: MOCK_FIELD_ID,
|
||||||
aggregateOperation: DATE_AGGREGATE_OPERATIONS.latest,
|
aggregateOperation: DATE_AGGREGATE_OPERATIONS.latest,
|
||||||
|
localeCatalog: enUS,
|
||||||
...defaultParams,
|
...defaultParams,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -215,6 +222,7 @@ describe('computeAggregateValueAndLabel', () => {
|
|||||||
const result = computeAggregateValueAndLabel({
|
const result = computeAggregateValueAndLabel({
|
||||||
data: mockData,
|
data: mockData,
|
||||||
objectMetadataItem: mockObjectMetadata,
|
objectMetadataItem: mockObjectMetadata,
|
||||||
|
localeCatalog: enUS,
|
||||||
...defaultParams,
|
...defaultParams,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -237,6 +245,7 @@ describe('computeAggregateValueAndLabel', () => {
|
|||||||
objectMetadataItem: mockObjectMetadata,
|
objectMetadataItem: mockObjectMetadata,
|
||||||
fieldMetadataId: MOCK_FIELD_ID,
|
fieldMetadataId: MOCK_FIELD_ID,
|
||||||
aggregateOperation: AGGREGATE_OPERATIONS.sum,
|
aggregateOperation: AGGREGATE_OPERATIONS.sum,
|
||||||
|
localeCatalog: enUS,
|
||||||
...defaultParams,
|
...defaultParams,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -26,6 +26,7 @@ export const computeAggregateValueAndLabel = ({
|
|||||||
dateFormat,
|
dateFormat,
|
||||||
timeFormat,
|
timeFormat,
|
||||||
timeZone,
|
timeZone,
|
||||||
|
localeCatalog,
|
||||||
}: {
|
}: {
|
||||||
data: AggregateRecordsData;
|
data: AggregateRecordsData;
|
||||||
objectMetadataItem: ObjectMetadataItem;
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
@ -34,6 +35,7 @@ export const computeAggregateValueAndLabel = ({
|
|||||||
dateFormat: DateFormat;
|
dateFormat: DateFormat;
|
||||||
timeFormat: TimeFormat;
|
timeFormat: TimeFormat;
|
||||||
timeZone: string;
|
timeZone: string;
|
||||||
|
localeCatalog: Locale;
|
||||||
}) => {
|
}) => {
|
||||||
if (isEmpty(data)) {
|
if (isEmpty(data)) {
|
||||||
return {};
|
return {};
|
||||||
@ -105,6 +107,7 @@ export const computeAggregateValueAndLabel = ({
|
|||||||
dateFormat,
|
dateFormat,
|
||||||
timeFormat,
|
timeFormat,
|
||||||
dateFieldSettings,
|
dateFieldSettings,
|
||||||
|
localeCatalog,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -116,6 +119,7 @@ export const computeAggregateValueAndLabel = ({
|
|||||||
timeZone,
|
timeZone,
|
||||||
dateFormat,
|
dateFormat,
|
||||||
dateFieldSettings,
|
dateFieldSettings,
|
||||||
|
localeCatalog,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,10 +4,10 @@ import { DateFormat } from '@/localization/constants/DateFormat';
|
|||||||
import { TimeFormat } from '@/localization/constants/TimeFormat';
|
import { TimeFormat } from '@/localization/constants/TimeFormat';
|
||||||
import { DateFieldDisplay } from '@/object-record/record-field/meta-types/display/components/DateFieldDisplay';
|
import { DateFieldDisplay } from '@/object-record/record-field/meta-types/display/components/DateFieldDisplay';
|
||||||
import { UserContext } from '@/users/contexts/UserContext';
|
import { UserContext } from '@/users/contexts/UserContext';
|
||||||
|
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||||
import { getFieldDecorator } from '~/testing/decorators/getFieldDecorator';
|
import { getFieldDecorator } from '~/testing/decorators/getFieldDecorator';
|
||||||
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
|
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
|
||||||
import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
|
import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
|
||||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
|
||||||
|
|
||||||
const meta: Meta = {
|
const meta: Meta = {
|
||||||
title: 'UI/Data/Field/Display/DateFieldDisplay',
|
title: 'UI/Data/Field/Display/DateFieldDisplay',
|
||||||
|
|||||||
@ -4,10 +4,10 @@ import { DateFormat } from '@/localization/constants/DateFormat';
|
|||||||
import { TimeFormat } from '@/localization/constants/TimeFormat';
|
import { TimeFormat } from '@/localization/constants/TimeFormat';
|
||||||
import { DateTimeFieldDisplay } from '@/object-record/record-field/meta-types/display/components/DateTimeFieldDisplay';
|
import { DateTimeFieldDisplay } from '@/object-record/record-field/meta-types/display/components/DateTimeFieldDisplay';
|
||||||
import { UserContext } from '@/users/contexts/UserContext';
|
import { UserContext } from '@/users/contexts/UserContext';
|
||||||
|
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||||
import { getFieldDecorator } from '~/testing/decorators/getFieldDecorator';
|
import { getFieldDecorator } from '~/testing/decorators/getFieldDecorator';
|
||||||
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
|
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
|
||||||
import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
|
import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
|
||||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
|
||||||
|
|
||||||
const meta: Meta = {
|
const meta: Meta = {
|
||||||
title: 'UI/Data/Field/Display/DateTimeFieldDisplay',
|
title: 'UI/Data/Field/Display/DateTimeFieldDisplay',
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
/* eslint-disable @nx/workspace-no-navigate-prefer-link */
|
/* eslint-disable @nx/workspace-no-navigate-prefer-link */
|
||||||
import { RecordTableEmptyStateDisplay } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay';
|
import { RecordTableEmptyStateDisplay } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay';
|
||||||
import { SettingsPath } from '@/types/SettingsPath';
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
|
import { t } from '@lingui/core/macro';
|
||||||
import { IconSettings } from 'twenty-ui/display';
|
import { IconSettings } from 'twenty-ui/display';
|
||||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||||
|
|
||||||
@ -13,9 +14,9 @@ export const RecordTableEmptyStateRemote = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RecordTableEmptyStateDisplay
|
<RecordTableEmptyStateDisplay
|
||||||
buttonTitle={'Go to Settings'}
|
buttonTitle={t`Go to Settings`}
|
||||||
subTitle={'If this is unexpected, please verify your settings.'}
|
subTitle={t`If this is unexpected, please verify your settings.`}
|
||||||
title={'No Data Available for Remote Table'}
|
title={t`No Data Available for Remote Table`}
|
||||||
ButtonIcon={IconSettings}
|
ButtonIcon={IconSettings}
|
||||||
animatedPlaceholderType="noRecord"
|
animatedPlaceholderType="noRecord"
|
||||||
onClick={handleButtonClick}
|
onClick={handleButtonClick}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { UserContext } from '@/users/contexts/UserContext';
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { dateLocaleState } from '~/localization/states/dateLocaleState';
|
||||||
|
|
||||||
type UseAggregateRecordsProps = {
|
type UseAggregateRecordsProps = {
|
||||||
objectMetadataItem: ObjectMetadataItem;
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
@ -35,6 +36,8 @@ export const useAggregateRecordsForHeader = ({
|
|||||||
recordIndexKanbanAggregateOperationState,
|
recordIndexKanbanAggregateOperationState,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const dateLocale = useRecoilValue(dateLocaleState);
|
||||||
|
|
||||||
const { filterValueDependencies } = useFilterValueDependencies();
|
const { filterValueDependencies } = useFilterValueDependencies();
|
||||||
|
|
||||||
const { dateFormat, timeFormat, timeZone } = useContext(UserContext);
|
const { dateFormat, timeFormat, timeZone } = useContext(UserContext);
|
||||||
@ -65,6 +68,7 @@ export const useAggregateRecordsForHeader = ({
|
|||||||
dateFormat,
|
dateFormat,
|
||||||
timeFormat,
|
timeFormat,
|
||||||
timeZone,
|
timeZone,
|
||||||
|
localeCatalog: dateLocale.localeCatalog,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import { UserContext } from '@/users/contexts/UserContext';
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils';
|
import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils';
|
||||||
|
import { dateLocaleState } from '~/localization/states/dateLocaleState';
|
||||||
|
|
||||||
export const useAggregateRecordsForRecordTableColumnFooter = (
|
export const useAggregateRecordsForRecordTableColumnFooter = (
|
||||||
fieldMetadataId: string,
|
fieldMetadataId: string,
|
||||||
@ -31,6 +32,8 @@ export const useAggregateRecordsForRecordTableColumnFooter = (
|
|||||||
currentRecordFiltersComponentState,
|
currentRecordFiltersComponentState,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const dateLocale = useRecoilValue(dateLocaleState);
|
||||||
|
|
||||||
const { filterValueDependencies } = useFilterValueDependencies();
|
const { filterValueDependencies } = useFilterValueDependencies();
|
||||||
|
|
||||||
const requestFilters = computeRecordGqlOperationFilter({
|
const requestFilters = computeRecordGqlOperationFilter({
|
||||||
@ -99,6 +102,7 @@ export const useAggregateRecordsForRecordTableColumnFooter = (
|
|||||||
dateFormat,
|
dateFormat,
|
||||||
timeFormat,
|
timeFormat,
|
||||||
timeZone,
|
timeZone,
|
||||||
|
localeCatalog: dateLocale.localeCatalog,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { FieldDateMetadataSettings } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldDateMetadataSettings } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import { UserContext } from '@/users/contexts/UserContext';
|
import { UserContext } from '@/users/contexts/UserContext';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { dateLocaleState } from '~/localization/states/dateLocaleState';
|
||||||
import { formatDateString } from '~/utils/string/formatDateString';
|
import { formatDateString } from '~/utils/string/formatDateString';
|
||||||
import { EllipsisDisplay } from './EllipsisDisplay';
|
import { EllipsisDisplay } from './EllipsisDisplay';
|
||||||
|
|
||||||
@ -8,15 +10,16 @@ type DateDisplayProps = {
|
|||||||
value: string | null | undefined;
|
value: string | null | undefined;
|
||||||
dateFieldSettings?: FieldDateMetadataSettings;
|
dateFieldSettings?: FieldDateMetadataSettings;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DateDisplay = ({ value, dateFieldSettings }: DateDisplayProps) => {
|
export const DateDisplay = ({ value, dateFieldSettings }: DateDisplayProps) => {
|
||||||
const { dateFormat, timeZone } = useContext(UserContext);
|
const { dateFormat } = useContext(UserContext);
|
||||||
|
const dateLocale = useRecoilValue(dateLocaleState);
|
||||||
|
|
||||||
const formattedDate = formatDateString({
|
const formattedDate = formatDateString({
|
||||||
value,
|
value,
|
||||||
timeZone,
|
timeZone: 'UTC', // Needed because we have db-stored date (yyyy-mm-dd) is converted to UTC dateTime by TypeORM
|
||||||
dateFormat,
|
dateFormat,
|
||||||
dateFieldSettings,
|
dateFieldSettings,
|
||||||
|
localeCatalog: dateLocale.localeCatalog,
|
||||||
});
|
});
|
||||||
|
|
||||||
return <EllipsisDisplay>{formattedDate}</EllipsisDisplay>;
|
return <EllipsisDisplay>{formattedDate}</EllipsisDisplay>;
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { FieldDateMetadataSettings } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldDateMetadataSettings } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import { UserContext } from '@/users/contexts/UserContext';
|
import { UserContext } from '@/users/contexts/UserContext';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { dateLocaleState } from '~/localization/states/dateLocaleState';
|
||||||
import { formatDateTimeString } from '~/utils/string/formatDateTimeString';
|
import { formatDateTimeString } from '~/utils/string/formatDateTimeString';
|
||||||
import { EllipsisDisplay } from './EllipsisDisplay';
|
import { EllipsisDisplay } from './EllipsisDisplay';
|
||||||
|
|
||||||
@ -14,6 +16,7 @@ export const DateTimeDisplay = ({
|
|||||||
dateFieldSettings,
|
dateFieldSettings,
|
||||||
}: DateTimeDisplayProps) => {
|
}: DateTimeDisplayProps) => {
|
||||||
const { dateFormat, timeFormat, timeZone } = useContext(UserContext);
|
const { dateFormat, timeFormat, timeZone } = useContext(UserContext);
|
||||||
|
const dateLocale = useRecoilValue(dateLocaleState);
|
||||||
|
|
||||||
const formattedDate = formatDateTimeString({
|
const formattedDate = formatDateTimeString({
|
||||||
value,
|
value,
|
||||||
@ -21,6 +24,7 @@ export const DateTimeDisplay = ({
|
|||||||
dateFormat,
|
dateFormat,
|
||||||
timeFormat,
|
timeFormat,
|
||||||
dateFieldSettings,
|
dateFieldSettings,
|
||||||
|
localeCatalog: dateLocale.localeCatalog,
|
||||||
});
|
});
|
||||||
|
|
||||||
return <EllipsisDisplay>{formattedDate}</EllipsisDisplay>;
|
return <EllipsisDisplay>{formattedDate}</EllipsisDisplay>;
|
||||||
|
|||||||
@ -0,0 +1,44 @@
|
|||||||
|
import { DateFormat } from '@/localization/constants/DateFormat';
|
||||||
|
import { TimeFormat } from '@/localization/constants/TimeFormat';
|
||||||
|
import { FieldDateDisplayFormat } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
|
import { UserContext } from '@/users/contexts/UserContext';
|
||||||
|
import { expect } from '@storybook/jest';
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { within } from '@storybook/testing-library';
|
||||||
|
import { DateDisplay } from '../DateDisplay';
|
||||||
|
|
||||||
|
const meta: Meta<typeof DateDisplay> = {
|
||||||
|
title: 'UI/Field/Display/DateDisplay',
|
||||||
|
component: DateDisplay,
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<UserContext.Provider
|
||||||
|
value={{
|
||||||
|
dateFormat: DateFormat.DAY_FIRST,
|
||||||
|
timeFormat: TimeFormat.HOUR_24,
|
||||||
|
timeZone: 'Pacific/Tahiti', // Needed for our test on time difference
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Story />
|
||||||
|
</UserContext.Provider>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof DateDisplay>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
value: '2025-05-01T00:00:00.000Z',
|
||||||
|
dateFieldSettings: {
|
||||||
|
displayFormat: FieldDateDisplayFormat.USER_SETTINGS,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
// Test that date is rightfully displayed and not converted to timeZone date which would be on April 30th
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
const dateElement = await canvas.findByText('1 May, 2025');
|
||||||
|
expect(dateElement).toBeInTheDocument();
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1,76 @@
|
|||||||
|
import { APP_LOCALES } from 'twenty-shared/translations';
|
||||||
|
|
||||||
|
type AppLocale = keyof typeof APP_LOCALES;
|
||||||
|
export const getDateFnsLocaleImport = (locale: AppLocale) => {
|
||||||
|
switch (locale) {
|
||||||
|
case 'af-ZA':
|
||||||
|
return import('date-fns/locale/af');
|
||||||
|
case 'ar-SA':
|
||||||
|
return import('date-fns/locale/ar');
|
||||||
|
case 'ca-ES':
|
||||||
|
return import('date-fns/locale/ca');
|
||||||
|
case 'cs-CZ':
|
||||||
|
return import('date-fns/locale/cs');
|
||||||
|
case 'da-DK':
|
||||||
|
return import('date-fns/locale/da');
|
||||||
|
case 'de-DE':
|
||||||
|
return import('date-fns/locale/de');
|
||||||
|
case 'el-GR':
|
||||||
|
return import('date-fns/locale/el');
|
||||||
|
case 'en':
|
||||||
|
case 'pseudo-en':
|
||||||
|
return import('date-fns/locale/en-US');
|
||||||
|
case 'es-ES':
|
||||||
|
return import('date-fns/locale/es');
|
||||||
|
case 'fi-FI':
|
||||||
|
return import('date-fns/locale/fi');
|
||||||
|
case 'fr-FR':
|
||||||
|
return import('date-fns/locale/fr');
|
||||||
|
case 'he-IL':
|
||||||
|
return import('date-fns/locale/he');
|
||||||
|
case 'hu-HU':
|
||||||
|
return import('date-fns/locale/hu');
|
||||||
|
case 'it-IT':
|
||||||
|
return import('date-fns/locale/it');
|
||||||
|
case 'ja-JP':
|
||||||
|
return import('date-fns/locale/ja');
|
||||||
|
case 'ko-KR':
|
||||||
|
return import('date-fns/locale/ko');
|
||||||
|
case 'nl-NL':
|
||||||
|
return import('date-fns/locale/nl');
|
||||||
|
case 'no-NO':
|
||||||
|
return import('date-fns/locale/nb');
|
||||||
|
case 'pl-PL':
|
||||||
|
return import('date-fns/locale/pl');
|
||||||
|
case 'pt-BR':
|
||||||
|
case 'pt-PT':
|
||||||
|
return import('date-fns/locale/pt');
|
||||||
|
case 'ro-RO':
|
||||||
|
return import('date-fns/locale/ro');
|
||||||
|
case 'ru-RU':
|
||||||
|
return import('date-fns/locale/ru');
|
||||||
|
case 'sr-Cyrl':
|
||||||
|
return import('date-fns/locale/sr');
|
||||||
|
case 'sv-SE':
|
||||||
|
return import('date-fns/locale/sv');
|
||||||
|
case 'tr-TR':
|
||||||
|
return import('date-fns/locale/tr');
|
||||||
|
case 'uk-UA':
|
||||||
|
return import('date-fns/locale/uk');
|
||||||
|
case 'vi-VN':
|
||||||
|
return import('date-fns/locale/vi');
|
||||||
|
case 'zh-CN':
|
||||||
|
return import('date-fns/locale/zh-CN');
|
||||||
|
case 'zh-TW':
|
||||||
|
return import('date-fns/locale/zh-TW');
|
||||||
|
default: {
|
||||||
|
return import('date-fns/locale/en-US');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDateFnsLocale = async (localeString?: string | null) => {
|
||||||
|
return getDateFnsLocaleImport(localeString as AppLocale)
|
||||||
|
.then((m) => m.default as unknown as Locale)
|
||||||
|
.catch((_e) => undefined);
|
||||||
|
};
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { currentUserState } from '@/auth/states/currentUserState';
|
import { currentUserState } from '@/auth/states/currentUserState';
|
||||||
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
|
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
|
||||||
@ -18,12 +18,15 @@ import { detectTimeZone } from '@/localization/utils/detectTimeZone';
|
|||||||
import { getDateFormatFromWorkspaceDateFormat } from '@/localization/utils/getDateFormatFromWorkspaceDateFormat';
|
import { getDateFormatFromWorkspaceDateFormat } from '@/localization/utils/getDateFormatFromWorkspaceDateFormat';
|
||||||
import { getTimeFormatFromWorkspaceTimeFormat } from '@/localization/utils/getTimeFormatFromWorkspaceTimeFormat';
|
import { getTimeFormatFromWorkspaceTimeFormat } from '@/localization/utils/getTimeFormatFromWorkspaceTimeFormat';
|
||||||
import { AppPath } from '@/types/AppPath';
|
import { AppPath } from '@/types/AppPath';
|
||||||
|
import { getDateFnsLocale } from '@/ui/field/display/utils/getDateFnsLocale.util';
|
||||||
import { ColorScheme } from '@/workspace-member/types/WorkspaceMember';
|
import { ColorScheme } from '@/workspace-member/types/WorkspaceMember';
|
||||||
|
import { enUS } from 'date-fns/locale';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
|
import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { WorkspaceMember } from '~/generated-metadata/graphql';
|
import { WorkspaceMember } from '~/generated-metadata/graphql';
|
||||||
import { useGetCurrentUserQuery } from '~/generated/graphql';
|
import { useGetCurrentUserQuery } from '~/generated/graphql';
|
||||||
|
import { dateLocaleState } from '~/localization/states/dateLocaleState';
|
||||||
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
|
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
|
||||||
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||||
|
|
||||||
@ -38,8 +41,22 @@ export const UserProviderEffect = () => {
|
|||||||
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
|
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
|
||||||
const setCurrentUserWorkspace = useSetRecoilState(currentUserWorkspaceState);
|
const setCurrentUserWorkspace = useSetRecoilState(currentUserWorkspaceState);
|
||||||
const setWorkspaces = useSetRecoilState(workspacesState);
|
const setWorkspaces = useSetRecoilState(workspacesState);
|
||||||
|
|
||||||
const setDateTimeFormat = useSetRecoilState(dateTimeFormatState);
|
const setDateTimeFormat = useSetRecoilState(dateTimeFormatState);
|
||||||
|
const updateLocaleCatalog = useRecoilCallback(
|
||||||
|
({ snapshot, set }) =>
|
||||||
|
async (newLocale: keyof typeof APP_LOCALES) => {
|
||||||
|
const localeValue = snapshot.getLoadable(dateLocaleState).getValue();
|
||||||
|
if (localeValue.locale !== newLocale) {
|
||||||
|
getDateFnsLocale(newLocale).then((localeCatalog) => {
|
||||||
|
set(dateLocaleState, {
|
||||||
|
locale: newLocale,
|
||||||
|
localeCatalog: localeCatalog || enUS,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const setCurrentWorkspaceMember = useSetRecoilState(
|
const setCurrentWorkspaceMember = useSetRecoilState(
|
||||||
currentWorkspaceMemberState,
|
currentWorkspaceMemberState,
|
||||||
@ -98,9 +115,11 @@ export const UserProviderEffect = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (isDefined(workspaceMember)) {
|
if (isDefined(workspaceMember)) {
|
||||||
setCurrentWorkspaceMember(
|
const updatedWorkspaceMember =
|
||||||
affectDefaultValuesOnEmptyWorkspaceMemberFields(workspaceMember),
|
affectDefaultValuesOnEmptyWorkspaceMemberFields(workspaceMember);
|
||||||
);
|
setCurrentWorkspaceMember(updatedWorkspaceMember);
|
||||||
|
|
||||||
|
updateLocaleCatalog(updatedWorkspaceMember.locale);
|
||||||
|
|
||||||
// TODO: factorize
|
// TODO: factorize
|
||||||
setDateTimeFormat({
|
setDateTimeFormat({
|
||||||
@ -152,6 +171,7 @@ export const UserProviderEffect = () => {
|
|||||||
setIsCurrentUserLoaded,
|
setIsCurrentUserLoaded,
|
||||||
setDateTimeFormat,
|
setDateTimeFormat,
|
||||||
setCurrentWorkspaceMembersWithDeleted,
|
setCurrentWorkspaceMembersWithDeleted,
|
||||||
|
updateLocaleCatalog,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return <></>;
|
return <></>;
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { useViewPickerMode } from '@/views/view-picker/hooks/useViewPickerMode';
|
|||||||
import { viewPickerIsPersistingComponentState } from '@/views/view-picker/states/viewPickerIsPersistingComponentState';
|
import { viewPickerIsPersistingComponentState } from '@/views/view-picker/states/viewPickerIsPersistingComponentState';
|
||||||
import { viewPickerKanbanFieldMetadataIdComponentState } from '@/views/view-picker/states/viewPickerKanbanFieldMetadataIdComponentState';
|
import { viewPickerKanbanFieldMetadataIdComponentState } from '@/views/view-picker/states/viewPickerKanbanFieldMetadataIdComponentState';
|
||||||
import { viewPickerTypeComponentState } from '@/views/view-picker/states/viewPickerTypeComponentState';
|
import { viewPickerTypeComponentState } from '@/views/view-picker/states/viewPickerTypeComponentState';
|
||||||
|
import { t } from '@lingui/core/macro';
|
||||||
import { Button } from 'twenty-ui/input';
|
import { Button } from 'twenty-ui/input';
|
||||||
|
|
||||||
export const ViewPickerEditButton = () => {
|
export const ViewPickerEditButton = () => {
|
||||||
@ -49,7 +50,7 @@ export const ViewPickerEditButton = () => {
|
|||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
title="Go to Settings"
|
title={t`Go to Settings`}
|
||||||
onClick={navigateToSelectSettings}
|
onClick={navigateToSelectSettings}
|
||||||
size="small"
|
size="small"
|
||||||
accent="blue"
|
accent="blue"
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import {
|
|||||||
} from '@/ui/layout/modal/components/ConfirmationModal';
|
} from '@/ui/layout/modal/components/ConfirmationModal';
|
||||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||||
import { useCreateDraftFromWorkflowVersion } from '@/workflow/hooks/useCreateDraftFromWorkflowVersion';
|
import { useCreateDraftFromWorkflowVersion } from '@/workflow/hooks/useCreateDraftFromWorkflowVersion';
|
||||||
|
import { t } from '@lingui/core/macro';
|
||||||
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
||||||
import { getAppPath } from '~/utils/navigation/getAppPath';
|
import { getAppPath } from '~/utils/navigation/getAppPath';
|
||||||
|
|
||||||
@ -56,7 +57,7 @@ export const OverrideWorkflowDraftConfirmationModal = ({
|
|||||||
closeModal(OVERRIDE_WORKFLOW_DRAFT_CONFIRMATION_MODAL_ID);
|
closeModal(OVERRIDE_WORKFLOW_DRAFT_CONFIRMATION_MODAL_ID);
|
||||||
}}
|
}}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
title="Go to Draft"
|
title={t`Go to Draft`}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,19 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
|
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||||
|
import { getDateFnsLocale } from '@/ui/field/display/utils/getDateFnsLocale.util';
|
||||||
import { Select } from '@/ui/input/components/Select';
|
import { Select } from '@/ui/input/components/Select';
|
||||||
|
|
||||||
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItem';
|
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItem';
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
|
import { enUS } from 'date-fns/locale';
|
||||||
import { APP_LOCALES } from 'twenty-shared/translations';
|
import { APP_LOCALES } from 'twenty-shared/translations';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { dateLocaleState } from '~/localization/states/dateLocaleState';
|
||||||
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
|
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
|
||||||
import { logError } from '~/utils/logError';
|
import { logError } from '~/utils/logError';
|
||||||
|
|
||||||
@ -25,6 +28,7 @@ export const LocalePicker = () => {
|
|||||||
const [currentWorkspaceMember, setCurrentWorkspaceMember] = useRecoilState(
|
const [currentWorkspaceMember, setCurrentWorkspaceMember] = useRecoilState(
|
||||||
currentWorkspaceMemberState,
|
currentWorkspaceMemberState,
|
||||||
);
|
);
|
||||||
|
const setDateLocale = useSetRecoilState(dateLocaleState);
|
||||||
const isDebugMode = useRecoilValue(isDebugModeState);
|
const isDebugMode = useRecoilValue(isDebugModeState);
|
||||||
|
|
||||||
const { updateOneRecord } = useUpdateOneRecord({
|
const { updateOneRecord } = useUpdateOneRecord({
|
||||||
@ -58,6 +62,12 @@ export const LocalePicker = () => {
|
|||||||
});
|
});
|
||||||
await updateWorkspaceMember({ locale: value });
|
await updateWorkspaceMember({ locale: value });
|
||||||
|
|
||||||
|
const dateFnsLocale = await getDateFnsLocale(value);
|
||||||
|
setDateLocale({
|
||||||
|
locale: value,
|
||||||
|
localeCatalog: dateFnsLocale || enUS,
|
||||||
|
});
|
||||||
|
|
||||||
await dynamicActivate(value);
|
await dynamicActivate(value);
|
||||||
try {
|
try {
|
||||||
localStorage.setItem('locale', value);
|
localStorage.setItem('locale', value);
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { DateFormat } from '@/localization/constants/DateFormat';
|
import { DateFormat } from '@/localization/constants/DateFormat';
|
||||||
import { FieldDateDisplayFormat } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldDateDisplayFormat } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
|
import { enUS } from 'date-fns/locale';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { formatDateString } from '~/utils/string/formatDateString';
|
import { formatDateString } from '~/utils/string/formatDateString';
|
||||||
|
|
||||||
@ -13,6 +14,7 @@ describe('formatDateString', () => {
|
|||||||
const result = formatDateString({
|
const result = formatDateString({
|
||||||
...defaultParams,
|
...defaultParams,
|
||||||
value: null,
|
value: null,
|
||||||
|
localeCatalog: enUS,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toBe('');
|
expect(result).toBe('');
|
||||||
@ -22,6 +24,7 @@ describe('formatDateString', () => {
|
|||||||
const result = formatDateString({
|
const result = formatDateString({
|
||||||
...defaultParams,
|
...defaultParams,
|
||||||
value: undefined,
|
value: undefined,
|
||||||
|
localeCatalog: enUS,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toBe('');
|
expect(result).toBe('');
|
||||||
@ -37,6 +40,7 @@ describe('formatDateString', () => {
|
|||||||
dateFieldSettings: {
|
dateFieldSettings: {
|
||||||
displayFormat: FieldDateDisplayFormat.RELATIVE,
|
displayFormat: FieldDateDisplayFormat.RELATIVE,
|
||||||
},
|
},
|
||||||
|
localeCatalog: enUS,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toBe(mockRelativeDate);
|
expect(result).toBe(mockRelativeDate);
|
||||||
@ -58,6 +62,7 @@ describe('formatDateString', () => {
|
|||||||
dateFieldSettings: {
|
dateFieldSettings: {
|
||||||
displayFormat: FieldDateDisplayFormat.USER_SETTINGS,
|
displayFormat: FieldDateDisplayFormat.USER_SETTINGS,
|
||||||
},
|
},
|
||||||
|
localeCatalog: enUS,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toBe(mockFormattedDate);
|
expect(result).toBe(mockFormattedDate);
|
||||||
@ -83,6 +88,7 @@ describe('formatDateString', () => {
|
|||||||
displayFormat: FieldDateDisplayFormat.CUSTOM,
|
displayFormat: FieldDateDisplayFormat.CUSTOM,
|
||||||
customUnicodeDateFormat: 'yyyy',
|
customUnicodeDateFormat: 'yyyy',
|
||||||
},
|
},
|
||||||
|
localeCatalog: enUS,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toBe(mockFormattedDate);
|
expect(result).toBe(mockFormattedDate);
|
||||||
@ -101,6 +107,7 @@ describe('formatDateString', () => {
|
|||||||
const result = formatDateString({
|
const result = formatDateString({
|
||||||
...defaultParams,
|
...defaultParams,
|
||||||
value: mockDate,
|
value: mockDate,
|
||||||
|
localeCatalog: enUS,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toBe(mockFormattedDate);
|
expect(result).toBe(mockFormattedDate);
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { DateFormat } from '@/localization/constants/DateFormat';
|
import { DateFormat } from '@/localization/constants/DateFormat';
|
||||||
import { TimeFormat } from '@/localization/constants/TimeFormat';
|
import { TimeFormat } from '@/localization/constants/TimeFormat';
|
||||||
import { FieldDateDisplayFormat } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldDateDisplayFormat } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
|
import { enUS } from 'date-fns/locale';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { formatDateTimeString } from '~/utils/string/formatDateTimeString';
|
import { formatDateTimeString } from '~/utils/string/formatDateTimeString';
|
||||||
|
|
||||||
@ -15,6 +16,7 @@ describe('formatDateTimeString', () => {
|
|||||||
const result = formatDateTimeString({
|
const result = formatDateTimeString({
|
||||||
...defaultParams,
|
...defaultParams,
|
||||||
value: null,
|
value: null,
|
||||||
|
localeCatalog: enUS,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toBe('');
|
expect(result).toBe('');
|
||||||
@ -24,6 +26,7 @@ describe('formatDateTimeString', () => {
|
|||||||
const result = formatDateTimeString({
|
const result = formatDateTimeString({
|
||||||
...defaultParams,
|
...defaultParams,
|
||||||
value: undefined,
|
value: undefined,
|
||||||
|
localeCatalog: enUS,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toBe('');
|
expect(result).toBe('');
|
||||||
@ -39,6 +42,7 @@ describe('formatDateTimeString', () => {
|
|||||||
dateFieldSettings: {
|
dateFieldSettings: {
|
||||||
displayFormat: FieldDateDisplayFormat.RELATIVE,
|
displayFormat: FieldDateDisplayFormat.RELATIVE,
|
||||||
},
|
},
|
||||||
|
localeCatalog: enUS,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toBe(mockRelativeDate);
|
expect(result).toBe(mockRelativeDate);
|
||||||
@ -60,6 +64,7 @@ describe('formatDateTimeString', () => {
|
|||||||
dateFieldSettings: {
|
dateFieldSettings: {
|
||||||
displayFormat: FieldDateDisplayFormat.USER_SETTINGS,
|
displayFormat: FieldDateDisplayFormat.USER_SETTINGS,
|
||||||
},
|
},
|
||||||
|
localeCatalog: enUS,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toBe(mockFormattedDate);
|
expect(result).toBe(mockFormattedDate);
|
||||||
@ -85,6 +90,7 @@ describe('formatDateTimeString', () => {
|
|||||||
displayFormat: FieldDateDisplayFormat.CUSTOM,
|
displayFormat: FieldDateDisplayFormat.CUSTOM,
|
||||||
customUnicodeDateFormat: 'yyyy',
|
customUnicodeDateFormat: 'yyyy',
|
||||||
},
|
},
|
||||||
|
localeCatalog: enUS,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toBe(mockFormattedDate);
|
expect(result).toBe(mockFormattedDate);
|
||||||
@ -103,6 +109,7 @@ describe('formatDateTimeString', () => {
|
|||||||
const result = formatDateTimeString({
|
const result = formatDateTimeString({
|
||||||
...defaultParams,
|
...defaultParams,
|
||||||
value: mockDate,
|
value: mockDate,
|
||||||
|
localeCatalog: enUS,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toBe(mockFormattedDate);
|
expect(result).toBe(mockFormattedDate);
|
||||||
|
|||||||
@ -13,11 +13,13 @@ export const formatDateString = ({
|
|||||||
timeZone,
|
timeZone,
|
||||||
dateFormat,
|
dateFormat,
|
||||||
dateFieldSettings,
|
dateFieldSettings,
|
||||||
|
localeCatalog,
|
||||||
}: {
|
}: {
|
||||||
timeZone: string;
|
timeZone: string;
|
||||||
dateFormat: DateFormat;
|
dateFormat: DateFormat;
|
||||||
value?: string | null;
|
value?: string | null;
|
||||||
dateFieldSettings?: FieldDateMetadataSettings;
|
dateFieldSettings?: FieldDateMetadataSettings;
|
||||||
|
localeCatalog: Locale;
|
||||||
}): string => {
|
}): string => {
|
||||||
if (!isDefined(value)) {
|
if (!isDefined(value)) {
|
||||||
return '';
|
return '';
|
||||||
@ -25,16 +27,31 @@ export const formatDateString = ({
|
|||||||
|
|
||||||
switch (dateFieldSettings?.displayFormat) {
|
switch (dateFieldSettings?.displayFormat) {
|
||||||
case FieldDateDisplayFormat.RELATIVE:
|
case FieldDateDisplayFormat.RELATIVE:
|
||||||
return formatDateISOStringToRelativeDate(value);
|
return formatDateISOStringToRelativeDate({
|
||||||
|
isoDate: value,
|
||||||
|
isDayMaximumPrecision: true,
|
||||||
|
localeCatalog,
|
||||||
|
});
|
||||||
case FieldDateDisplayFormat.USER_SETTINGS:
|
case FieldDateDisplayFormat.USER_SETTINGS:
|
||||||
return formatDateISOStringToDate(value, timeZone, dateFormat);
|
return formatDateISOStringToDate({
|
||||||
case FieldDateDisplayFormat.CUSTOM:
|
date: value,
|
||||||
return formatDateISOStringToCustomUnicodeFormat(
|
|
||||||
value,
|
|
||||||
timeZone,
|
timeZone,
|
||||||
dateFieldSettings.customUnicodeDateFormat,
|
dateFormat,
|
||||||
);
|
localeCatalog,
|
||||||
|
});
|
||||||
|
case FieldDateDisplayFormat.CUSTOM:
|
||||||
|
return formatDateISOStringToCustomUnicodeFormat({
|
||||||
|
date: value,
|
||||||
|
timeZone,
|
||||||
|
dateFormat: dateFieldSettings.customUnicodeDateFormat,
|
||||||
|
localeCatalog,
|
||||||
|
});
|
||||||
default:
|
default:
|
||||||
return formatDateISOStringToDate(value, timeZone, dateFormat);
|
return formatDateISOStringToDate({
|
||||||
|
date: value,
|
||||||
|
timeZone,
|
||||||
|
dateFormat,
|
||||||
|
localeCatalog,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
FieldDateDisplayFormat,
|
FieldDateDisplayFormat,
|
||||||
FieldDateMetadataSettings,
|
FieldDateMetadataSettings,
|
||||||
} from '@/object-record/record-field/types/FieldMetadata';
|
} from '@/object-record/record-field/types/FieldMetadata';
|
||||||
|
import { Locale } from 'date-fns';
|
||||||
|
|
||||||
export const formatDateTimeString = ({
|
export const formatDateTimeString = ({
|
||||||
value,
|
value,
|
||||||
@ -14,12 +15,14 @@ export const formatDateTimeString = ({
|
|||||||
dateFormat,
|
dateFormat,
|
||||||
timeFormat,
|
timeFormat,
|
||||||
dateFieldSettings,
|
dateFieldSettings,
|
||||||
|
localeCatalog,
|
||||||
}: {
|
}: {
|
||||||
timeZone: string;
|
timeZone: string;
|
||||||
dateFormat: DateFormat;
|
dateFormat: DateFormat;
|
||||||
timeFormat: TimeFormat;
|
timeFormat: TimeFormat;
|
||||||
value?: string | null;
|
value?: string | null;
|
||||||
dateFieldSettings?: FieldDateMetadataSettings;
|
dateFieldSettings?: FieldDateMetadataSettings;
|
||||||
|
localeCatalog: Locale;
|
||||||
}) => {
|
}) => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return '';
|
return '';
|
||||||
@ -27,26 +30,32 @@ export const formatDateTimeString = ({
|
|||||||
|
|
||||||
switch (dateFieldSettings?.displayFormat) {
|
switch (dateFieldSettings?.displayFormat) {
|
||||||
case FieldDateDisplayFormat.RELATIVE:
|
case FieldDateDisplayFormat.RELATIVE:
|
||||||
return formatDateISOStringToRelativeDate(value);
|
return formatDateISOStringToRelativeDate({
|
||||||
|
isoDate: value,
|
||||||
|
localeCatalog: localeCatalog,
|
||||||
|
});
|
||||||
case FieldDateDisplayFormat.USER_SETTINGS:
|
case FieldDateDisplayFormat.USER_SETTINGS:
|
||||||
return formatDateISOStringToDateTime(
|
return formatDateISOStringToDateTime({
|
||||||
value,
|
date: value,
|
||||||
timeZone,
|
timeZone,
|
||||||
dateFormat,
|
dateFormat,
|
||||||
timeFormat,
|
timeFormat,
|
||||||
);
|
localeCatalog,
|
||||||
|
});
|
||||||
case FieldDateDisplayFormat.CUSTOM:
|
case FieldDateDisplayFormat.CUSTOM:
|
||||||
return formatDateISOStringToCustomUnicodeFormat(
|
return formatDateISOStringToCustomUnicodeFormat({
|
||||||
value,
|
date: value,
|
||||||
timeZone,
|
timeZone,
|
||||||
dateFieldSettings.customUnicodeDateFormat,
|
dateFormat: dateFieldSettings.customUnicodeDateFormat,
|
||||||
);
|
localeCatalog,
|
||||||
|
});
|
||||||
default:
|
default:
|
||||||
return formatDateISOStringToDateTime(
|
return formatDateISOStringToDateTime({
|
||||||
value,
|
date: value,
|
||||||
timeZone,
|
timeZone,
|
||||||
dateFormat,
|
dateFormat,
|
||||||
timeFormat,
|
timeFormat,
|
||||||
);
|
localeCatalog,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user