mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-17 03:38:32 +02:00
fix(issues): load all issues by paginating through API results
The frontend was fetching at most 200 issues in a single request, causing workspaces with more than 200 total issues to show incomplete data in status columns (especially "done"). The backend also returned len(results) as "total" instead of the actual database count. - Add CountIssues SQL query to return true total from the database - Update ListIssues handler to return the real total count - Implement auto-pagination in the issue store's fetch() to load all pages - Consolidate error-recovery fetches to use the store's paginated fetch Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
},
|
||||
[]
|
||||
|
||||
@@ -30,9 +30,20 @@ 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 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");
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
},
|
||||
[],
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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