mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-10 20:46:28 +01:00
Fix kanban column issue limits
This commit is contained in:
1 parent
80c5216dba
commit
1ce7c17ad0
2 files changed
+124
-4
No files matched your search
@@ -22,6 +22,8 @@ const mockIssuesApi = vi.hoisted(() => ({
|
||||
listLabels: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockKanbanBoard = vi.hoisted(() => vi.fn());
|
||||
|
||||
const mockAuthApi = vi.hoisted(() => ({
|
||||
getSession: vi.fn(),
|
||||
}));
|
||||
@@ -87,7 +89,16 @@ vi.mock("./IssueRow", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./KanbanBoard", () => ({
|
||||
KanbanBoard: () => null,
|
||||
KanbanBoard: (props: { issues: Issue[] }) => {
|
||||
mockKanbanBoard(props);
|
||||
return (
|
||||
<div data-testid="kanban-board">
|
||||
{props.issues.map((issue) => (
|
||||
<span key={issue.id}>{issue.title}</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -189,6 +200,7 @@ describe("IssuesList", () => {
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
dialogState.openNewIssue.mockReset();
|
||||
mockKanbanBoard.mockReset();
|
||||
mockIssuesApi.list.mockReset();
|
||||
mockIssuesApi.listLabels.mockReset();
|
||||
mockAuthApi.getSession.mockReset();
|
||||
@@ -404,6 +416,73 @@ describe("IssuesList", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("loads board issues with a separate result limit for each status column", async () => {
|
||||
localStorage.setItem(
|
||||
"paperclip:test-issues:company-1",
|
||||
JSON.stringify({ viewMode: "board" }),
|
||||
);
|
||||
|
||||
const parentIssue = createIssue({
|
||||
id: "issue-parent-total-limit",
|
||||
title: "Parent total-limited issue",
|
||||
status: "todo",
|
||||
});
|
||||
const backlogIssue = createIssue({
|
||||
id: "issue-backlog",
|
||||
title: "Backlog column issue",
|
||||
status: "backlog",
|
||||
});
|
||||
const doneIssue = createIssue({
|
||||
id: "issue-done",
|
||||
title: "Done column issue",
|
||||
status: "done",
|
||||
});
|
||||
|
||||
mockIssuesApi.list.mockImplementation((_companyId, filters) => {
|
||||
if (filters?.status === "backlog") return Promise.resolve([backlogIssue]);
|
||||
if (filters?.status === "done") return Promise.resolve([doneIssue]);
|
||||
return Promise.resolve([]);
|
||||
});
|
||||
|
||||
const { root } = renderWithQueryClient(
|
||||
<IssuesList
|
||||
issues={[parentIssue]}
|
||||
agents={[]}
|
||||
projects={[]}
|
||||
viewStateKey="paperclip:test-issues"
|
||||
enableRoutineVisibilityFilter
|
||||
onUpdateIssue={() => undefined}
|
||||
/>,
|
||||
container,
|
||||
);
|
||||
|
||||
await waitForAssertion(() => {
|
||||
expect(mockIssuesApi.list).toHaveBeenCalledWith("company-1", expect.objectContaining({
|
||||
status: "backlog",
|
||||
limit: 200,
|
||||
includeRoutineExecutions: true,
|
||||
}));
|
||||
expect(mockIssuesApi.list).toHaveBeenCalledWith("company-1", expect.objectContaining({
|
||||
status: "done",
|
||||
limit: 200,
|
||||
includeRoutineExecutions: true,
|
||||
}));
|
||||
expect(mockKanbanBoard).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||
issues: expect.arrayContaining([
|
||||
expect.objectContaining({ id: "issue-backlog" }),
|
||||
expect.objectContaining({ id: "issue-done" }),
|
||||
]),
|
||||
}));
|
||||
expect(container.textContent).toContain("Backlog column issue");
|
||||
expect(container.textContent).toContain("Done column issue");
|
||||
expect(container.textContent).not.toContain("Parent total-limited issue");
|
||||
});
|
||||
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it("caps the first paint for large issue lists", async () => {
|
||||
const manyIssues = Array.from({ length: 220 }, (_, index) =>
|
||||
createIssue({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { startTransition, useDeferredValue, useEffect, useMemo, useState, useCallback, useRef } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useQueries, useQuery } from "@tanstack/react-query";
|
||||
import { accessApi } from "../api/access";
|
||||
import { useDialog } from "../context/DialogContext";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
@@ -60,9 +60,11 @@ import { statusBadge } from "../lib/status-colors";
|
||||
import type { Issue, 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"];
|
||||
|
||||
/* ── View state ── */
|
||||
|
||||
@@ -394,6 +396,31 @@ export function IssuesList({
|
||||
enabled: !!selectedCompanyId && normalizedIssueSearch.length > 0,
|
||||
placeholderData: (previousData) => previousData,
|
||||
});
|
||||
const boardIssueQueries = useQueries({
|
||||
queries: boardIssueStatuses.map((status) => ({
|
||||
queryKey: [
|
||||
...queryKeys.issues.list(selectedCompanyId ?? "__no-company__"),
|
||||
"board-column",
|
||||
status,
|
||||
normalizedIssueSearch,
|
||||
projectId ?? "__all-projects__",
|
||||
searchFilters ?? {},
|
||||
ISSUE_BOARD_COLUMN_RESULT_LIMIT,
|
||||
enableRoutineVisibilityFilter ? "with-routine-executions" : "without-routine-executions",
|
||||
],
|
||||
queryFn: () =>
|
||||
issuesApi.list(selectedCompanyId!, {
|
||||
...searchFilters,
|
||||
...(normalizedIssueSearch.length > 0 ? { q: normalizedIssueSearch } : {}),
|
||||
projectId,
|
||||
status,
|
||||
limit: ISSUE_BOARD_COLUMN_RESULT_LIMIT,
|
||||
...(enableRoutineVisibilityFilter ? { includeRoutineExecutions: true } : {}),
|
||||
}),
|
||||
enabled: !!selectedCompanyId && viewState.viewMode === "board",
|
||||
placeholderData: (previousData: Issue[] | undefined) => previousData,
|
||||
})),
|
||||
});
|
||||
const { data: executionWorkspaces = [] } = useQuery({
|
||||
queryKey: selectedCompanyId
|
||||
? queryKeys.executionWorkspaces.summaryList(selectedCompanyId)
|
||||
@@ -570,11 +597,25 @@ export function IssuesList({
|
||||
return map;
|
||||
}, [issues]);
|
||||
|
||||
const boardIssues = useMemo(() => {
|
||||
if (viewState.viewMode !== "board") return null;
|
||||
const merged = new Map<string, Issue>();
|
||||
let isPending = false;
|
||||
for (const query of boardIssueQueries) {
|
||||
isPending ||= query.isPending;
|
||||
for (const issue of query.data ?? []) {
|
||||
merged.set(issue.id, issue);
|
||||
}
|
||||
}
|
||||
if (merged.size > 0) return [...merged.values()];
|
||||
return isPending ? issues : [];
|
||||
}, [boardIssueQueries, issues, viewState.viewMode]);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const sourceIssues = normalizedIssueSearch.length > 0 ? searchedIssues : issues;
|
||||
const sourceIssues = boardIssues ?? (normalizedIssueSearch.length > 0 ? searchedIssues : issues);
|
||||
const filteredByControls = applyIssueFilters(sourceIssues, viewState, currentUserId, enableRoutineVisibilityFilter);
|
||||
return sortIssues(filteredByControls, viewState);
|
||||
}, [issues, searchedIssues, viewState, normalizedIssueSearch, currentUserId, enableRoutineVisibilityFilter]);
|
||||
}, [boardIssues, issues, searchedIssues, viewState, normalizedIssueSearch, currentUserId, enableRoutineVisibilityFilter]);
|
||||
|
||||
const { data: labels } = useQuery({
|
||||
queryKey: queryKeys.issues.labels(selectedCompanyId!),
|
||||
|
||||
Reference in new issue
Block a user