fix(web,server): load all issues by paginating and returning true total count

The ListIssues API returned len(resp) as "total" instead of the actual
database count, and the frontend fetched at most 200 issues with no
pagination. Workspaces with 200+ issues showed incomplete data in status
columns (e.g. done list missing items).

Backend: add CountIssues query to return the true filtered count.
Frontend: auto-paginate in fetch() until all issues are loaded.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
yushen
2026-04-08 13:45:47 +08:00
parent 52a9a6ae5f
commit fa01db1411
4 changed files with 58 additions and 4 deletions

View File

@@ -30,9 +30,18 @@ export const useIssueStore = create<IssueState>((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 allIssues: Issue[] = [];
let offset = 0;
let total = 0;
do {
const res = await api.listIssues({ limit: pageSize, offset });
allIssues = allIssues.concat(res.issues);
total = res.total;
offset += res.issues.length;
} while (allIssues.length < total);
logger.info("fetched", allIssues.length, "issues");
set({ issues: allIssues, loading: false });
} catch (err) {
logger.error("fetch failed", err);
toast.error("Failed to load issues");

View File

@@ -124,6 +124,17 @@ func (h *Handler) ListIssues(w http.ResponseWriter, r *http.Request) {
return
}
total, err := h.Queries.CountIssues(ctx, db.CountIssuesParams{
WorkspaceID: parseUUID(workspaceID),
Status: statusFilter,
Priority: priorityFilter,
AssigneeID: assigneeFilter,
})
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to count issues")
return
}
prefix := h.getIssuePrefix(ctx, parseUUID(workspaceID))
resp := make([]IssueResponse, len(issues))
for i, issue := range issues {
@@ -132,7 +143,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,
})
}

View File

@@ -189,6 +189,33 @@ func (q *Queries) GetIssueInWorkspace(ctx context.Context, arg GetIssueInWorkspa
return i, err
}
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 listIssues = `-- name: ListIssues :many
SELECT id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number FROM issue
WHERE workspace_id = $1

View File

@@ -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;