From 52cb2b6c77bc3912b356887bdd48e9c8b9b1728a Mon Sep 17 00:00:00 2001 From: Dries Augustyns Date: Mon, 13 Apr 2026 17:06:22 +0200 Subject: [PATCH] fix: Refactor date filtering logic for pagination in ActivityFeed and ActivityService --- apps/api/src/services/ActivityService.ts | 14 +++++++++++++- apps/web/src/components/ActivityFeed.tsx | 17 +++++++++-------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/apps/api/src/services/ActivityService.ts b/apps/api/src/services/ActivityService.ts index 9a66e38..b4f8a6e 100644 --- a/apps/api/src/services/ActivityService.ts +++ b/apps/api/src/services/ActivityService.ts @@ -67,10 +67,14 @@ export class ActivityService { const fetchLimit = effectiveLimit; // Default date range to last 30 days if not specified + // IMPORTANT: When cursor is provided (pagination), we should NOT apply the gte constraint + // to allow users to paginate back beyond the initial date range const now = new Date(); const defaultStartDate = new Date(now.getTime() - this.DEFAULT_DAYS_BACK * 24 * 60 * 60 * 1000); const dateFilter: Prisma.DateTimeFilter = { - gte: startDate || defaultStartDate, + // Only apply start date filter on initial load (no cursor) + // This allows pagination to go back indefinitely + ...(cursor ? {} : {gte: startDate || defaultStartDate}), ...(endDate ? {lte: endDate} : {}), }; @@ -364,6 +368,14 @@ export class ActivityService { const where: Prisma.EmailWhereInput = { projectId, ...(contactId ? {contactId} : {}), + // Apply cursor-based pagination filter on createdAt + // This is critical for pagination to work correctly + createdAt: cursorTimestamp + ? { + ...dateFilter, + lt: cursorTimestamp, + } + : dateFilter, }; // Build OR conditions to filter by the appropriate timestamp field for each activity type diff --git a/apps/web/src/components/ActivityFeed.tsx b/apps/web/src/components/ActivityFeed.tsx index 26d2100..2634175 100644 --- a/apps/web/src/components/ActivityFeed.tsx +++ b/apps/web/src/components/ActivityFeed.tsx @@ -27,7 +27,7 @@ export function ActivityFeed({typeFilter, dateRangeDays = 30, contactId}: Activi return date.toISOString(); }, [dateRangeDays]); - const fetchActivities = useCallback( + const fetchActivities = useCallback( async (cursor?: string) => { try { if (cursor) { @@ -43,9 +43,14 @@ export function ActivityFeed({typeFilter, dateRangeDays = 30, contactId}: Activi const params = new URLSearchParams({ limit: '20', // Conservative limit to avoid overloading - startDate: startDate, }); + // Only apply startDate filter on initial load, not during pagination + // When cursor is present, we're paginating backwards and should not limit by startDate + if (!cursor) { + params.set('startDate', startDate); + } + if (cursor) { params.set('cursor', cursor); } @@ -74,9 +79,7 @@ export function ActivityFeed({typeFilter, dateRangeDays = 30, contactId}: Activi // For each new activity, reuse existing object if ID matches (preserves React component instances) // Otherwise use new object. This maintains correct ordering while preserving component state. - return result.data.map(newActivity => - existingMap.get(newActivity.id) ?? newActivity - ); + return result.data.map(newActivity => existingMap.get(newActivity.id) ?? newActivity); }); } @@ -121,9 +124,7 @@ export function ActivityFeed({typeFilter, dateRangeDays = 30, contactId}: Activi const existingMap = new Map(prev.map(activity => [activity.id, activity])); // For each new activity, reuse existing object if ID matches - return result.activities.map(newActivity => - existingMap.get(newActivity.id) ?? newActivity - ); + return result.activities.map(newActivity => existingMap.get(newActivity.id) ?? newActivity); }); } catch (err) { console.error('Error fetching upcoming activities:', err);