diff --git a/apps/api/internal/handler/issue.go b/apps/api/internal/handler/issue.go index c562294..ea8dd21 100644 --- a/apps/api/internal/handler/issue.go +++ b/apps/api/internal/handler/issue.go @@ -49,8 +49,10 @@ func (h *IssueHandler) ListWorkspaceDrafts(c *gin.Context) { slug := c.Param("slug") limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50")) offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0")) - if limit <= 0 || limit > 100 { + if limit <= 0 { limit = 50 + } else if limit > 100 { + limit = 100 } list, err := h.Issue.ListDraftsForWorkspace(c.Request.Context(), slug, user.ID, limit, offset) if err != nil { @@ -75,8 +77,10 @@ func (h *IssueHandler) ListWorkspaceArchived(c *gin.Context) { slug := c.Param("slug") limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50")) offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0")) - if limit <= 0 || limit > 100 { + if limit <= 0 { limit = 50 + } else if limit > 100 { + limit = 100 } list, err := h.Issue.ListArchivedForWorkspace(c.Request.Context(), slug, user.ID, limit, offset) if err != nil { @@ -106,8 +110,10 @@ func (h *IssueHandler) List(c *gin.Context) { } limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50")) offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0")) - if limit <= 0 || limit > 100 { + if limit <= 0 { limit = 50 + } else if limit > 100 { + limit = 100 } list, err := h.Issue.List(c.Request.Context(), slug, projectID, user.ID, limit, offset) if err != nil { diff --git a/apps/api/internal/handler/issue_archive.go b/apps/api/internal/handler/issue_archive.go index eaea67c..d6ce2b6 100644 --- a/apps/api/internal/handler/issue_archive.go +++ b/apps/api/internal/handler/issue_archive.go @@ -175,8 +175,10 @@ func (h *IssueHandler) ListArchived(c *gin.Context) { } limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50")) offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0")) - if limit <= 0 || limit > 100 { + if limit <= 0 { limit = 50 + } else if limit > 100 { + limit = 100 } list, err := h.Issue.ListArchived(c.Request.Context(), slug, projectID, user.ID, limit, offset) if err != nil { diff --git a/apps/web/src/components/AddExistingWorkItemModal.tsx b/apps/web/src/components/AddExistingWorkItemModal.tsx index c15962c..42ed294 100644 --- a/apps/web/src/components/AddExistingWorkItemModal.tsx +++ b/apps/web/src/components/AddExistingWorkItemModal.tsx @@ -59,7 +59,7 @@ export function AddExistingWorkItemModal({ setLoading(true); setError(null); Promise.all([ - issueService.list(workspaceSlug, projectId, { limit: 2000 }), + issueService.listAll(workspaceSlug, projectId), moduleService.listIssueIds(workspaceSlug, projectId, moduleId), ]) .then(([issues, ids]) => { diff --git a/apps/web/src/components/layout/PageHeader.tsx b/apps/web/src/components/layout/PageHeader.tsx index 112cea0..e5f64bc 100644 --- a/apps/web/src/components/layout/PageHeader.tsx +++ b/apps/web/src/components/layout/PageHeader.tsx @@ -92,7 +92,7 @@ export function PageHeader() { .get(workspaceSlug, projectId) .then((p) => { if (!cancelled) setProject(p ?? null); - return p ? issueService.list(workspaceSlug, projectId, { limit: 1000 }) : []; + return p ? issueService.listAll(workspaceSlug, projectId) : []; }) .then((issues) => { if (!cancelled && Array.isArray(issues)) setProjectIssueCount(issues.length); diff --git a/apps/web/src/components/settings/modals/ExportModal.tsx b/apps/web/src/components/settings/modals/ExportModal.tsx index 77fc58a..c1d142a 100644 --- a/apps/web/src/components/settings/modals/ExportModal.tsx +++ b/apps/web/src/components/settings/modals/ExportModal.tsx @@ -86,7 +86,8 @@ export function ExportModal({ project_name?: string; } > = []; - const limit = 2000; + // The server caps `limit` at 100, so page through in 100s. + const limit = 100; for (const pid of projectIds) { const proj = projects.find((p) => p.id === pid); let offset = 0; diff --git a/apps/web/src/pages/AnalyticsOverviewPage.tsx b/apps/web/src/pages/AnalyticsOverviewPage.tsx index 243a678..fcac38b 100644 --- a/apps/web/src/pages/AnalyticsOverviewPage.tsx +++ b/apps/web/src/pages/AnalyticsOverviewPage.tsx @@ -56,9 +56,7 @@ export function AnalyticsOverviewPage() { setMembers(mem ?? []); } if (!cancelled && projs?.length) { - return Promise.all( - projs.map((p) => issueService.list(workspaceSlug!, p.id, { limit: 200 })), - ); + return Promise.all(projs.map((p) => issueService.listAll(workspaceSlug!, p.id))); } return []; }) diff --git a/apps/web/src/pages/AnalyticsWorkItemsPage.tsx b/apps/web/src/pages/AnalyticsWorkItemsPage.tsx index 902869a..cded0e7 100644 --- a/apps/web/src/pages/AnalyticsWorkItemsPage.tsx +++ b/apps/web/src/pages/AnalyticsWorkItemsPage.tsx @@ -126,7 +126,7 @@ export function AnalyticsWorkItemsPage() { if (!cancelled && projs?.length) setProjects(projs); if (!cancelled && projs?.length) { return Promise.all([ - ...projs.map((p) => issueService.list(workspaceSlug!, p.id, { limit: 200 })), + ...projs.map((p) => issueService.listAll(workspaceSlug!, p.id)), ...projs.map((p) => stateService.list(workspaceSlug!, p.id)), ]); } diff --git a/apps/web/src/pages/CycleDetailPage.tsx b/apps/web/src/pages/CycleDetailPage.tsx index d8df10b..53cdd9a 100644 --- a/apps/web/src/pages/CycleDetailPage.tsx +++ b/apps/web/src/pages/CycleDetailPage.tsx @@ -158,7 +158,7 @@ export function CycleDetailPage() { workspaceService.getBySlug(workspaceSlug), projectService.get(workspaceSlug, projectId), cycleService.list(workspaceSlug, projectId), - issueService.list(workspaceSlug, projectId, { limit: 1000 }), + issueService.listAll(workspaceSlug, projectId), stateService.list(workspaceSlug, projectId), labelService.list(workspaceSlug, projectId), moduleService.list(workspaceSlug, projectId), @@ -309,7 +309,7 @@ export function CycleDetailPage() { setCycle(res.cycle); // Some work items may have moved out; refresh the list and the snapshot. const [allIssues, snap] = await Promise.all([ - issueService.list(workspaceSlug, projectId, { limit: 500 }), + issueService.listAll(workspaceSlug, projectId), cycleService.getProgress(workspaceSlug, projectId, cycle.id), ]); setIssues(allIssues ?? []); diff --git a/apps/web/src/pages/CyclesPage.tsx b/apps/web/src/pages/CyclesPage.tsx index a2010b6..a2fe94e 100644 --- a/apps/web/src/pages/CyclesPage.tsx +++ b/apps/web/src/pages/CyclesPage.tsx @@ -373,7 +373,7 @@ export function CyclesPage() { projectService.get(workspaceSlug, projectId), cycleService.list(workspaceSlug, projectId), workspaceService.listMembers(workspaceSlug), - issueService.list(workspaceSlug, projectId, { limit: 500 }), + issueService.listAll(workspaceSlug, projectId), stateService.list(workspaceSlug, projectId), labelService.list(workspaceSlug, projectId), ]) @@ -471,7 +471,7 @@ export function CyclesPage() { } Promise.all([ cycleService.list(workspaceSlug, projectId), - issueService.list(workspaceSlug, projectId, { limit: 500 }), + issueService.listAll(workspaceSlug, projectId), ]) .then(([list, iss]) => { setCycles(list ?? []); diff --git a/apps/web/src/pages/EpicDetailPage.tsx b/apps/web/src/pages/EpicDetailPage.tsx index 68f1ea4..fb70acf 100644 --- a/apps/web/src/pages/EpicDetailPage.tsx +++ b/apps/web/src/pages/EpicDetailPage.tsx @@ -65,7 +65,7 @@ export function EpicDetailPage() { projectService.get(workspaceSlug, projectId), epicService.get(workspaceSlug, projectId, epicId), epicService.listIssues(workspaceSlug, projectId, epicId), - issueService.list(workspaceSlug, projectId, { limit: 250 }), + issueService.listAll(workspaceSlug, projectId), stateService.list(workspaceSlug, projectId), epicService.listLinks(workspaceSlug, projectId, epicId).catch(() => []), epicService.listProgress(workspaceSlug, projectId).catch(() => ({})), diff --git a/apps/web/src/pages/IntakePage.tsx b/apps/web/src/pages/IntakePage.tsx index ccb5150..9b555aa 100644 --- a/apps/web/src/pages/IntakePage.tsx +++ b/apps/web/src/pages/IntakePage.tsx @@ -112,7 +112,7 @@ export function IntakePage() { projectService.get(workspaceSlug, projectId), intakeService.list(workspaceSlug, projectId, 'pending'), intakeService.pendingCount(workspaceSlug, projectId), - issueService.list(workspaceSlug, projectId, { limit: 500 }), + issueService.listAll(workspaceSlug, projectId), ]) .then(([w, p, pending, count, issues]) => { if (cancelled) return; diff --git a/apps/web/src/pages/IssueDetailPage.tsx b/apps/web/src/pages/IssueDetailPage.tsx index 1785833..e3926a9 100644 --- a/apps/web/src/pages/IssueDetailPage.tsx +++ b/apps/web/src/pages/IssueDetailPage.tsx @@ -158,7 +158,7 @@ export function IssueDetailPage() { cycleService.list(workspaceSlug, projectId), moduleService.list(workspaceSlug, projectId), workspaceService.listMembers(workspaceSlug), - issueService.list(workspaceSlug, projectId, { limit: 250 }), + issueService.listAll(workspaceSlug, projectId), commentService.list(workspaceSlug, projectId, issueId), issueService .listActivities(workspaceSlug, projectId, issueId) @@ -1594,7 +1594,7 @@ export function IssueDetailPage() { .catch(() => {}); } const refreshedAll = await issueService - .list(workspaceSlug, project.id, { limit: 250 }) + .listAll(workspaceSlug, project.id) .catch(() => null); if (refreshedAll) setAllIssues(refreshedAll); setSubCreateOpen(false); diff --git a/apps/web/src/pages/ModuleDetailPage.tsx b/apps/web/src/pages/ModuleDetailPage.tsx index 630e8a0..060ebbc 100644 --- a/apps/web/src/pages/ModuleDetailPage.tsx +++ b/apps/web/src/pages/ModuleDetailPage.tsx @@ -275,7 +275,7 @@ export function ModuleDetailPage() { const refetchIssues = () => { if (!workspaceSlug || !projectId || !resolvedModuleId) return; issueService - .list(workspaceSlug, projectId, { limit: 1000 }) + .listAll(workspaceSlug, projectId) .then((list) => { setIssues((list ?? []).filter((i) => i.module_ids?.includes(resolvedModuleId))); }) @@ -293,7 +293,7 @@ export function ModuleDetailPage() { workspaceService.getBySlug(workspaceSlug), projectService.get(workspaceSlug, projectId), moduleService.list(workspaceSlug, projectId), - issueService.list(workspaceSlug, projectId, { limit: 1000 }), + issueService.listAll(workspaceSlug, projectId), stateService.list(workspaceSlug, projectId), labelService.list(workspaceSlug, projectId), cycleService.list(workspaceSlug, projectId), diff --git a/apps/web/src/pages/ViewDetailPage.tsx b/apps/web/src/pages/ViewDetailPage.tsx index eff95bc..69717e6 100644 --- a/apps/web/src/pages/ViewDetailPage.tsx +++ b/apps/web/src/pages/ViewDetailPage.tsx @@ -197,7 +197,7 @@ export function ViewDetailPage() { const refetchIssues = () => { if (!workspaceSlug || !projectId) return; issueService - .list(workspaceSlug, projectId, { limit: 500 }) + .listAll(workspaceSlug, projectId) .then(setIssues) .catch(() => {}); }; @@ -279,7 +279,7 @@ export function ViewDetailPage() { workspaceService.getBySlug(workspaceSlug), projectService.get(workspaceSlug, projectId), projectService.list(workspaceSlug), - issueService.list(workspaceSlug, projectId, { limit: 500 }), + issueService.listAll(workspaceSlug, projectId), stateService.list(workspaceSlug, projectId), labelService.list(workspaceSlug, projectId), workspaceService.listMembers(workspaceSlug), diff --git a/apps/web/src/services/issueService.ts b/apps/web/src/services/issueService.ts index 78b8e68..e5ce35c 100644 --- a/apps/web/src/services/issueService.ts +++ b/apps/web/src/services/issueService.ts @@ -63,6 +63,25 @@ export const issueService = { return data; }, + /** + * Fetch every issue for a project by paging through `list` in chunks of 100 + * (the server caps `limit` at 100). Use this instead of passing a large + * `limit`, which the server silently caps and truncates. + */ + async listAll(workspaceSlug: string, projectId: string): Promise { + const pageSize = 100; + const all: IssueApiResponse[] = []; + let offset = 0; + // Bound the loop so a misbehaving backend can't spin forever. + for (let page = 0; page < 500; page++) { + const batch = await this.list(workspaceSlug, projectId, { limit: pageSize, offset }); + all.push(...batch); + if (batch.length < pageSize) break; + offset += pageSize; + } + return all; + }, + async get(workspaceSlug: string, projectId: string, issueId: string): Promise { const { data } = await apiClient.get( `/api/workspaces/${encodeURIComponent(workspaceSlug)}/projects/${encodeURIComponent(projectId)}/issues/${encodeURIComponent(issueId)}/`,