diff --git a/apps/web/features/issues/components/batch-action-toolbar.tsx b/apps/web/features/issues/components/batch-action-toolbar.tsx index 9b259e3e8..7ac9905f1 100644 --- a/apps/web/features/issues/components/batch-action-toolbar.tsx +++ b/apps/web/features/issues/components/batch-action-toolbar.tsx @@ -53,9 +53,7 @@ export function BatchActionToolbar() { toast.success(`Updated ${count} issue${count > 1 ? "s" : ""}`); } catch { toast.error("Failed to update issues"); - api.listIssues({ limit: 200 }).then((res) => { - useIssueStore.getState().setIssues(res.issues); - }).catch(console.error); + useIssueStore.getState().fetch().catch(console.error); } finally { setLoading(false); } @@ -72,9 +70,7 @@ export function BatchActionToolbar() { toast.success(`Deleted ${count} issue${count > 1 ? "s" : ""}`); } catch { toast.error("Failed to delete issues"); - api.listIssues({ limit: 200 }).then((res) => { - useIssueStore.getState().setIssues(res.issues); - }).catch(console.error); + useIssueStore.getState().fetch().catch(console.error); } finally { setLoading(false); setDeleteOpen(false); diff --git a/apps/web/features/issues/components/issues-page.tsx b/apps/web/features/issues/components/issues-page.tsx index aa0709405..56a335809 100644 --- a/apps/web/features/issues/components/issues-page.tsx +++ b/apps/web/features/issues/components/issues-page.tsx @@ -82,9 +82,7 @@ export function IssuesPage() { api.updateIssue(issueId, updates).catch(() => { toast.error("Failed to move issue"); - api.listIssues({ limit: 200 }).then((res) => { - useIssueStore.getState().setIssues(res.issues); - }).catch(console.error); + useIssueStore.getState().fetch().catch(console.error); }); }, [] diff --git a/apps/web/features/issues/store.ts b/apps/web/features/issues/store.ts index 1e47b7d73..104803558 100644 --- a/apps/web/features/issues/store.ts +++ b/apps/web/features/issues/store.ts @@ -30,9 +30,20 @@ export const useIssueStore = create((set, get) => ({ const isInitialLoad = get().issues.length === 0; if (isInitialLoad) set({ loading: true }); try { - const res = await api.listIssues({ limit: 200 }); - logger.info("fetched", res.issues.length, "issues"); - set({ issues: res.issues, loading: false }); + const pageSize = 200; + let offset = 0; + let allIssues: Issue[] = []; + let total = 0; + // Fetch all pages + for (;;) { + const res = await api.listIssues({ limit: pageSize, offset }); + allIssues = allIssues.concat(res.issues); + total = res.total; + if (allIssues.length >= total || res.issues.length < pageSize) break; + offset += pageSize; + } + logger.info("fetched", allIssues.length, "of", total, "issues"); + set({ issues: allIssues, loading: false }); } catch (err) { logger.error("fetch failed", err); toast.error("Failed to load issues"); diff --git a/apps/web/features/my-issues/components/my-issues-page.tsx b/apps/web/features/my-issues/components/my-issues-page.tsx index ac7b13ba7..ba40393f5 100644 --- a/apps/web/features/my-issues/components/my-issues-page.tsx +++ b/apps/web/features/my-issues/components/my-issues-page.tsx @@ -122,9 +122,7 @@ export function MyIssuesPage() { api.updateIssue(issueId, updates).catch(() => { toast.error("Failed to move issue"); - api.listIssues({ limit: 200 }).then((res) => { - useIssueStore.getState().setIssues(res.issues); - }).catch(console.error); + useIssueStore.getState().fetch().catch(console.error); }); }, [], diff --git a/server/internal/handler/issue.go b/server/internal/handler/issue.go index 418252e24..620f2ac0e 100644 --- a/server/internal/handler/issue.go +++ b/server/internal/handler/issue.go @@ -111,8 +111,10 @@ func (h *Handler) ListIssues(w http.ResponseWriter, r *http.Request) { assigneeFilter = parseUUID(a) } + wsUUID := parseUUID(workspaceID) + issues, err := h.Queries.ListIssues(ctx, db.ListIssuesParams{ - WorkspaceID: parseUUID(workspaceID), + WorkspaceID: wsUUID, Limit: int32(limit), Offset: int32(offset), Status: statusFilter, @@ -124,7 +126,18 @@ func (h *Handler) ListIssues(w http.ResponseWriter, r *http.Request) { return } - prefix := h.getIssuePrefix(ctx, parseUUID(workspaceID)) + total, err := h.Queries.CountIssues(ctx, db.CountIssuesParams{ + WorkspaceID: wsUUID, + Status: statusFilter, + Priority: priorityFilter, + AssigneeID: assigneeFilter, + }) + if err != nil { + writeError(w, http.StatusInternalServerError, "failed to count issues") + return + } + + prefix := h.getIssuePrefix(ctx, wsUUID) resp := make([]IssueResponse, len(issues)) for i, issue := range issues { resp[i] = issueToResponse(issue, prefix) @@ -132,7 +145,7 @@ func (h *Handler) ListIssues(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]any{ "issues": resp, - "total": len(resp), + "total": total, }) } diff --git a/server/pkg/db/generated/issue.sql.go b/server/pkg/db/generated/issue.sql.go index f899eb6eb..95b15a244 100644 --- a/server/pkg/db/generated/issue.sql.go +++ b/server/pkg/db/generated/issue.sql.go @@ -11,6 +11,33 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +const countIssues = `-- name: CountIssues :one +SELECT count(*) FROM issue +WHERE workspace_id = $1 + AND ($2::text IS NULL OR status = $2) + AND ($3::text IS NULL OR priority = $3) + AND ($4::uuid IS NULL OR assignee_id = $4) +` + +type CountIssuesParams struct { + WorkspaceID pgtype.UUID `json:"workspace_id"` + Status pgtype.Text `json:"status"` + Priority pgtype.Text `json:"priority"` + AssigneeID pgtype.UUID `json:"assignee_id"` +} + +func (q *Queries) CountIssues(ctx context.Context, arg CountIssuesParams) (int64, error) { + row := q.db.QueryRow(ctx, countIssues, + arg.WorkspaceID, + arg.Status, + arg.Priority, + arg.AssigneeID, + ) + var count int64 + err := row.Scan(&count) + return count, err +} + const createIssue = `-- name: CreateIssue :one INSERT INTO issue ( workspace_id, title, description, status, priority, diff --git a/server/pkg/db/queries/issue.sql b/server/pkg/db/queries/issue.sql index edc229c31..ce89cce73 100644 --- a/server/pkg/db/queries/issue.sql +++ b/server/pkg/db/queries/issue.sql @@ -7,6 +7,13 @@ WHERE workspace_id = $1 ORDER BY position ASC, created_at DESC LIMIT $2 OFFSET $3; +-- name: CountIssues :one +SELECT count(*) FROM issue +WHERE workspace_id = $1 + AND (sqlc.narg('status')::text IS NULL OR status = sqlc.narg('status')) + AND (sqlc.narg('priority')::text IS NULL OR priority = sqlc.narg('priority')) + AND (sqlc.narg('assignee_id')::uuid IS NULL OR assignee_id = sqlc.narg('assignee_id')); + -- name: GetIssue :one SELECT * FROM issue WHERE id = $1;