Skip to content

Commit 0bdcfe2

Browse files
committed
feat: Enhance filtering and pagination capabilities in Devlog API and context
1 parent 7506883 commit 0bdcfe2

File tree

6 files changed

+233
-140
lines changed

6 files changed

+233
-140
lines changed

packages/core/src/services/devlog-service.ts

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -526,25 +526,83 @@ export class DevlogService {
526526
}
527527

528528
// Apply status filter
529-
if (projectFilter.status) {
529+
if (projectFilter.status && projectFilter.status.length > 0) {
530530
queryBuilder.andWhere('devlog.status IN (:...statuses)', { statuses: projectFilter.status });
531531
}
532532

533+
// Apply type filter
534+
if (projectFilter.type && projectFilter.type.length > 0) {
535+
queryBuilder.andWhere('devlog.type IN (:...types)', { types: projectFilter.type });
536+
}
537+
533538
// Apply priority filter
534-
if (projectFilter.priority) {
539+
if (projectFilter.priority && projectFilter.priority.length > 0) {
535540
queryBuilder.andWhere('devlog.priority IN (:...priorities)', {
536541
priorities: projectFilter.priority,
537542
});
538543
}
539544

540-
// Apply pagination
541-
const pagination = projectFilter.pagination || { page: 1, limit: 20 };
545+
// Apply assignee filter
546+
if (projectFilter.assignee !== undefined) {
547+
if (projectFilter.assignee === null) {
548+
queryBuilder.andWhere('devlog.assignee IS NULL');
549+
} else {
550+
queryBuilder.andWhere('devlog.assignee = :assignee', { assignee: projectFilter.assignee });
551+
}
552+
}
553+
554+
// Apply archived filter
555+
if (projectFilter.archived !== undefined) {
556+
queryBuilder.andWhere('devlog.archived = :archived', { archived: projectFilter.archived });
557+
}
558+
559+
// Apply date range filters
560+
if (projectFilter.fromDate) {
561+
queryBuilder.andWhere('devlog.createdAt >= :fromDate', { fromDate: projectFilter.fromDate });
562+
}
563+
if (projectFilter.toDate) {
564+
queryBuilder.andWhere('devlog.createdAt <= :toDate', { toDate: projectFilter.toDate });
565+
}
566+
567+
// Apply search filter (if not already applied by search method)
568+
if (projectFilter.search && !queryBuilder.getQueryAndParameters()[0].includes('LIKE')) {
569+
queryBuilder.andWhere(
570+
'(devlog.title LIKE :search OR devlog.description LIKE :search OR devlog.businessContext LIKE :search OR devlog.technicalContext LIKE :search)',
571+
{ search: `%${projectFilter.search}%` },
572+
);
573+
}
574+
575+
// Apply pagination and sorting
576+
const pagination = projectFilter.pagination || {
577+
page: 1,
578+
limit: 20,
579+
sortBy: 'updatedAt',
580+
sortOrder: 'desc',
581+
};
542582
const page = pagination.page || 1;
543583
const limit = pagination.limit || 20;
544584
const offset = (page - 1) * limit;
585+
const sortBy = pagination.sortBy || 'updatedAt';
586+
const sortOrder = pagination.sortOrder || 'desc';
545587

546588
queryBuilder.skip(offset).take(limit);
547-
queryBuilder.orderBy('devlog.updatedAt', 'DESC');
589+
590+
// Apply sorting
591+
const validSortColumns = [
592+
'id',
593+
'title',
594+
'type',
595+
'status',
596+
'priority',
597+
'createdAt',
598+
'updatedAt',
599+
'closedAt',
600+
];
601+
if (validSortColumns.includes(sortBy)) {
602+
queryBuilder.orderBy(`devlog.${sortBy}`, sortOrder.toUpperCase() as 'ASC' | 'DESC');
603+
} else {
604+
queryBuilder.orderBy('devlog.updatedAt', 'DESC');
605+
}
548606

549607
const [entities, total] = await queryBuilder.getManyAndCount();
550608
const entries = entities.map((entity) => entity.toDevlogEntry());

packages/web/app/api/projects/[id]/devlogs/route.ts

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,38 @@ export async function GET(request: NextRequest, { params }: { params: { id: stri
5151
if (queryData.priority) filter.priority = [queryData.priority];
5252
if (queryData.assignee) filter.assignee = queryData.assignee;
5353
if (queryData.archived !== undefined) filter.archived = queryData.archived;
54-
55-
// Pagination
56-
if (queryData.limit || queryData.offset) {
57-
filter.pagination = {
58-
page: queryData.offset ? Math.floor(queryData.offset / (queryData.limit || 20)) + 1 : 1,
59-
limit: queryData.limit || 20,
60-
};
54+
if (queryData.fromDate) filter.fromDate = queryData.fromDate;
55+
if (queryData.toDate) filter.toDate = queryData.toDate;
56+
if (queryData.search) filter.search = queryData.search;
57+
58+
// Handle special filter types for backwards compatibility
59+
if (queryData.filterType) {
60+
switch (queryData.filterType) {
61+
case 'open':
62+
filter.status = ['new', 'in-progress', 'blocked', 'in-review', 'testing'];
63+
break;
64+
case 'closed':
65+
filter.status = ['done', 'cancelled'];
66+
break;
67+
case 'total':
68+
// No status filter - show all
69+
break;
70+
}
6171
}
6272

73+
// Pagination - support both offset/limit and page-based pagination
74+
const page =
75+
queryData.page ||
76+
(queryData.offset ? Math.floor(queryData.offset / (queryData.limit || 20)) + 1 : 1);
77+
const limit = queryData.limit || 20;
78+
79+
filter.pagination = {
80+
page,
81+
limit,
82+
sortBy: queryData.sortBy || 'updatedAt',
83+
sortOrder: queryData.sortOrder || 'desc',
84+
};
85+
6386
let result;
6487
if (queryData.search) {
6588
result = await devlogService.search(queryData.search, filter);

packages/web/app/contexts/DevlogContext.tsx

Lines changed: 32 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -158,16 +158,38 @@ export function DevlogProvider({ children }: { children: React.ReactNode }) {
158158
try {
159159
setLoading(true);
160160

161-
// Convert query filters to DevlogApiClient filters format
161+
// Convert filters to DevlogFilters format for the API client
162162
const apiFilters: any = {};
163163

164+
// Convert array filters to single values (API expects single values currently)
165+
if (filters.status && filters.status.length > 0) {
166+
apiFilters.status = filters.status[0];
167+
}
168+
if (filters.type && filters.type.length > 0) {
169+
apiFilters.type = filters.type[0];
170+
}
171+
if (filters.priority && filters.priority.length > 0) {
172+
apiFilters.priority = filters.priority[0];
173+
}
174+
175+
// Direct mappings
164176
if (filters.search) apiFilters.search = filters.search;
165-
if (filters.status?.length === 1) apiFilters.status = filters.status[0];
166-
if (filters.type?.length === 1) apiFilters.type = filters.type[0];
167-
if (filters.priority?.length === 1) apiFilters.priority = filters.priority[0];
168-
if (filters.pagination?.limit) apiFilters.limit = filters.pagination.limit;
169-
if (filters.pagination?.page && filters.pagination?.limit) {
170-
apiFilters.offset = (filters.pagination.page - 1) * filters.pagination.limit;
177+
if (filters.assignee) apiFilters.assignee = filters.assignee;
178+
if (filters.archived !== undefined) apiFilters.archived = filters.archived;
179+
if (filters.fromDate) apiFilters.fromDate = filters.fromDate;
180+
if (filters.toDate) apiFilters.toDate = filters.toDate;
181+
182+
// Handle filterType - only pass through valid values
183+
if (filters.filterType && ['total', 'open', 'closed'].includes(filters.filterType)) {
184+
apiFilters.filterType = filters.filterType;
185+
}
186+
187+
// Pagination
188+
if (filters.pagination) {
189+
if (filters.pagination.page) apiFilters.page = filters.pagination.page;
190+
if (filters.pagination.limit) apiFilters.limit = filters.pagination.limit;
191+
if (filters.pagination.sortBy) apiFilters.sortBy = filters.pagination.sortBy;
192+
if (filters.pagination.sortOrder) apiFilters.sortOrder = filters.pagination.sortOrder;
171193
}
172194

173195
const data = await devlogApiClient.list(apiFilters);
@@ -227,54 +249,10 @@ export function DevlogProvider({ children }: { children: React.ReactNode }) {
227249
}
228250
}, [currentProject, devlogApiClient]);
229251

230-
// Client-side filtered devlogs
252+
// All filtering is now handled server-side - simply return the devlogs from API
231253
const filteredDevlogs = useMemo(() => {
232-
if (queryString) {
233-
return devlogs;
234-
}
235-
236-
let filtered = [...devlogs];
237-
238-
if (filters.status && filters.status.length > 0) {
239-
filtered = filtered.filter((devlog) => filters.status!.includes(devlog.status));
240-
}
241-
242-
if (filters.type && filters.type.length > 0) {
243-
filtered = filtered.filter((devlog) => filters.type!.includes(devlog.type));
244-
}
245-
246-
if (filters.priority && filters.priority.length > 0) {
247-
filtered = filtered.filter((devlog) => filters.priority!.includes(devlog.priority));
248-
}
249-
250-
if (filters.assignee) {
251-
filtered = filtered.filter((devlog) => devlog.assignee === filters.assignee);
252-
}
253-
254-
if (filters.fromDate) {
255-
const fromDate = new Date(filters.fromDate);
256-
fromDate.setHours(0, 0, 0, 0);
257-
filtered = filtered.filter((devlog) => new Date(devlog.createdAt) >= fromDate);
258-
}
259-
260-
if (filters.toDate) {
261-
const toDate = new Date(filters.toDate);
262-
toDate.setHours(23, 59, 59, 999);
263-
filtered = filtered.filter((devlog) => new Date(devlog.createdAt) <= toDate);
264-
}
265-
266-
if (filters.search) {
267-
const searchQuery = filters.search.toLowerCase().trim();
268-
filtered = filtered.filter((devlog) => {
269-
const titleMatch = devlog.title.toLowerCase().includes(searchQuery);
270-
const descriptionMatch = devlog.description.toLowerCase().includes(searchQuery);
271-
// Note: Search in notes is handled separately by the notes API
272-
return titleMatch || descriptionMatch;
273-
});
274-
}
275-
276-
return filtered;
277-
}, [devlogs, filters, queryString]);
254+
return devlogs;
255+
}, [devlogs]);
278256

279257
// CRUD operations
280258
const createDevlog = async (data: Partial<DevlogEntry>) => {

0 commit comments

Comments
 (0)