mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-18 19:50:38 +09:00
Merge pull request #3385 from paperclipai/pap-1347-inbox-issue-search
feat(inbox): improve issue search matches
This commit is contained in:
commit
96637a1e09
7 changed files with 383 additions and 9 deletions
|
|
@ -298,6 +298,51 @@ describeEmbeddedPostgres("issueService.list participantAgentId", () => {
|
||||||
expect(result.map((issue) => issue.id)).toEqual([titleMatchId, descriptionMatchId]);
|
expect(result.map((issue) => issue.id)).toEqual([titleMatchId, descriptionMatchId]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("ranks comment matches ahead of description-only matches", async () => {
|
||||||
|
const companyId = randomUUID();
|
||||||
|
const commentMatchId = randomUUID();
|
||||||
|
const descriptionMatchId = randomUUID();
|
||||||
|
|
||||||
|
await db.insert(companies).values({
|
||||||
|
id: companyId,
|
||||||
|
name: "Paperclip",
|
||||||
|
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||||
|
requireBoardApprovalForNewAgents: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.insert(issues).values([
|
||||||
|
{
|
||||||
|
id: commentMatchId,
|
||||||
|
companyId,
|
||||||
|
title: "Comment match",
|
||||||
|
status: "todo",
|
||||||
|
priority: "medium",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: descriptionMatchId,
|
||||||
|
companyId,
|
||||||
|
title: "Description match",
|
||||||
|
description: "Contains pull/3303 in the description",
|
||||||
|
status: "todo",
|
||||||
|
priority: "medium",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await db.insert(issueComments).values({
|
||||||
|
companyId,
|
||||||
|
issueId: commentMatchId,
|
||||||
|
body: "Reference: https://github.com/paperclipai/paperclip/pull/3303",
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await svc.list(companyId, {
|
||||||
|
q: "pull/3303",
|
||||||
|
limit: 2,
|
||||||
|
includeRoutineExecutions: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.map((issue) => issue.id)).toEqual([commentMatchId, descriptionMatchId]);
|
||||||
|
});
|
||||||
|
|
||||||
it("accepts issue identifiers through getById", async () => {
|
it("accepts issue identifiers through getById", async () => {
|
||||||
const companyId = randomUUID();
|
const companyId = randomUUID();
|
||||||
const issueId = randomUUID();
|
const issueId = randomUUID();
|
||||||
|
|
|
||||||
|
|
@ -997,8 +997,8 @@ export function issueService(db: Db) {
|
||||||
WHEN ${titleContainsMatch} THEN 1
|
WHEN ${titleContainsMatch} THEN 1
|
||||||
WHEN ${identifierStartsWithMatch} THEN 2
|
WHEN ${identifierStartsWithMatch} THEN 2
|
||||||
WHEN ${identifierContainsMatch} THEN 3
|
WHEN ${identifierContainsMatch} THEN 3
|
||||||
WHEN ${descriptionContainsMatch} THEN 4
|
WHEN ${commentContainsMatch} THEN 4
|
||||||
WHEN ${commentContainsMatch} THEN 5
|
WHEN ${descriptionContainsMatch} THEN 5
|
||||||
ELSE 6
|
ELSE 6
|
||||||
END
|
END
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
190
ui/src/components/CommandPalette.test.tsx
Normal file
190
ui/src/components/CommandPalette.test.tsx
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
|
||||||
|
import { act } from "react";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { CommandPalette } from "./CommandPalette";
|
||||||
|
|
||||||
|
const companyState = vi.hoisted(() => ({
|
||||||
|
selectedCompanyId: "company-1",
|
||||||
|
}));
|
||||||
|
|
||||||
|
const dialogState = vi.hoisted(() => ({
|
||||||
|
openNewIssue: vi.fn(),
|
||||||
|
openNewAgent: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const sidebarState = vi.hoisted(() => ({
|
||||||
|
isMobile: false,
|
||||||
|
setSidebarOpen: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockIssuesApi = vi.hoisted(() => ({
|
||||||
|
list: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockAgentsApi = vi.hoisted(() => ({
|
||||||
|
list: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockProjectsApi = vi.hoisted(() => ({
|
||||||
|
list: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../context/CompanyContext", () => ({
|
||||||
|
useCompany: () => companyState,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../context/DialogContext", () => ({
|
||||||
|
useDialog: () => dialogState,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../context/SidebarContext", () => ({
|
||||||
|
useSidebar: () => sidebarState,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/lib/router", () => ({
|
||||||
|
useNavigate: () => vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../api/issues", () => ({
|
||||||
|
issuesApi: mockIssuesApi,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../api/agents", () => ({
|
||||||
|
agentsApi: mockAgentsApi,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../api/projects", () => ({
|
||||||
|
projectsApi: mockProjectsApi,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("./Identity", () => ({
|
||||||
|
Identity: ({ name }: { name: string }) => <span>{name}</span>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/components/ui/command", () => ({
|
||||||
|
CommandDialog: ({ open, children }: { open: boolean; children: ReactNode }) => (open ? <div>{children}</div> : null),
|
||||||
|
CommandEmpty: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||||
|
CommandGroup: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||||
|
CommandInput: ({
|
||||||
|
value,
|
||||||
|
onValueChange,
|
||||||
|
}: {
|
||||||
|
value: string;
|
||||||
|
onValueChange: (value: string) => void;
|
||||||
|
}) => (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
aria-label="Command search"
|
||||||
|
value={value}
|
||||||
|
onChange={(event) => onValueChange(event.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<button type="button" aria-label="Set query" onClick={() => onValueChange("pull/3303")} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
CommandItem: ({
|
||||||
|
children,
|
||||||
|
onSelect,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
onSelect?: () => void;
|
||||||
|
}) => <button onClick={onSelect}>{children}</button>,
|
||||||
|
CommandList: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||||
|
CommandSeparator: () => <hr />,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
|
||||||
|
|
||||||
|
async function flush() {
|
||||||
|
await act(async () => {
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForAssertion(assertion: () => void, attempts = 20) {
|
||||||
|
let lastError: unknown;
|
||||||
|
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
||||||
|
try {
|
||||||
|
assertion();
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
await flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw lastError;
|
||||||
|
}
|
||||||
|
|
||||||
|
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("CommandPalette", () => {
|
||||||
|
let container: HTMLDivElement;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
container = document.createElement("div");
|
||||||
|
document.body.appendChild(container);
|
||||||
|
dialogState.openNewIssue.mockReset();
|
||||||
|
dialogState.openNewAgent.mockReset();
|
||||||
|
sidebarState.setSidebarOpen.mockReset();
|
||||||
|
mockIssuesApi.list.mockReset();
|
||||||
|
mockAgentsApi.list.mockReset();
|
||||||
|
mockProjectsApi.list.mockReset();
|
||||||
|
mockIssuesApi.list.mockResolvedValue([]);
|
||||||
|
mockAgentsApi.list.mockResolvedValue([]);
|
||||||
|
mockProjectsApi.list.mockResolvedValue([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
container.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes routine execution issues in search queries", async () => {
|
||||||
|
const { root } = renderWithQueryClient(<CommandPalette />, container);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
document.dispatchEvent(new KeyboardEvent("keydown", { key: "k", metaKey: true, bubbles: true }));
|
||||||
|
});
|
||||||
|
|
||||||
|
const setQueryButton = container.querySelector('button[aria-label="Set query"]');
|
||||||
|
expect(setQueryButton).not.toBeNull();
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
setQueryButton!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForAssertion(() => {
|
||||||
|
expect(mockIssuesApi.list).toHaveBeenCalledWith("company-1", {
|
||||||
|
q: "pull/3303",
|
||||||
|
limit: 10,
|
||||||
|
includeRoutineExecutions: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
root.unmount();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -65,7 +65,7 @@ export function CommandPalette() {
|
||||||
|
|
||||||
const { data: searchedIssues = [] } = useQuery({
|
const { data: searchedIssues = [] } = useQuery({
|
||||||
queryKey: queryKeys.issues.search(selectedCompanyId!, searchQuery, undefined, 10),
|
queryKey: queryKeys.issues.search(selectedCompanyId!, searchQuery, undefined, 10),
|
||||||
queryFn: () => issuesApi.list(selectedCompanyId!, { q: searchQuery, limit: 10 }),
|
queryFn: () => issuesApi.list(selectedCompanyId!, { q: searchQuery, limit: 10, includeRoutineExecutions: true }),
|
||||||
enabled: !!selectedCompanyId && open && searchQuery.length > 0,
|
enabled: !!selectedCompanyId && open && searchQuery.length > 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import {
|
||||||
getApprovalsForTab,
|
getApprovalsForTab,
|
||||||
getInboxWorkItems,
|
getInboxWorkItems,
|
||||||
getInboxKeyboardSelectionIndex,
|
getInboxKeyboardSelectionIndex,
|
||||||
|
getInboxSearchSupplementIssues,
|
||||||
getRecentTouchedIssues,
|
getRecentTouchedIssues,
|
||||||
getUnreadTouchedIssues,
|
getUnreadTouchedIssues,
|
||||||
groupInboxWorkItems,
|
groupInboxWorkItems,
|
||||||
|
|
@ -611,6 +612,65 @@ describe("inbox helpers", () => {
|
||||||
).toEqual(["newer", "older"]);
|
).toEqual(["newer", "older"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("adds remote issue results that are not already present in inbox search results", () => {
|
||||||
|
const remoteMatch = makeIssue("remote-match", false);
|
||||||
|
remoteMatch.status = "in_progress";
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getInboxSearchSupplementIssues({
|
||||||
|
query: "pull/3303",
|
||||||
|
filteredWorkItems: [],
|
||||||
|
archivedSearchIssues: [],
|
||||||
|
remoteIssues: [remoteMatch],
|
||||||
|
issueFilters: {
|
||||||
|
statuses: ["in_progress"],
|
||||||
|
priorities: [],
|
||||||
|
assignees: [],
|
||||||
|
labels: [],
|
||||||
|
projects: [],
|
||||||
|
workspaces: [],
|
||||||
|
showRoutineExecutions: false,
|
||||||
|
},
|
||||||
|
}).map((issue) => issue.id),
|
||||||
|
).toEqual(["remote-match"]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getInboxSearchSupplementIssues({
|
||||||
|
query: "pull/3303",
|
||||||
|
filteredWorkItems: [{ kind: "issue", timestamp: 1, issue: makeIssue("remote-match", false) }],
|
||||||
|
archivedSearchIssues: [],
|
||||||
|
remoteIssues: [remoteMatch],
|
||||||
|
issueFilters: {
|
||||||
|
statuses: [],
|
||||||
|
priorities: [],
|
||||||
|
assignees: [],
|
||||||
|
labels: [],
|
||||||
|
projects: [],
|
||||||
|
workspaces: [],
|
||||||
|
showRoutineExecutions: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toEqual([]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getInboxSearchSupplementIssues({
|
||||||
|
query: "pull/3303",
|
||||||
|
filteredWorkItems: [],
|
||||||
|
archivedSearchIssues: [makeIssue("remote-match", false)],
|
||||||
|
remoteIssues: [remoteMatch],
|
||||||
|
issueFilters: {
|
||||||
|
statuses: [],
|
||||||
|
priorities: [],
|
||||||
|
assignees: [],
|
||||||
|
labels: [],
|
||||||
|
projects: [],
|
||||||
|
workspaces: [],
|
||||||
|
showRoutineExecutions: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
it("defaults the remembered inbox tab to mine and persists all", () => {
|
it("defaults the remembered inbox tab to mine and persists all", () => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
expect(loadLastInboxTab()).toBe("mine");
|
expect(loadLastInboxTab()).toBe("mine");
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import type {
|
||||||
JoinRequest,
|
JoinRequest,
|
||||||
} from "@paperclipai/shared";
|
} from "@paperclipai/shared";
|
||||||
import {
|
import {
|
||||||
|
applyIssueFilters,
|
||||||
defaultIssueFilterState,
|
defaultIssueFilterState,
|
||||||
type IssueFilterState,
|
type IssueFilterState,
|
||||||
} from "./issue-filters";
|
} from "./issue-filters";
|
||||||
|
|
@ -370,6 +371,35 @@ export function getArchivedInboxSearchIssues({
|
||||||
.sort(sortIssuesByMostRecentActivity);
|
.sort(sortIssuesByMostRecentActivity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getInboxSearchSupplementIssues({
|
||||||
|
query,
|
||||||
|
filteredWorkItems,
|
||||||
|
archivedSearchIssues,
|
||||||
|
remoteIssues,
|
||||||
|
issueFilters,
|
||||||
|
currentUserId,
|
||||||
|
enableRoutineVisibilityFilter = false,
|
||||||
|
}: {
|
||||||
|
query: string;
|
||||||
|
filteredWorkItems: InboxWorkItem[];
|
||||||
|
archivedSearchIssues: Issue[];
|
||||||
|
remoteIssues: Issue[];
|
||||||
|
issueFilters: IssueFilterState;
|
||||||
|
currentUserId?: string | null;
|
||||||
|
enableRoutineVisibilityFilter?: boolean;
|
||||||
|
}): Issue[] {
|
||||||
|
const normalizedQuery = query.trim();
|
||||||
|
if (!normalizedQuery) return [];
|
||||||
|
const visibleIssueIds = new Set([
|
||||||
|
...filteredWorkItems
|
||||||
|
.filter((item): item is Extract<InboxWorkItem, { kind: "issue" }> => item.kind === "issue")
|
||||||
|
.map((item) => item.issue.id),
|
||||||
|
...archivedSearchIssues.map((issue) => issue.id),
|
||||||
|
]);
|
||||||
|
return applyIssueFilters(remoteIssues, issueFilters, currentUserId, enableRoutineVisibilityFilter)
|
||||||
|
.filter((issue) => !visibleIssueIds.has(issue.id));
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveIssueWorkspaceName(
|
export function resolveIssueWorkspaceName(
|
||||||
issue: Pick<Issue, "executionWorkspaceId" | "projectId" | "projectWorkspaceId">,
|
issue: Pick<Issue, "executionWorkspaceId" | "projectId" | "projectWorkspaceId">,
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,7 @@ import {
|
||||||
getArchivedInboxSearchIssues,
|
getArchivedInboxSearchIssues,
|
||||||
getInboxWorkItems,
|
getInboxWorkItems,
|
||||||
getInboxKeyboardSelectionIndex,
|
getInboxKeyboardSelectionIndex,
|
||||||
|
getInboxSearchSupplementIssues,
|
||||||
getLatestFailedRunsByAgent,
|
getLatestFailedRunsByAgent,
|
||||||
matchesInboxIssueSearch,
|
matchesInboxIssueSearch,
|
||||||
getRecentTouchedIssues,
|
getRecentTouchedIssues,
|
||||||
|
|
@ -642,6 +643,7 @@ export function Inbox() {
|
||||||
retry: false,
|
retry: false,
|
||||||
});
|
});
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const normalizedSearchQuery = searchQuery.trim();
|
||||||
const [filterPreferences, setFilterPreferences] = useState<InboxFilterPreferences>(
|
const [filterPreferences, setFilterPreferences] = useState<InboxFilterPreferences>(
|
||||||
() => loadInboxFilterPreferences(selectedCompanyId),
|
() => loadInboxFilterPreferences(selectedCompanyId),
|
||||||
);
|
);
|
||||||
|
|
@ -945,7 +947,7 @@ export function Inbox() {
|
||||||
);
|
);
|
||||||
|
|
||||||
const filteredWorkItems = useMemo(() => {
|
const filteredWorkItems = useMemo(() => {
|
||||||
const q = searchQuery.trim().toLowerCase();
|
const q = normalizedSearchQuery.toLowerCase();
|
||||||
if (!q) return workItemsToRender;
|
if (!q) return workItemsToRender;
|
||||||
return workItemsToRender.filter((item) => {
|
return workItemsToRender.filter((item) => {
|
||||||
if (item.kind === "issue") {
|
if (item.kind === "issue") {
|
||||||
|
|
@ -987,12 +989,12 @@ export function Inbox() {
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
workItemsToRender,
|
workItemsToRender,
|
||||||
searchQuery,
|
|
||||||
agentById,
|
agentById,
|
||||||
defaultProjectWorkspaceIdByProjectId,
|
defaultProjectWorkspaceIdByProjectId,
|
||||||
executionWorkspaceById,
|
executionWorkspaceById,
|
||||||
issueById,
|
issueById,
|
||||||
isolatedWorkspacesEnabled,
|
isolatedWorkspacesEnabled,
|
||||||
|
normalizedSearchQuery,
|
||||||
projectWorkspaceById,
|
projectWorkspaceById,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -1002,7 +1004,7 @@ export function Inbox() {
|
||||||
? getArchivedInboxSearchIssues({
|
? getArchivedInboxSearchIssues({
|
||||||
visibleIssues: visibleMineIssues,
|
visibleIssues: visibleMineIssues,
|
||||||
searchableIssues: visibleTouchedIssues,
|
searchableIssues: visibleTouchedIssues,
|
||||||
query: searchQuery,
|
query: normalizedSearchQuery,
|
||||||
isolatedWorkspacesEnabled,
|
isolatedWorkspacesEnabled,
|
||||||
executionWorkspaceById,
|
executionWorkspaceById,
|
||||||
projectWorkspaceById,
|
projectWorkspaceById,
|
||||||
|
|
@ -1013,13 +1015,60 @@ export function Inbox() {
|
||||||
defaultProjectWorkspaceIdByProjectId,
|
defaultProjectWorkspaceIdByProjectId,
|
||||||
executionWorkspaceById,
|
executionWorkspaceById,
|
||||||
isolatedWorkspacesEnabled,
|
isolatedWorkspacesEnabled,
|
||||||
|
normalizedSearchQuery,
|
||||||
projectWorkspaceById,
|
projectWorkspaceById,
|
||||||
searchQuery,
|
|
||||||
tab,
|
tab,
|
||||||
visibleMineIssues,
|
visibleMineIssues,
|
||||||
visibleTouchedIssues,
|
visibleTouchedIssues,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
const shouldUseIssueSearchSupplement =
|
||||||
|
!!selectedCompanyId
|
||||||
|
&& normalizedSearchQuery.length > 0;
|
||||||
|
const { data: remoteIssueSearchResults = [] } = useQuery({
|
||||||
|
queryKey: [
|
||||||
|
...queryKeys.issues.search(selectedCompanyId!, normalizedSearchQuery, undefined, 25),
|
||||||
|
"inbox-supplement",
|
||||||
|
],
|
||||||
|
queryFn: () =>
|
||||||
|
issuesApi.list(selectedCompanyId!, {
|
||||||
|
q: normalizedSearchQuery,
|
||||||
|
limit: 25,
|
||||||
|
includeRoutineExecutions: true,
|
||||||
|
}),
|
||||||
|
enabled: shouldUseIssueSearchSupplement,
|
||||||
|
placeholderData: (previousData) => previousData,
|
||||||
|
});
|
||||||
|
const issueSearchSupplementResults = useMemo(
|
||||||
|
() =>
|
||||||
|
getInboxSearchSupplementIssues({
|
||||||
|
query: normalizedSearchQuery,
|
||||||
|
filteredWorkItems,
|
||||||
|
archivedSearchIssues,
|
||||||
|
remoteIssues: remoteIssueSearchResults,
|
||||||
|
issueFilters,
|
||||||
|
currentUserId,
|
||||||
|
enableRoutineVisibilityFilter: true,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
archivedSearchIssues,
|
||||||
|
currentUserId,
|
||||||
|
filteredWorkItems,
|
||||||
|
issueFilters,
|
||||||
|
normalizedSearchQuery,
|
||||||
|
remoteIssueSearchResults,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
const effectiveWorkItems = useMemo(
|
||||||
|
() =>
|
||||||
|
issueSearchSupplementResults.length > 0
|
||||||
|
? [
|
||||||
|
...filteredWorkItems,
|
||||||
|
...getInboxWorkItems({ issues: issueSearchSupplementResults, approvals: [] }),
|
||||||
|
]
|
||||||
|
: filteredWorkItems,
|
||||||
|
[filteredWorkItems, issueSearchSupplementResults],
|
||||||
|
);
|
||||||
const archivedSearchIssueIds = useMemo(
|
const archivedSearchIssueIds = useMemo(
|
||||||
() => new Set(archivedSearchIssues.map((issue) => issue.id)),
|
() => new Set(archivedSearchIssues.map((issue) => issue.id)),
|
||||||
[archivedSearchIssues],
|
[archivedSearchIssues],
|
||||||
|
|
@ -1037,14 +1086,14 @@ export function Inbox() {
|
||||||
}, []);
|
}, []);
|
||||||
const [collapsedInboxParents, setCollapsedInboxParents] = useState<Set<string>>(new Set());
|
const [collapsedInboxParents, setCollapsedInboxParents] = useState<Set<string>>(new Set());
|
||||||
const groupedSections = useMemo<InboxGroupedSection[]>(() => [
|
const groupedSections = useMemo<InboxGroupedSection[]>(() => [
|
||||||
...buildGroupedInboxSections(filteredWorkItems, groupBy, nestingEnabled),
|
...buildGroupedInboxSections(effectiveWorkItems, groupBy, nestingEnabled),
|
||||||
...buildGroupedInboxSections(
|
...buildGroupedInboxSections(
|
||||||
getInboxWorkItems({ issues: archivedSearchIssues, approvals: [] }),
|
getInboxWorkItems({ issues: archivedSearchIssues, approvals: [] }),
|
||||||
groupBy,
|
groupBy,
|
||||||
nestingEnabled,
|
nestingEnabled,
|
||||||
{ keyPrefix: "archived-search:", isArchivedSearch: true },
|
{ keyPrefix: "archived-search:", isArchivedSearch: true },
|
||||||
),
|
),
|
||||||
], [archivedSearchIssues, filteredWorkItems, groupBy, nestingEnabled]);
|
], [archivedSearchIssues, effectiveWorkItems, groupBy, nestingEnabled]);
|
||||||
const totalVisibleWorkItems = useMemo(
|
const totalVisibleWorkItems = useMemo(
|
||||||
() => groupedSections.reduce((count, group) => count + group.displayItems.length, 0),
|
() => groupedSections.reduce((count, group) => count + group.displayItems.length, 0),
|
||||||
[groupedSections],
|
[groupedSections],
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue