fix: Refactor date filtering logic for pagination in ActivityFeed and ActivityService

This commit is contained in:
Dries Augustyns
2026-04-13 17:06:22 +02:00
parent 7834b9e7ef
commit 52cb2b6c77
2 changed files with 22 additions and 9 deletions
+13 -1
View File
@@ -67,10 +67,14 @@ export class ActivityService {
const fetchLimit = effectiveLimit; const fetchLimit = effectiveLimit;
// Default date range to last 30 days if not specified // 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 now = new Date();
const defaultStartDate = new Date(now.getTime() - this.DEFAULT_DAYS_BACK * 24 * 60 * 60 * 1000); const defaultStartDate = new Date(now.getTime() - this.DEFAULT_DAYS_BACK * 24 * 60 * 60 * 1000);
const dateFilter: Prisma.DateTimeFilter = { 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} : {}), ...(endDate ? {lte: endDate} : {}),
}; };
@@ -364,6 +368,14 @@ export class ActivityService {
const where: Prisma.EmailWhereInput = { const where: Prisma.EmailWhereInput = {
projectId, projectId,
...(contactId ? {contactId} : {}), ...(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 // Build OR conditions to filter by the appropriate timestamp field for each activity type
+9 -8
View File
@@ -27,7 +27,7 @@ export function ActivityFeed({typeFilter, dateRangeDays = 30, contactId}: Activi
return date.toISOString(); return date.toISOString();
}, [dateRangeDays]); }, [dateRangeDays]);
const fetchActivities = useCallback( const fetchActivities = useCallback(
async (cursor?: string) => { async (cursor?: string) => {
try { try {
if (cursor) { if (cursor) {
@@ -43,9 +43,14 @@ export function ActivityFeed({typeFilter, dateRangeDays = 30, contactId}: Activi
const params = new URLSearchParams({ const params = new URLSearchParams({
limit: '20', // Conservative limit to avoid overloading 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) { if (cursor) {
params.set('cursor', 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) // 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. // Otherwise use new object. This maintains correct ordering while preserving component state.
return result.data.map(newActivity => return result.data.map(newActivity => existingMap.get(newActivity.id) ?? 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])); const existingMap = new Map(prev.map(activity => [activity.id, activity]));
// For each new activity, reuse existing object if ID matches // For each new activity, reuse existing object if ID matches
return result.activities.map(newActivity => return result.activities.map(newActivity => existingMap.get(newActivity.id) ?? newActivity);
existingMap.get(newActivity.id) ?? newActivity
);
}); });
} catch (err) { } catch (err) {
console.error('Error fetching upcoming activities:', err); console.error('Error fetching upcoming activities:', err);