mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-16 19:00:38 +09:00
Speed up issue search
This commit is contained in:
parent
0edac73a68
commit
5136381d8f
15 changed files with 13127 additions and 27 deletions
|
|
@ -60,12 +60,12 @@ export function CommandPalette() {
|
|||
const { data: issues = [] } = useQuery({
|
||||
queryKey: queryKeys.issues.list(selectedCompanyId!),
|
||||
queryFn: () => issuesApi.list(selectedCompanyId!),
|
||||
enabled: !!selectedCompanyId && open,
|
||||
enabled: !!selectedCompanyId && open && searchQuery.length === 0,
|
||||
});
|
||||
|
||||
const { data: searchedIssues = [] } = useQuery({
|
||||
queryKey: queryKeys.issues.search(selectedCompanyId!, searchQuery),
|
||||
queryFn: () => issuesApi.list(selectedCompanyId!, { q: searchQuery }),
|
||||
queryKey: queryKeys.issues.search(selectedCompanyId!, searchQuery, undefined, 10),
|
||||
queryFn: () => issuesApi.list(selectedCompanyId!, { q: searchQuery, limit: 10 }),
|
||||
enabled: !!selectedCompanyId && open && searchQuery.length > 0,
|
||||
});
|
||||
|
||||
|
|
|
|||
172
ui/src/components/IssuesList.test.tsx
Normal file
172
ui/src/components/IssuesList.test.tsx
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
// @vitest-environment jsdom
|
||||
|
||||
import { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import type { ReactNode } from "react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import type { Issue } from "@paperclipai/shared";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { IssuesList } from "./IssuesList";
|
||||
|
||||
const companyState = vi.hoisted(() => ({
|
||||
selectedCompanyId: "company-1",
|
||||
}));
|
||||
|
||||
const dialogState = vi.hoisted(() => ({
|
||||
openNewIssue: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockIssuesApi = vi.hoisted(() => ({
|
||||
list: vi.fn(),
|
||||
listLabels: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockAuthApi = vi.hoisted(() => ({
|
||||
getSession: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../context/CompanyContext", () => ({
|
||||
useCompany: () => companyState,
|
||||
}));
|
||||
|
||||
vi.mock("../context/DialogContext", () => ({
|
||||
useDialog: () => dialogState,
|
||||
}));
|
||||
|
||||
vi.mock("../api/issues", () => ({
|
||||
issuesApi: mockIssuesApi,
|
||||
}));
|
||||
|
||||
vi.mock("../api/auth", () => ({
|
||||
authApi: mockAuthApi,
|
||||
}));
|
||||
|
||||
vi.mock("./IssueRow", () => ({
|
||||
IssueRow: ({ issue }: { issue: Issue }) => <div data-testid="issue-row">{issue.title}</div>,
|
||||
}));
|
||||
|
||||
vi.mock("./KanbanBoard", () => ({
|
||||
KanbanBoard: () => null,
|
||||
}));
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
function createIssue(overrides: Partial<Issue> = {}): Issue {
|
||||
return {
|
||||
id: "issue-1",
|
||||
identifier: "PAP-1",
|
||||
companyId: "company-1",
|
||||
projectId: null,
|
||||
projectWorkspaceId: null,
|
||||
goalId: null,
|
||||
parentId: null,
|
||||
title: "Issue title",
|
||||
description: null,
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
assigneeAgentId: null,
|
||||
assigneeUserId: null,
|
||||
createdByAgentId: null,
|
||||
createdByUserId: null,
|
||||
issueNumber: 1,
|
||||
requestDepth: 0,
|
||||
billingCode: null,
|
||||
assigneeAdapterOverrides: null,
|
||||
executionWorkspaceId: null,
|
||||
executionWorkspacePreference: null,
|
||||
executionWorkspaceSettings: null,
|
||||
checkoutRunId: null,
|
||||
executionRunId: null,
|
||||
executionAgentNameKey: null,
|
||||
executionLockedAt: null,
|
||||
startedAt: null,
|
||||
completedAt: null,
|
||||
cancelledAt: null,
|
||||
hiddenAt: null,
|
||||
createdAt: new Date("2026-04-07T00:00:00.000Z"),
|
||||
updatedAt: new Date("2026-04-07T00:00:00.000Z"),
|
||||
labels: [],
|
||||
labelIds: [],
|
||||
myLastTouchAt: null,
|
||||
lastExternalCommentAt: null,
|
||||
isUnreadForMe: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
async function flush() {
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
function renderWithQueryClient(node: ReactNode, container: HTMLDivElement) {
|
||||
const root = createRoot(container);
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
act(() => {
|
||||
root.render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{node}
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
});
|
||||
|
||||
return { root, queryClient };
|
||||
}
|
||||
|
||||
describe("IssuesList", () => {
|
||||
let container: HTMLDivElement;
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
dialogState.openNewIssue.mockReset();
|
||||
mockIssuesApi.list.mockReset();
|
||||
mockIssuesApi.listLabels.mockReset();
|
||||
mockAuthApi.getSession.mockReset();
|
||||
mockIssuesApi.listLabels.mockResolvedValue([]);
|
||||
mockAuthApi.getSession.mockResolvedValue({ user: null, session: null });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
container.remove();
|
||||
});
|
||||
|
||||
it("renders server search results instead of filtering the full issue list locally", async () => {
|
||||
const localIssue = createIssue({ id: "issue-local", identifier: "PAP-1", title: "Local issue" });
|
||||
const serverIssue = createIssue({ id: "issue-server", identifier: "PAP-2", title: "Server result" });
|
||||
|
||||
mockIssuesApi.list.mockResolvedValue([serverIssue]);
|
||||
|
||||
const { root } = renderWithQueryClient(
|
||||
<IssuesList
|
||||
issues={[localIssue]}
|
||||
agents={[]}
|
||||
projects={[]}
|
||||
viewStateKey="paperclip:test-issues"
|
||||
initialSearch="server"
|
||||
onUpdateIssue={() => undefined}
|
||||
/>,
|
||||
container,
|
||||
);
|
||||
|
||||
await flush();
|
||||
await flush();
|
||||
|
||||
expect(mockIssuesApi.list).toHaveBeenCalledWith("company-1", { q: "server", projectId: undefined });
|
||||
expect(container.textContent).toContain("Server result");
|
||||
expect(container.textContent).not.toContain("Local issue");
|
||||
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -145,18 +145,6 @@ function countActiveFilters(state: IssueViewState): number {
|
|||
return count;
|
||||
}
|
||||
|
||||
function matchesIssueSearch(issue: Issue, normalizedSearch: string): boolean {
|
||||
if (!normalizedSearch) return true;
|
||||
|
||||
return [
|
||||
issue.identifier,
|
||||
issue.title,
|
||||
issue.description,
|
||||
]
|
||||
.filter((value): value is string => Boolean(value))
|
||||
.some((value) => value.toLowerCase().includes(normalizedSearch));
|
||||
}
|
||||
|
||||
/* ── Component ── */
|
||||
|
||||
interface Agent {
|
||||
|
|
@ -278,12 +266,10 @@ export function IssuesList({
|
|||
}, [agents]);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const sourceIssues = normalizedIssueSearch.length > 0
|
||||
? issues.filter((issue) => matchesIssueSearch(issue, normalizedIssueSearch))
|
||||
: issues;
|
||||
const sourceIssues = normalizedIssueSearch.length > 0 ? searchedIssues : issues;
|
||||
const filteredByControls = applyFilters(sourceIssues, viewState, currentUserId);
|
||||
return sortIssues(filteredByControls, viewState);
|
||||
}, [issues, viewState, normalizedIssueSearch, currentUserId]);
|
||||
}, [issues, searchedIssues, viewState, normalizedIssueSearch, currentUserId]);
|
||||
|
||||
const { data: labels } = useQuery({
|
||||
queryKey: queryKeys.issues.labels(selectedCompanyId!),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue