diff --git a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts index 7a7de0807f1b..ae13d831fb7a 100644 --- a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts +++ b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts @@ -32,6 +32,8 @@ import { import { isDefined } from '~/utils/isDefined'; import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates'; +import { DateFormat } from '@/localization/constants/DateFormat'; +import { TimeFormat } from '@/localization/constants/TimeFormat'; import { dateTimeFormatState } from '@/localization/states/dateTimeFormatState'; import { detectDateFormat } from '@/localization/utils/detectDateFormat'; import { detectTimeFormat } from '@/localization/utils/detectTimeFormat'; @@ -143,12 +145,12 @@ export const useAuth = () => { ? getDateFormatFromWorkspaceDateFormat( user.workspaceMember.dateFormat, ) - : detectDateFormat(), + : DateFormat[detectDateFormat()], timeFormat: isDefined(user.workspaceMember.timeFormat) ? getTimeFormatFromWorkspaceTimeFormat( user.workspaceMember.timeFormat, ) - : detectTimeFormat(), + : TimeFormat[detectTimeFormat()], }); } diff --git a/packages/twenty-front/src/modules/localization/constants/DateFormatWithoutYear.ts b/packages/twenty-front/src/modules/localization/constants/DateFormatWithoutYear.ts new file mode 100644 index 000000000000..a1c7f2af3b72 --- /dev/null +++ b/packages/twenty-front/src/modules/localization/constants/DateFormatWithoutYear.ts @@ -0,0 +1,11 @@ +import { DateFormat } from '@/localization/constants/DateFormat'; + +type DateFormatWithoutYear = { + [K in keyof typeof DateFormat]: string; +}; +export const DATE_FORMAT_WITHOUT_YEAR: DateFormatWithoutYear = { + SYSTEM: 'SYSTEM', + MONTH_FIRST: 'MMM d', + DAY_FIRST: 'd MMM', + YEAR_FIRST: 'MMM d', +}; diff --git a/packages/twenty-front/src/modules/localization/utils/__tests__/detectDateFormat.test.ts b/packages/twenty-front/src/modules/localization/utils/__tests__/detectDateFormat.test.ts index 2b641f302a63..b267622bf0cc 100644 --- a/packages/twenty-front/src/modules/localization/utils/__tests__/detectDateFormat.test.ts +++ b/packages/twenty-front/src/modules/localization/utils/__tests__/detectDateFormat.test.ts @@ -1,8 +1,7 @@ -import { DateFormat } from '@/localization/constants/DateFormat'; import { detectDateFormat } from '@/localization/utils/detectDateFormat'; describe('detectDateFormat', () => { - it('should return DateFormat.MONTH_FIRST if the detected format starts with month', () => { + it('should return MONTH_FIRST if the detected format starts with month', () => { // Mock the Intl.DateTimeFormat to return a specific format const mockDateTimeFormat = jest.fn().mockReturnValue({ formatToParts: () => [ @@ -16,10 +15,10 @@ describe('detectDateFormat', () => { const result = detectDateFormat(); - expect(result).toBe(DateFormat.MONTH_FIRST); + expect(result).toBe('MONTH_FIRST'); }); - it('should return DateFormat.DAY_FIRST if the detected format starts with day', () => { + it('should return DAY_FIRST if the detected format starts with day', () => { // Mock the Intl.DateTimeFormat to return a specific format const mockDateTimeFormat = jest.fn().mockReturnValue({ formatToParts: () => [ @@ -32,10 +31,10 @@ describe('detectDateFormat', () => { const result = detectDateFormat(); - expect(result).toBe(DateFormat.DAY_FIRST); + expect(result).toBe('DAY_FIRST'); }); - it('should return DateFormat.YEAR_FIRST if the detected format starts with year', () => { + it('should return YEAR_FIRST if the detected format starts with year', () => { // Mock the Intl.DateTimeFormat to return a specific format const mockDateTimeFormat = jest.fn().mockReturnValue({ formatToParts: () => [ @@ -48,10 +47,10 @@ describe('detectDateFormat', () => { const result = detectDateFormat(); - expect(result).toBe(DateFormat.YEAR_FIRST); + expect(result).toBe('YEAR_FIRST'); }); - it('should return DateFormat.MONTH_FIRST by default if the detected format does not match any specific order', () => { + it('should return MONTH_FIRST by default if the detected format does not match any specific order', () => { // Mock the Intl.DateTimeFormat to return a specific format const mockDateTimeFormat = jest.fn().mockReturnValue({ formatToParts: () => [ @@ -64,6 +63,6 @@ describe('detectDateFormat', () => { const result = detectDateFormat(); - expect(result).toBe(DateFormat.MONTH_FIRST); + expect(result).toBe('MONTH_FIRST'); }); }); diff --git a/packages/twenty-front/src/modules/localization/utils/__tests__/detectTimeFormat.test.ts b/packages/twenty-front/src/modules/localization/utils/__tests__/detectTimeFormat.test.ts index 6433495789ee..9445068a5f7f 100644 --- a/packages/twenty-front/src/modules/localization/utils/__tests__/detectTimeFormat.test.ts +++ b/packages/twenty-front/src/modules/localization/utils/__tests__/detectTimeFormat.test.ts @@ -1,8 +1,7 @@ -import { TimeFormat } from '@/localization/constants/TimeFormat'; import { detectTimeFormat } from '@/localization/utils/detectTimeFormat'; describe('detectTimeFormat', () => { - it('should return TimeFormat.HOUR_12 if the hour format is 12-hour', () => { + it('should return HOUR_12 if the hour format is 12-hour', () => { // Mock the resolvedOptions method to return hour12 as true const mockResolvedOptions = jest.fn(() => ({ hour12: true })); Intl.DateTimeFormat = jest.fn().mockImplementation(() => ({ @@ -11,11 +10,11 @@ describe('detectTimeFormat', () => { const result = detectTimeFormat(); - expect(result).toBe(TimeFormat.HOUR_12); + expect(result).toBe('HOUR_12'); expect(mockResolvedOptions).toHaveBeenCalled(); }); - it('should return TimeFormat.HOUR_24 if the hour format is 24-hour', () => { + it('should return HOUR_24 if the hour format is 24-hour', () => { // Mock the resolvedOptions method to return hour12 as false const mockResolvedOptions = jest.fn(() => ({ hour12: false })); Intl.DateTimeFormat = jest.fn().mockImplementation(() => ({ @@ -24,7 +23,7 @@ describe('detectTimeFormat', () => { const result = detectTimeFormat(); - expect(result).toBe(TimeFormat.HOUR_24); + expect(result).toBe('HOUR_24'); expect(mockResolvedOptions).toHaveBeenCalled(); }); }); diff --git a/packages/twenty-front/src/modules/localization/utils/__tests__/formatDateISOStringToDateTimeSimplified.test.js b/packages/twenty-front/src/modules/localization/utils/__tests__/formatDateISOStringToDateTimeSimplified.test.js new file mode 100644 index 000000000000..4caee3aedf0d --- /dev/null +++ b/packages/twenty-front/src/modules/localization/utils/__tests__/formatDateISOStringToDateTimeSimplified.test.js @@ -0,0 +1,90 @@ +import { detectDateFormat } from '@/localization/utils/detectDateFormat'; +import { formatDateISOStringToDateTimeSimplified } from '@/localization/utils/formatDateISOStringToDateTimeSimplified'; +import { formatInTimeZone } from 'date-fns-tz'; +// Mock the imported modules +jest.mock('@/localization/utils/detectDateFormat'); +jest.mock('date-fns-tz'); + +describe('formatDateISOStringToDateTimeSimplified', () => { + const mockDate = new Date('2023-08-15T10:30:00Z'); + const mockTimeZone = 'America/New_York'; + const mockTimeFormat = 'HH:mm'; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should format the date correctly when DATE_FORMAT is MONTH_FIRST', () => { + detectDateFormat.mockReturnValue('MONTH_FIRST'); + formatInTimeZone.mockReturnValue('Oct 15 · 06:30'); + + const result = formatDateISOStringToDateTimeSimplified( + mockDate, + mockTimeZone, + mockTimeFormat, + ); + + expect(detectDateFormat).toHaveBeenCalled(); + expect(formatInTimeZone).toHaveBeenCalledWith( + mockDate, + mockTimeZone, + 'MMM d · HH:mm', + ); + expect(result).toBe('Oct 15 · 06:30'); + }); + + it('should format the date correctly when DATE_FORMAT is DAY_FIRST', () => { + detectDateFormat.mockReturnValue('DAY_FIRST'); + formatInTimeZone.mockReturnValue('15 Oct · 06:30'); + + const result = formatDateISOStringToDateTimeSimplified( + mockDate, + mockTimeZone, + mockTimeFormat, + ); + + expect(detectDateFormat).toHaveBeenCalled(); + expect(formatInTimeZone).toHaveBeenCalledWith( + mockDate, + mockTimeZone, + 'd MMM · HH:mm', + ); + expect(result).toBe('15 Oct · 06:30'); + }); + + it('should use the provided time format', () => { + detectDateFormat.mockReturnValue('MONTH_FIRST'); + formatInTimeZone.mockReturnValue('Oct 15 · 6:30 AM'); + + const result = formatDateISOStringToDateTimeSimplified( + mockDate, + mockTimeZone, + 'h:mm aa', + ); + + expect(formatInTimeZone).toHaveBeenCalledWith( + mockDate, + mockTimeZone, + 'MMM d · h:mm aa', + ); + expect(result).toBe('Oct 15 · 6:30 AM'); + }); + + it('should handle different time zones', () => { + detectDateFormat.mockReturnValue('MONTH_FIRST'); + formatInTimeZone.mockReturnValue('Oct 16 · 02:30'); + + const result = formatDateISOStringToDateTimeSimplified( + mockDate, + 'Asia/Tokyo', + mockTimeFormat, + ); + + expect(formatInTimeZone).toHaveBeenCalledWith( + mockDate, + 'Asia/Tokyo', + 'MMM d · HH:mm', + ); + expect(result).toBe('Oct 16 · 02:30'); + }); +}); diff --git a/packages/twenty-front/src/modules/localization/utils/detectDateFormat.ts b/packages/twenty-front/src/modules/localization/utils/detectDateFormat.ts index b503ef826e60..e38b018df445 100644 --- a/packages/twenty-front/src/modules/localization/utils/detectDateFormat.ts +++ b/packages/twenty-front/src/modules/localization/utils/detectDateFormat.ts @@ -1,6 +1,6 @@ import { DateFormat } from '@/localization/constants/DateFormat'; -export const detectDateFormat = (): DateFormat => { +export const detectDateFormat = (): keyof typeof DateFormat => { const date = new Date(); const formatter = new Intl.DateTimeFormat(navigator.language); const parts = formatter.formatToParts(date); @@ -9,9 +9,9 @@ export const detectDateFormat = (): DateFormat => { .filter((part) => ['year', 'month', 'day'].includes(part.type)) .map((part) => part.type); - if (partOrder[0] === 'month') return DateFormat.MONTH_FIRST; - if (partOrder[0] === 'day') return DateFormat.DAY_FIRST; - if (partOrder[0] === 'year') return DateFormat.YEAR_FIRST; + if (partOrder[0] === 'month') return 'MONTH_FIRST'; + if (partOrder[0] === 'day') return 'DAY_FIRST'; + if (partOrder[0] === 'year') return 'YEAR_FIRST'; - return DateFormat.MONTH_FIRST; + return 'MONTH_FIRST'; }; diff --git a/packages/twenty-front/src/modules/localization/utils/detectTimeFormat.ts b/packages/twenty-front/src/modules/localization/utils/detectTimeFormat.ts index 01bad17167a5..d6d914d83637 100644 --- a/packages/twenty-front/src/modules/localization/utils/detectTimeFormat.ts +++ b/packages/twenty-front/src/modules/localization/utils/detectTimeFormat.ts @@ -1,14 +1,14 @@ import { TimeFormat } from '@/localization/constants/TimeFormat'; import { isDefined } from '~/utils/isDefined'; -export const detectTimeFormat = () => { +export const detectTimeFormat = (): keyof typeof TimeFormat => { const isHour12 = Intl.DateTimeFormat(navigator.language, { hour: 'numeric', }).resolvedOptions().hour12; if (isDefined(isHour12) && isHour12) { - return TimeFormat.HOUR_12; + return 'HOUR_12'; } - return TimeFormat.HOUR_24; + return 'HOUR_24'; }; diff --git a/packages/twenty-front/src/modules/localization/utils/formatDateISOStringToDateTimeSimplified.ts b/packages/twenty-front/src/modules/localization/utils/formatDateISOStringToDateTimeSimplified.ts index ae016444e331..c96d9f2f885d 100644 --- a/packages/twenty-front/src/modules/localization/utils/formatDateISOStringToDateTimeSimplified.ts +++ b/packages/twenty-front/src/modules/localization/utils/formatDateISOStringToDateTimeSimplified.ts @@ -1,18 +1,15 @@ -import { DateFormat } from '@/localization/constants/DateFormat'; +import { DATE_FORMAT_WITHOUT_YEAR } from '@/localization/constants/DateFormatWithoutYear'; import { TimeFormat } from '@/localization/constants/TimeFormat'; +import { detectDateFormat } from '@/localization/utils/detectDateFormat'; import { formatInTimeZone } from 'date-fns-tz'; export const formatDateISOStringToDateTimeSimplified = ( date: Date, timeZone: string, - dateFormat: DateFormat, timeFormat: TimeFormat, ) => { - const simplifiedDateFormat = dateFormat - .replace(/,/g, '') - .split(' ') - .filter((part) => part !== 'yyyy') - .join(' '); + const simplifiedDateFormat = DATE_FORMAT_WITHOUT_YEAR[detectDateFormat()]; + return formatInTimeZone( date, timeZone, diff --git a/packages/twenty-front/src/modules/localization/utils/getDateFormatFromWorkspaceDateFormat.ts b/packages/twenty-front/src/modules/localization/utils/getDateFormatFromWorkspaceDateFormat.ts index f32bdbb93355..09293fbb8ec8 100644 --- a/packages/twenty-front/src/modules/localization/utils/getDateFormatFromWorkspaceDateFormat.ts +++ b/packages/twenty-front/src/modules/localization/utils/getDateFormatFromWorkspaceDateFormat.ts @@ -7,7 +7,7 @@ export const getDateFormatFromWorkspaceDateFormat = ( ) => { switch (workspaceDateFormat) { case WorkspaceMemberDateFormatEnum.System: - return detectDateFormat(); + return DateFormat[detectDateFormat()]; case WorkspaceMemberDateFormatEnum.MonthFirst: return DateFormat.MONTH_FIRST; case WorkspaceMemberDateFormatEnum.DayFirst: diff --git a/packages/twenty-front/src/modules/localization/utils/getTimeFormatFromWorkspaceTimeFormat.ts b/packages/twenty-front/src/modules/localization/utils/getTimeFormatFromWorkspaceTimeFormat.ts index f6aebb43779b..7519d0cb4068 100644 --- a/packages/twenty-front/src/modules/localization/utils/getTimeFormatFromWorkspaceTimeFormat.ts +++ b/packages/twenty-front/src/modules/localization/utils/getTimeFormatFromWorkspaceTimeFormat.ts @@ -7,7 +7,7 @@ export const getTimeFormatFromWorkspaceTimeFormat = ( ) => { switch (workspaceTimeFormat) { case WorkspaceMemberTimeFormatEnum.System: - return detectTimeFormat(); + return TimeFormat[detectTimeFormat()]; case WorkspaceMemberTimeFormatEnum.Hour_24: return TimeFormat.HOUR_24; case WorkspaceMemberTimeFormatEnum.Hour_12: diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookTooltip.tsx b/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookTooltip.tsx index fad726f38394..40925c5d3830 100644 --- a/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookTooltip.tsx +++ b/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookTooltip.tsx @@ -64,12 +64,11 @@ type SettingsDevelopersWebhookTooltipProps = { export const SettingsDevelopersWebhookTooltip = ({ point, }: SettingsDevelopersWebhookTooltipProps): ReactElement => { - const { dateFormat, timeFormat, timeZone } = useContext(UserContext); + const { timeFormat, timeZone } = useContext(UserContext); const windowInterval = new Date(point.data.x); const windowIntervalDate = formatDateISOStringToDateTimeSimplified( windowInterval, timeZone, - dateFormat, timeFormat, ); return ( diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/utils/__tests__/fetchGraphDataOrThrow.test.js b/packages/twenty-front/src/modules/settings/developers/webhook/utils/__tests__/fetchGraphDataOrThrow.test.js new file mode 100644 index 000000000000..365c964d2c97 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/developers/webhook/utils/__tests__/fetchGraphDataOrThrow.test.js @@ -0,0 +1,115 @@ +import { WEBHOOK_GRAPH_API_OPTIONS_MAP } from '@/settings/developers/webhook/constants/WebhookGraphApiOptionsMap'; +import { fetchGraphDataOrThrow } from '@/settings/developers/webhook/utils/fetchGraphDataOrThrow'; + +// Mock the global fetch function +global.fetch = jest.fn(); + +describe('fetchGraphDataOrThrow', () => { + const mockWebhookId = 'test-webhook-id'; + const mockWindowLength = '7D'; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should fetch and transform data successfully', async () => { + const mockResponse = { + ok: true, + json: jest.fn().mockResolvedValue({ + data: [ + { start_interval: '2023-05-01', failure_count: 2, success_count: 8 }, + { start_interval: '2023-05-02', failure_count: 1, success_count: 9 }, + ], + }), + }; + global.fetch.mockResolvedValue(mockResponse); + + const result = await fetchGraphDataOrThrow({ + webhookId: mockWebhookId, + windowLength: mockWindowLength, + }); + + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining( + `https://api.eu-central-1.aws.tinybird.co/v0/pipes/getWebhooksAnalyticsV2.json?`, + ), + expect.objectContaining({ + headers: { + Authorization: expect.stringContaining('Bearer '), + }, + }), + ); + + expect(result).toEqual([ + { + id: 'Failed', + color: 'red', + data: [ + { x: '2023-05-01', y: 2 }, + { x: '2023-05-02', y: 1 }, + ], + }, + { + id: 'Succeeded', + color: 'blue', + data: [ + { x: '2023-05-01', y: 8 }, + { x: '2023-05-02', y: 9 }, + ], + }, + ]); + }); + + it('should throw an error when the response is not ok', async () => { + const mockResponse = { + ok: false, + json: jest.fn().mockResolvedValue({ error: 'Some error' }), + }; + global.fetch.mockResolvedValue(mockResponse); + + await expect( + fetchGraphDataOrThrow({ + webhookId: mockWebhookId, + windowLength: mockWindowLength, + }), + ).rejects.toThrow('Something went wrong while fetching webhook usage'); + }); + + it('should use correct query parameters based on window length', async () => { + const mockResponse = { + ok: true, + json: jest.fn().mockResolvedValue({ data: [] }), + }; + global.fetch.mockResolvedValue(mockResponse); + + await fetchGraphDataOrThrow({ + webhookId: mockWebhookId, + windowLength: '1D', + }); + + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining( + new URLSearchParams({ + ...WEBHOOK_GRAPH_API_OPTIONS_MAP['1D'], + webhookIdRequest: mockWebhookId, + }).toString(), + ), + expect.any(Object), + ); + }); + + it('should handle empty response data', async () => { + const mockResponse = { + ok: true, + json: jest.fn().mockResolvedValue({ data: [] }), + }; + global.fetch.mockResolvedValue(mockResponse); + + const result = await fetchGraphDataOrThrow({ + webhookId: mockWebhookId, + windowLength: mockWindowLength, + }); + + expect(result).toEqual([]); + }); +}); diff --git a/packages/twenty-front/src/modules/users/components/UserProviderEffect.tsx b/packages/twenty-front/src/modules/users/components/UserProviderEffect.tsx index 4c51e6da446d..c41b7ff7e6cd 100644 --- a/packages/twenty-front/src/modules/users/components/UserProviderEffect.tsx +++ b/packages/twenty-front/src/modules/users/components/UserProviderEffect.tsx @@ -7,6 +7,8 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMembe import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadingState'; import { workspacesState } from '@/auth/states/workspaces'; +import { DateFormat } from '@/localization/constants/DateFormat'; +import { TimeFormat } from '@/localization/constants/TimeFormat'; import { dateTimeFormatState } from '@/localization/states/dateTimeFormatState'; import { detectDateFormat } from '@/localization/utils/detectDateFormat'; import { detectTimeFormat } from '@/localization/utils/detectTimeFormat'; @@ -81,10 +83,10 @@ export const UserProviderEffect = () => { : detectTimeZone(), dateFormat: isDefined(workspaceMember.dateFormat) ? getDateFormatFromWorkspaceDateFormat(workspaceMember.dateFormat) - : detectDateFormat(), + : DateFormat[detectDateFormat()], timeFormat: isDefined(workspaceMember.timeFormat) ? getTimeFormatFromWorkspaceTimeFormat(workspaceMember.timeFormat) - : detectTimeFormat(), + : TimeFormat[detectTimeFormat()], }); } diff --git a/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettings.tsx b/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettings.tsx index 6a2e58e52c1f..c1c20f324995 100644 --- a/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettings.tsx +++ b/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettings.tsx @@ -77,7 +77,7 @@ export const DateTimeSettings = () => { ); dateTime[settingName] = (value as DateFormat) === DateFormat.SYSTEM - ? detectDateFormat() + ? DateFormat[detectDateFormat()] : (value as DateFormat); break; } @@ -87,7 +87,7 @@ export const DateTimeSettings = () => { ); dateTime[settingName] = (value as TimeFormat) === TimeFormat.SYSTEM - ? detectTimeFormat() + ? TimeFormat[detectTimeFormat()] : (value as TimeFormat); break; } diff --git a/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettingsDateFormatSelect.tsx b/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettingsDateFormatSelect.tsx index dcadd5ba9460..2957051875aa 100644 --- a/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettingsDateFormatSelect.tsx +++ b/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettingsDateFormatSelect.tsx @@ -20,7 +20,7 @@ export const DateTimeSettingsDateFormatSelect = ({ const usedTimeZone = timeZone === 'system' ? systemTimeZone : timeZone; - const systemDateFormat = detectDateFormat(); + const systemDateFormat = DateFormat[detectDateFormat()]; return (