mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-17 15:19:00 +02:00
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:
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user