From 4fb7e8ff16dab15ea351a84712d936dda672ba22 Mon Sep 17 00:00:00 2001
From: Dotta
Date: Fri, 24 Apr 2026 07:19:15 -0500
Subject: [PATCH] fix: polish issue board query limits
---
ui/src/components/IssuesList.test.tsx | 40 +++++++++++++++++++++++++++
ui/src/components/IssuesList.tsx | 16 +++++++++--
2 files changed, 54 insertions(+), 2 deletions(-)
diff --git a/ui/src/components/IssuesList.test.tsx b/ui/src/components/IssuesList.test.tsx
index ab2921df8e6..d6219038ab9 100644
--- a/ui/src/components/IssuesList.test.tsx
+++ b/ui/src/components/IssuesList.test.tsx
@@ -483,6 +483,46 @@ describe("IssuesList", () => {
});
});
+ it("shows a refinement hint when a board column hits its server cap", async () => {
+ localStorage.setItem(
+ "paperclip:test-issues:company-1",
+ JSON.stringify({ viewMode: "board" }),
+ );
+
+ const cappedBacklogIssues = Array.from({ length: 200 }, (_, index) =>
+ createIssue({
+ id: `issue-backlog-${index + 1}`,
+ identifier: `PAP-${index + 1}`,
+ title: `Backlog issue ${index + 1}`,
+ status: "backlog",
+ }),
+ );
+
+ mockIssuesApi.list.mockImplementation((_companyId, filters) => {
+ if (filters?.status === "backlog") return Promise.resolve(cappedBacklogIssues);
+ return Promise.resolve([]);
+ });
+
+ const { root } = renderWithQueryClient(
+ undefined}
+ />,
+ container,
+ );
+
+ await waitForAssertion(() => {
+ expect(container.textContent).toContain("Some board columns are showing up to 200 issues. Refine filters or search to reveal the rest.");
+ });
+
+ act(() => {
+ root.unmount();
+ });
+ });
+
it("caps the first paint for large issue lists", async () => {
const manyIssues = Array.from({ length: 220 }, (_, index) =>
createIssue({
diff --git a/ui/src/components/IssuesList.tsx b/ui/src/components/IssuesList.tsx
index c19d76a9b25..3a63a92ab7f 100644
--- a/ui/src/components/IssuesList.tsx
+++ b/ui/src/components/IssuesList.tsx
@@ -57,14 +57,14 @@ import { KanbanBoard } from "./KanbanBoard";
import { buildIssueTree, countDescendants } from "../lib/issue-tree";
import { buildSubIssueDefaultsForViewer } from "../lib/subIssueDefaults";
import { statusBadge } from "../lib/status-colors";
-import type { Issue, Project } from "@paperclipai/shared";
+import { ISSUE_STATUSES, type Issue, type Project } from "@paperclipai/shared";
const ISSUE_SEARCH_DEBOUNCE_MS = 250;
const ISSUE_SEARCH_RESULT_LIMIT = 200;
const ISSUE_BOARD_COLUMN_RESULT_LIMIT = 200;
const INITIAL_ISSUE_ROW_RENDER_LIMIT = 150;
const ISSUE_ROW_RENDER_BATCH_SIZE = 150;
const ISSUE_ROW_RENDER_BATCH_DELAY_MS = 0;
-const boardIssueStatuses = ["backlog", "todo", "in_progress", "in_review", "blocked", "done", "cancelled"];
+const boardIssueStatuses = ISSUE_STATUSES;
/* ── View state ── */
@@ -621,6 +621,13 @@ export function IssuesList({
if (merged.size > 0) return [...merged.values()];
return isPending ? issues : [];
}, [boardIssueQueries, issues, searchWithinLoadedIssues, viewState.viewMode]);
+ const boardColumnLimitReached = useMemo(
+ () =>
+ viewState.viewMode === "board" &&
+ !searchWithinLoadedIssues &&
+ boardIssueQueries.some((query) => (query.data?.length ?? 0) === ISSUE_BOARD_COLUMN_RESULT_LIMIT),
+ [boardIssueQueries, searchWithinLoadedIssues, viewState.viewMode],
+ );
const filtered = useMemo(() => {
const useRemoteSearch = normalizedIssueSearch.length > 0 && !searchWithinLoadedIssues;
@@ -933,6 +940,11 @@ export function IssuesList({
Showing up to {ISSUE_SEARCH_RESULT_LIMIT} matches. Refine the search to narrow further.
)}
+ {boardColumnLimitReached && (
+
+ Some board columns are showing up to {ISSUE_BOARD_COLUMN_RESULT_LIMIT} issues. Refine filters or search to reveal the rest.
+
+ )}
{!isLoading && filtered.length === 0 && viewState.viewMode === "list" && (