Add inbox other search results section

(cherry picked from commit 81b3aacc81da69b548680f11f6dfa1356863d628)
This commit is contained in:
Dotta committed 2026-04-15 09:12:41 -05:00
1 parent cba4e4a3a5
commit 20a37c87e3
3 files changed
+110 -59

No files matched your search

+40
View File
@@ -12,6 +12,7 @@ import type {
} from "@paperclipai/shared";
import {
DEFAULT_INBOX_ISSUE_COLUMNS,
buildGroupedInboxSections,
buildInboxKeyboardNavEntries,
buildInboxDismissedAtByKey,
computeInboxBadgeData,
@@ -760,6 +761,45 @@ describe("inbox helpers", () => {
).toEqual([]);
});
it("keeps inbox search matches ahead of archived and other result sections", () => {
const inboxIssue = makeIssue("inbox", false);
inboxIssue.lastActivityAt = new Date("2026-03-11T04:00:00.000Z");
const archivedIssue = makeIssue("archived", false);
archivedIssue.lastActivityAt = new Date("2026-03-11T03:00:00.000Z");
const otherIssue = makeIssue("other", false);
otherIssue.lastActivityAt = new Date("2026-03-11T05:00:00.000Z");
const sections = [
...buildGroupedInboxSections(
getInboxWorkItems({ issues: [inboxIssue], approvals: [] }),
"none",
{},
),
...buildGroupedInboxSections(
getInboxWorkItems({ issues: [archivedIssue], approvals: [] }),
"none",
{},
{ keyPrefix: "archived-search:", searchSection: "archived" },
),
...buildGroupedInboxSections(
getInboxWorkItems({ issues: [otherIssue], approvals: [] }),
"none",
{},
{ keyPrefix: "other-search:", searchSection: "other" },
),
];
expect(sections.map((section) => section.searchSection)).toEqual(["none", "archived", "other"]);
expect(
sections.map((section) => {
const [item] = section.displayItems;
return item?.kind === "issue" ? item.issue.id : null;
}),
).toEqual(["inbox", "archived", "other"]);
});
it("defaults the remembered inbox tab to mine and persists all", () => {
localStorage.clear();
expect(loadLastInboxTab()).toBe("mine");
+35
View File
@@ -87,6 +87,16 @@ export interface InboxWorkItemGroup {
items: InboxWorkItem[];
}
export type InboxSearchSection = "none" | "archived" | "other";
export interface InboxGroupedSection {
key: string;
label: string | null;
displayItems: InboxWorkItem[];
childrenByIssueId: Map<string, Issue[]>;
searchSection: InboxSearchSection;
}
export interface InboxKeyboardGroupSection {
key: string;
displayItems: InboxWorkItem[];
@@ -916,6 +926,31 @@ export function buildInboxNesting(items: InboxWorkItem[]): {
return { displayItems, childrenByIssueId };
}
export function buildGroupedInboxSections(
items: InboxWorkItem[],
groupBy: InboxWorkItemGroupBy,
workspaceGrouping: InboxWorkspaceGroupingOptions,
options?: { keyPrefix?: string; searchSection?: InboxSearchSection; nestingEnabled?: boolean },
): InboxGroupedSection[] {
const keyPrefix = options?.keyPrefix ?? "";
const searchSection = options?.searchSection ?? "none";
const nestingEnabled = options?.nestingEnabled ?? false;
return groupInboxWorkItems(items, groupBy, workspaceGrouping).map((group) => {
const nestedGroup = nestingEnabled && group.items.some((item) => item.kind === "issue")
? buildInboxNesting(group.items)
: { displayItems: group.items, childrenByIssueId: new Map<string, Issue[]>() };
return {
key: `${keyPrefix}${group.key}`,
label: group.label,
displayItems: nestedGroup.displayItems,
childrenByIssueId: nestedGroup.childrenByIssueId,
searchSection,
};
});
}
export function getInboxWorkItemKey(item: InboxWorkItem): string {
if (item.kind === "issue") return `issue:${item.issue.id}`;
if (item.kind === "approval") return `approval:${item.approval.id}`;
+35 -59
View File
@@ -97,8 +97,8 @@ import type { Approval, HeartbeatRun, Issue, JoinRequest } from "@paperclipai/sh
import {
ACTIONABLE_APPROVAL_STATUSES,
DEFAULT_INBOX_ISSUE_COLUMNS,
buildGroupedInboxSections,
buildInboxKeyboardNavEntries,
buildInboxNesting,
getAvailableInboxIssueColumns,
getInboxWorkItemKey,
getApprovalsForTab,
@@ -109,7 +109,6 @@ import {
getLatestFailedRunsByAgent,
matchesInboxIssueSearch,
getRecentTouchedIssues,
groupInboxWorkItems,
isInboxEntityDismissed,
isMineInboxTab,
loadCollapsedInboxGroupKeys,
@@ -135,6 +134,7 @@ import {
type InboxKeyboardNavEntry,
saveLastInboxTab,
shouldShowInboxSection,
type InboxGroupedSection,
type InboxTab,
type InboxWorkItem,
type InboxWorkItemGroupBy,
@@ -150,38 +150,6 @@ type SectionKey =
/** A flat navigation entry for keyboard j/k traversal that includes expanded children. */
type NavEntry = InboxKeyboardNavEntry;
type InboxGroupedSection = {
key: string;
label: string | null;
displayItems: InboxWorkItem[];
childrenByIssueId: Map<string, Issue[]>;
isArchivedSearch: boolean;
};
function buildGroupedInboxSections(
items: InboxWorkItem[],
groupBy: InboxWorkItemGroupBy,
nestingEnabled: boolean,
workspaceGrouping: InboxWorkspaceGroupingOptions,
options?: { keyPrefix?: string; isArchivedSearch?: boolean },
): InboxGroupedSection[] {
const keyPrefix = options?.keyPrefix ?? "";
const isArchivedSearch = options?.isArchivedSearch ?? false;
return groupInboxWorkItems(items, groupBy, workspaceGrouping).map((group) => {
const nestedGroup = nestingEnabled && group.items.some((item) => item.kind === "issue")
? buildInboxNesting(group.items)
: { displayItems: group.items, childrenByIssueId: new Map<string, Issue[]>() };
return {
key: `${keyPrefix}${group.key}`,
label: group.label,
displayItems: nestedGroup.displayItems,
childrenByIssueId: nestedGroup.childrenByIssueId,
isArchivedSearch,
};
});
}
function firstNonEmptyLine(value: string | null | undefined): string | null {
if (!value) return null;
const line = value.split("\n").map((chunk) => chunk.trim()).find(Boolean);
@@ -1081,19 +1049,12 @@ export function Inbox() {
remoteIssueSearchResults,
],
);
const effectiveWorkItems = useMemo(
() =>
issueSearchSupplementResults.length > 0
? [
...filteredWorkItems,
...getInboxWorkItems({ issues: issueSearchSupplementResults, approvals: [] }),
]
: filteredWorkItems,
[filteredWorkItems, issueSearchSupplementResults],
);
const archivedSearchIssueIds = useMemo(
() => new Set(archivedSearchIssues.map((issue) => issue.id)),
[archivedSearchIssues],
const nonInboxSearchIssueIds = useMemo(
() => new Set([
...archivedSearchIssues.map((issue) => issue.id),
...issueSearchSupplementResults.map((issue) => issue.id),
]),
[archivedSearchIssues, issueSearchSupplementResults],
);
// --- Parent-child nesting for inbox issues ---
@@ -1123,15 +1084,27 @@ export function Inbox() {
});
}, [selectedCompanyId]);
const groupedSections = useMemo<InboxGroupedSection[]>(() => [
...buildGroupedInboxSections(effectiveWorkItems, groupBy, nestingEnabled, inboxWorkspaceGrouping),
...buildGroupedInboxSections(filteredWorkItems, groupBy, inboxWorkspaceGrouping, { nestingEnabled }),
...buildGroupedInboxSections(
getInboxWorkItems({ issues: archivedSearchIssues, approvals: [] }),
groupBy,
nestingEnabled,
inboxWorkspaceGrouping,
{ keyPrefix: "archived-search:", isArchivedSearch: true },
{ keyPrefix: "archived-search:", searchSection: "archived", nestingEnabled },
),
], [archivedSearchIssues, effectiveWorkItems, groupBy, inboxWorkspaceGrouping, nestingEnabled]);
...buildGroupedInboxSections(
getInboxWorkItems({ issues: issueSearchSupplementResults, approvals: [] }),
groupBy,
inboxWorkspaceGrouping,
{ keyPrefix: "other-search:", searchSection: "other", nestingEnabled },
),
], [
archivedSearchIssues,
filteredWorkItems,
groupBy,
inboxWorkspaceGrouping,
issueSearchSupplementResults,
nestingEnabled,
]);
const totalVisibleWorkItems = useMemo(
() => groupedSections.reduce((count, group) => count + group.displayItems.length, 0),
[groupedSections],
@@ -1500,7 +1473,7 @@ export function Inbox() {
flatNavItems,
selectedIndex,
canArchive: canArchiveFromTab,
archivedSearchIssueIds,
nonInboxSearchIssueIds,
archivingIssueIds,
undoableArchiveIssueIds,
unarchivingIssueIds,
@@ -1513,7 +1486,7 @@ export function Inbox() {
flatNavItems,
selectedIndex,
canArchive: canArchiveFromTab,
archivedSearchIssueIds,
nonInboxSearchIssueIds,
archivingIssueIds,
undoableArchiveIssueIds,
unarchivingIssueIds,
@@ -1616,10 +1589,10 @@ export function Inbox() {
e.preventDefault();
const { issue, item } = resolveNavEntry(st.selectedIndex);
if (issue) {
if (!st.archivedSearchIssueIds.has(issue.id) && !st.archivingIssueIds.has(issue.id)) act.archiveIssue(issue.id);
if (!st.nonInboxSearchIssueIds.has(issue.id) && !st.archivingIssueIds.has(issue.id)) act.archiveIssue(issue.id);
} else if (item) {
if (item.kind === "issue") {
if (!st.archivedSearchIssueIds.has(item.issue.id) && !st.archivingIssueIds.has(item.issue.id)) {
if (!st.nonInboxSearchIssueIds.has(item.issue.id) && !st.archivingIssueIds.has(item.issue.id)) {
act.archiveIssue(item.issue.id);
}
} else {
@@ -2113,15 +2086,18 @@ export function Inbox() {
return groupedSections.flatMap((group, groupIndex) => {
const elements: ReactNode[] = [];
const isGroupCollapsed = collapsedGroupKeys.has(group.key);
if (group.isArchivedSearch && (groupIndex === 0 || !groupedSections[groupIndex - 1]?.isArchivedSearch)) {
if (
group.searchSection !== "none"
&& group.searchSection !== groupedSections[groupIndex - 1]?.searchSection
) {
elements.push(
<div
key="archived-search-divider"
key={`${group.searchSection}-search-divider`}
className="flex items-center gap-3 border-y border-border/70 bg-muted/30 px-4 py-2"
>
<div className="h-px flex-1 bg-border/80" />
<span className="shrink-0 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">
Archived
{group.searchSection === "archived" ? "Archived" : "Other results"}
</span>
<div className="h-px flex-1 bg-border/80" />
</div>,
@@ -2292,7 +2268,7 @@ export function Inbox() {
const childIssues = group.childrenByIssueId.get(issue.id) ?? [];
const hasChildren = childIssues.length > 0;
const isExpanded = hasChildren && !collapsedInboxParents.has(issue.id);
const canArchiveIssue = canArchiveFromTab && !group.isArchivedSearch;
const canArchiveIssue = canArchiveFromTab && group.searchSection === "none";
const parentRow = renderInboxIssue({
issue,
depth: 0,