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" && (