diff --git a/apps/journeys-admin/src/components/Editor/Slider/JourneyFlow/AnalyticsOverlaySwitch/AnalyticsOverlaySwitch.spec.tsx b/apps/journeys-admin/src/components/Editor/Slider/JourneyFlow/AnalyticsOverlaySwitch/AnalyticsOverlaySwitch.spec.tsx index 74e198594f5..95bdd1537f2 100644 --- a/apps/journeys-admin/src/components/Editor/Slider/JourneyFlow/AnalyticsOverlaySwitch/AnalyticsOverlaySwitch.spec.tsx +++ b/apps/journeys-admin/src/components/Editor/Slider/JourneyFlow/AnalyticsOverlaySwitch/AnalyticsOverlaySwitch.spec.tsx @@ -1,5 +1,7 @@ import { MockedProvider } from '@apollo/client/testing' -import { render, screen, waitFor } from '@testing-library/react' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { formatISO } from 'date-fns' import { EditorProvider } from '@core/journeys/ui/EditorProvider' import { JourneyProvider } from '@core/journeys/ui/JourneyProvider' @@ -8,12 +10,22 @@ import { getJourneyAnalytics } from '@core/journeys/ui/useJourneyAnalyticsQuery/ import { GetJourney_journey } from '../../../../../../__generated__/GetJourney' import { earliestStatsCollected } from './AnalyticsOverlaySwitch' +import { buildPlausibleDateRange } from './buildPlausibleDateRange' import { AnalyticsOverlaySwitch } from '.' +jest.mock('./buildPlausibleDateRange') + +const mockBuildPlausibleDateRange = + buildPlausibleDateRange as jest.MockedFunction + const mockCurrentDate = '2024-06-02' describe('AnalyticsOverlaySwitch', () => { + beforeEach(() => { + mockBuildPlausibleDateRange.mockClear() + }) + beforeAll(() => { jest.useFakeTimers() jest.setSystemTime(new Date('2024-06-02')) @@ -24,6 +36,9 @@ describe('AnalyticsOverlaySwitch', () => { }) it('toggles showAnalytics', async () => { + mockBuildPlausibleDateRange.mockReturnValue( + `${earliestStatsCollected},${mockCurrentDate}` + ) const result = jest.fn().mockReturnValue(getJourneyAnalytics.result) const journey = { id: 'journeyId' } as unknown as GetJourney_journey const request = { @@ -78,4 +93,55 @@ describe('AnalyticsOverlaySwitch', () => { screen.getByRole('checkbox').click() expect(showAnalytics).toHaveTextContent('false') }) + + it('gets analytics for selected date range', async () => { + const selectedStartDate = new Date('2024-06-05') + const selectedEndDate = new Date('2024-06-10') + const formattedDateRange = `${formatISO(selectedStartDate, { + representation: 'date' + })},${formatISO(selectedEndDate, { representation: 'date' })}` + + // Pretend the date picker has already produced our custom range + mockBuildPlausibleDateRange.mockReturnValue(formattedDateRange) + + const result = jest.fn().mockReturnValue(getJourneyAnalytics.result) + const journey = { id: 'journeyId' } as unknown as GetJourney_journey + + render( + + + + {({ state: { analytics } }) => ( + <> +
{JSON.stringify(analytics)}
+ + + )} +
+
+
+ ) + + const analyticsCheckbox = screen.getByRole('checkbox') + fireEvent.click(analyticsCheckbox) + + // Verify the network call was made with the mocked date range + await waitFor(() => { + expect(result).toHaveBeenCalled() + }) + }) }) diff --git a/apps/journeys-admin/src/components/Editor/Slider/JourneyFlow/AnalyticsOverlaySwitch/AnalyticsOverlaySwitch.tsx b/apps/journeys-admin/src/components/Editor/Slider/JourneyFlow/AnalyticsOverlaySwitch/AnalyticsOverlaySwitch.tsx index aebf69cd997..34055eb3876 100644 --- a/apps/journeys-admin/src/components/Editor/Slider/JourneyFlow/AnalyticsOverlaySwitch/AnalyticsOverlaySwitch.tsx +++ b/apps/journeys-admin/src/components/Editor/Slider/JourneyFlow/AnalyticsOverlaySwitch/AnalyticsOverlaySwitch.tsx @@ -1,15 +1,19 @@ import FormControlLabel from '@mui/material/FormControlLabel' +import Stack from '@mui/material/Stack' import Switch from '@mui/material/Switch' import Typography from '@mui/material/Typography' -import { formatISO } from 'date-fns' import { useTranslation } from 'next-i18next' import { enqueueSnackbar } from 'notistack' -import { ReactElement } from 'react' +import { ReactElement, useState } from 'react' import { useEditor } from '@core/journeys/ui/EditorProvider' import { useJourney } from '@core/journeys/ui/JourneyProvider' import { useJourneyAnalyticsQuery } from '@core/journeys/ui/useJourneyAnalyticsQuery' +import { DateRangePicker } from '../../../../JourneyVisitorsList/FilterDrawer/ExportDialog/DateRangePicker' + +import { buildPlausibleDateRange } from './buildPlausibleDateRange' + // Used to for filter all time stats export const earliestStatsCollected = '2024-06-01' @@ -20,15 +24,32 @@ export function AnalyticsOverlaySwitch(): ReactElement { state: { showAnalytics }, dispatch } = useEditor() - const currentDate = formatISO(new Date(), { representation: 'date' }) + + const [startDate, setStartDate] = useState( + new Date(earliestStatsCollected) + ) + const [endDate, setEndDate] = useState(new Date()) + + const isDateRangeValid = + startDate != null && endDate != null && startDate <= endDate + + const formattedDateRange = buildPlausibleDateRange( + startDate, + endDate, + earliestStatsCollected, + new Date() + ) useJourneyAnalyticsQuery({ variables: { id: journey?.id ?? '', period: 'custom', - date: `${earliestStatsCollected},${currentDate}` + date: formattedDateRange }, - skip: journey?.id == null || showAnalytics !== true, + skip: + journey?.id == null || + showAnalytics !== true || + isDateRangeValid !== true, onCompleted: (analytics) => { dispatch({ type: 'SetAnalyticsAction', @@ -36,6 +57,13 @@ export function AnalyticsOverlaySwitch(): ReactElement { }) }, onError: (_) => { + if (isDateRangeValid) { + enqueueSnackbar(t('Invalid date range'), { + variant: 'error', + preventDuplicate: true + }) + return + } enqueueSnackbar(t('Error fetching analytics'), { variant: 'error', preventDuplicate: true @@ -51,17 +79,27 @@ export function AnalyticsOverlaySwitch(): ReactElement { } return ( - + + } + label={ + {t('Analytics Overlay')} + } + labelPlacement="start" + /> + {showAnalytics && ( + - } - label={ - {t('Analytics Overlay')} - } - labelPlacement="start" - /> + )} + ) } diff --git a/apps/journeys-admin/src/components/Editor/Slider/JourneyFlow/AnalyticsOverlaySwitch/buildPlausibleDateRange/buildPlausibleDateRange.spec.ts b/apps/journeys-admin/src/components/Editor/Slider/JourneyFlow/AnalyticsOverlaySwitch/buildPlausibleDateRange/buildPlausibleDateRange.spec.ts new file mode 100644 index 00000000000..dc0062b21e8 --- /dev/null +++ b/apps/journeys-admin/src/components/Editor/Slider/JourneyFlow/AnalyticsOverlaySwitch/buildPlausibleDateRange/buildPlausibleDateRange.spec.ts @@ -0,0 +1,54 @@ +// Ensure we use the real formatISO implementation, not a mock from other tests +import { formatISO } from 'date-fns' + +import { buildPlausibleDateRange } from './buildPlausibleDateRange' + +describe('buildPlausibleDateRange', () => { + const fallbackStart = '2024-06-01' + const fallbackEnd = new Date('2024-06-10') + + it('returns formatted range when start and end dates are valid', () => { + const startDate = new Date('2024-06-02') + const endDate = new Date('2024-06-05') + + const result = buildPlausibleDateRange( + startDate, + endDate, + fallbackStart, + fallbackEnd + ) + + expect(result).toBe( + `${formatISO(startDate, { representation: 'date' })},${formatISO(endDate, { representation: 'date' })}` + ) + }) + + it('falls back when dates are null', () => { + const result = buildPlausibleDateRange( + null, + null, + fallbackStart, + fallbackEnd + ) + + expect(result).toBe( + `${fallbackStart},${formatISO(fallbackEnd, { representation: 'date' })}` + ) + }) + + it('falls back when start date is after end date', () => { + const startDate = new Date('2024-06-06') + const endDate = new Date('2024-06-05') + + const result = buildPlausibleDateRange( + startDate, + endDate, + fallbackStart, + fallbackEnd + ) + + expect(result).toBe( + `${formatISO(fallbackStart, { representation: 'date' })},${formatISO(fallbackEnd, { representation: 'date' })}` + ) + }) +}) diff --git a/apps/journeys-admin/src/components/Editor/Slider/JourneyFlow/AnalyticsOverlaySwitch/buildPlausibleDateRange/buildPlausibleDateRange.ts b/apps/journeys-admin/src/components/Editor/Slider/JourneyFlow/AnalyticsOverlaySwitch/buildPlausibleDateRange/buildPlausibleDateRange.ts new file mode 100644 index 00000000000..5dc64a02b51 --- /dev/null +++ b/apps/journeys-admin/src/components/Editor/Slider/JourneyFlow/AnalyticsOverlaySwitch/buildPlausibleDateRange/buildPlausibleDateRange.ts @@ -0,0 +1,18 @@ +import { formatISO } from 'date-fns' + +export function buildPlausibleDateRange( + startDate: Date | null, + endDate: Date | null, + fallbackStartDate: string, + fallbackEndDate: Date +): string { + if (startDate != null && endDate != null && startDate <= endDate) { + return `${formatISO(startDate, { + representation: 'date' + })},${formatISO(endDate, { representation: 'date' })}` + } + + return `${fallbackStartDate},${formatISO(fallbackEndDate, { + representation: 'date' + })}` +} diff --git a/apps/journeys-admin/src/components/Editor/Slider/JourneyFlow/AnalyticsOverlaySwitch/buildPlausibleDateRange/index.ts b/apps/journeys-admin/src/components/Editor/Slider/JourneyFlow/AnalyticsOverlaySwitch/buildPlausibleDateRange/index.ts new file mode 100644 index 00000000000..51f70313f8b --- /dev/null +++ b/apps/journeys-admin/src/components/Editor/Slider/JourneyFlow/AnalyticsOverlaySwitch/buildPlausibleDateRange/index.ts @@ -0,0 +1 @@ +export { buildPlausibleDateRange } from './buildPlausibleDateRange' diff --git a/libs/locales/en/apps-journeys-admin.json b/libs/locales/en/apps-journeys-admin.json index 36e046eab01..edbc7dc53c8 100644 --- a/libs/locales/en/apps-journeys-admin.json +++ b/libs/locales/en/apps-journeys-admin.json @@ -124,6 +124,7 @@ "Headline": "Headline", "Secondary Text": "Secondary Text", "Social Post View": "Social Post View", + "Invalid date range": "Invalid date range", "Error fetching analytics": "Error fetching analytics", "Analytics Overlay": "Analytics Overlay", "Zoom in": "Zoom in",