diff --git a/ui/src/lib/inbox.ts b/ui/src/lib/inbox.ts index d2f9c1be..925870cb 100644 --- a/ui/src/lib/inbox.ts +++ b/ui/src/lib/inbox.ts @@ -7,6 +7,7 @@ export const DISMISSED_KEY = "paperclip:inbox:dismissed"; export const READ_ITEMS_KEY = "paperclip:inbox:read-items"; export const INBOX_LAST_TAB_KEY = "paperclip:inbox:last-tab"; export const INBOX_ISSUE_COLUMNS_KEY = "paperclip:inbox:issue-columns"; +export const INBOX_NESTING_KEY = "paperclip:inbox:nesting"; export type InboxTab = "mine" | "recent" | "unread" | "all"; export type InboxApprovalFilter = "all" | "actionable" | "resolved"; export const inboxIssueColumns = ["status", "id", "assignee", "project", "workspace", "parent", "labels", "updated"] as const; @@ -151,6 +152,23 @@ export function resolveIssueWorkspaceName( return null; } +export function loadInboxNesting(): boolean { + try { + const raw = localStorage.getItem(INBOX_NESTING_KEY); + return raw !== "false"; + } catch { + return true; + } +} + +export function saveInboxNesting(enabled: boolean) { + try { + localStorage.setItem(INBOX_NESTING_KEY, String(enabled)); + } catch { + // Ignore localStorage failures. + } +} + export function loadLastInboxTab(): InboxTab { try { const raw = localStorage.getItem(INBOX_LAST_TAB_KEY); diff --git a/ui/src/pages/Inbox.tsx b/ui/src/pages/Inbox.tsx index d6872ff9..79358364 100644 --- a/ui/src/pages/Inbox.tsx +++ b/ui/src/pages/Inbox.tsx @@ -68,6 +68,7 @@ import { RotateCcw, UserPlus, Search, + ListTree, } from "lucide-react"; import { Input } from "@/components/ui/input"; import { PageTabBar } from "../components/PageTabBar"; @@ -84,10 +85,12 @@ import { getRecentTouchedIssues, isMineInboxTab, loadInboxIssueColumns, + loadInboxNesting, normalizeInboxIssueColumns, resolveIssueWorkspaceName, resolveInboxSelectionIndex, saveInboxIssueColumns, + saveInboxNesting, InboxApprovalFilter, type InboxIssueColumn, saveLastInboxTab, @@ -110,6 +113,11 @@ type SectionKey = | "work_items" | "alerts"; +/** A flat navigation entry for keyboard j/k traversal that includes expanded children. */ +type NavEntry = + | { type: "top"; index: number; item: InboxWorkItem } + | { type: "child"; parentIndex: number; issue: Issue }; + function firstNonEmptyLine(value: string | null | undefined): string | null { if (!value) return null; const line = value.split("\n").map((chunk) => chunk.trim()).find(Boolean); @@ -903,10 +911,20 @@ export function Inbox() { ]); // --- Parent-child nesting for inbox issues --- + const [nestingEnabled, setNestingEnabled] = useState(() => loadInboxNesting()); + const toggleNesting = useCallback(() => { + setNestingEnabled((prev) => { + const next = !prev; + saveInboxNesting(next); + return next; + }); + }, []); const [collapsedInboxParents, setCollapsedInboxParents] = useState>(new Set()); const { displayItems: nestedWorkItems, childrenByIssueId } = useMemo( - () => buildInboxNesting(filteredWorkItems), - [filteredWorkItems], + () => nestingEnabled + ? buildInboxNesting(filteredWorkItems) + : { displayItems: filteredWorkItems, childrenByIssueId: new Map() }, + [filteredWorkItems, nestingEnabled], ); const toggleInboxParentCollapse = useCallback((parentId: string) => { setCollapsedInboxParents((prev) => { @@ -1429,6 +1447,16 @@ export function Inbox() { className="h-8 w-[220px] pl-8 text-xs" /> +