feat(ui): add toggle button for inbox parent-child nesting

Adds a ListTree icon button in the inbox top bar to toggle nesting
on/off. Preference is persisted in localStorage. When disabled, all
issues display as a flat list without grouping.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta 2026-04-08 08:07:31 -05:00
parent 097f30b138
commit d3e66c789e
2 changed files with 48 additions and 2 deletions

View file

@ -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);

View file

@ -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<Set<string>>(new Set());
const { displayItems: nestedWorkItems, childrenByIssueId } = useMemo(
() => buildInboxNesting(filteredWorkItems),
[filteredWorkItems],
() => nestingEnabled
? buildInboxNesting(filteredWorkItems)
: { displayItems: filteredWorkItems, childrenByIssueId: new Map<string, Issue[]>() },
[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"
/>
</div>
<Button
type="button"
variant="outline"
size="icon"
className={cn("hidden h-8 w-8 shrink-0 sm:inline-flex", nestingEnabled && "bg-accent")}
onClick={toggleNesting}
title={nestingEnabled ? "Disable parent-child nesting" : "Enable parent-child nesting"}
>
<ListTree className="h-3.5 w-3.5" />
</Button>
<IssueColumnPicker
availableColumns={availableIssueColumns}
visibleColumnSet={visibleIssueColumnSet}