diff --git a/ui/src/lib/inbox.test.ts b/ui/src/lib/inbox.test.ts index 8759d914761..37f0fc0fd5e 100644 --- a/ui/src/lib/inbox.test.ts +++ b/ui/src/lib/inbox.test.ts @@ -31,6 +31,7 @@ import { loadInboxFilterPreferences, loadInboxIssueColumns, loadInboxWorkItemGroupBy, + loadCollapsedInboxGroupKeys, loadLastInboxTab, matchesInboxIssueSearch, normalizeInboxIssueColumns, @@ -40,6 +41,7 @@ import { resolveIssueWorkspaceGroup, resolveInboxSelectionIndex, saveInboxFilterPreferences, + saveCollapsedInboxGroupKeys, saveInboxIssueColumns, saveInboxWorkItemGroupBy, saveLastInboxTab, @@ -1081,6 +1083,20 @@ describe("inbox helpers", () => { expect(loadInboxWorkItemGroupBy()).toBe("workspace"); }); + it("persists collapsed inbox groups per company", () => { + saveCollapsedInboxGroupKeys("company-1", new Set(["workspace:alpha", "workspace:beta"])); + saveCollapsedInboxGroupKeys("company-2", new Set(["type:approval"])); + + expect(loadCollapsedInboxGroupKeys("company-1")).toEqual(new Set(["workspace:alpha", "workspace:beta"])); + expect(loadCollapsedInboxGroupKeys("company-2")).toEqual(new Set(["type:approval"])); + }); + + it("returns empty collapsed inbox groups for missing or invalid storage", () => { + expect(loadCollapsedInboxGroupKeys("company-1")).toEqual(new Set()); + localStorage.setItem("paperclip:inbox:collapsed-groups:company-1", JSON.stringify({ nope: true })); + expect(loadCollapsedInboxGroupKeys("company-1")).toEqual(new Set()); + }); + it("does not reset workspace grouping before experimental settings have loaded", () => { expect(shouldResetInboxWorkspaceGrouping("workspace", false, false)).toBe(false); }); diff --git a/ui/src/lib/inbox.ts b/ui/src/lib/inbox.ts index 9f1373990b5..9060d0a2026 100644 --- a/ui/src/lib/inbox.ts +++ b/ui/src/lib/inbox.ts @@ -22,6 +22,7 @@ export const INBOX_ISSUE_COLUMNS_KEY = "paperclip:inbox:issue-columns"; export const INBOX_NESTING_KEY = "paperclip:inbox:nesting"; export const INBOX_GROUP_BY_KEY = "paperclip:inbox:group-by"; export const INBOX_FILTER_PREFERENCES_KEY_PREFIX = "paperclip:inbox:filters"; +export const INBOX_COLLAPSED_GROUPS_KEY_PREFIX = "paperclip:inbox:collapsed-groups"; export type InboxTab = "mine" | "recent" | "unread" | "all"; export type InboxCategoryFilter = | "everything" @@ -164,6 +165,11 @@ function getInboxFilterPreferencesStorageKey(companyId: string | null | undefine return `${INBOX_FILTER_PREFERENCES_KEY_PREFIX}:${companyId}`; } +function getInboxCollapsedGroupsStorageKey(companyId: string | null | undefined): string | null { + if (!companyId) return null; + return `${INBOX_COLLAPSED_GROUPS_KEY_PREFIX}:${companyId}`; +} + export function loadInboxFilterPreferences( companyId: string | null | undefined, ): InboxFilterPreferences { @@ -218,6 +224,36 @@ export function saveInboxFilterPreferences( } } +export function loadCollapsedInboxGroupKeys( + companyId: string | null | undefined, +): Set { + const storageKey = getInboxCollapsedGroupsStorageKey(companyId); + if (!storageKey) return new Set(); + + try { + const raw = localStorage.getItem(storageKey); + if (!raw) return new Set(); + const parsed = JSON.parse(raw); + return new Set(normalizeStringArray(parsed)); + } catch { + return new Set(); + } +} + +export function saveCollapsedInboxGroupKeys( + companyId: string | null | undefined, + groupKeys: ReadonlySet, +) { + const storageKey = getInboxCollapsedGroupsStorageKey(companyId); + if (!storageKey) return; + + try { + localStorage.setItem(storageKey, JSON.stringify([...groupKeys])); + } catch { + // Ignore localStorage failures. + } +} + export function loadDismissedInboxAlerts(): Set { try { const raw = localStorage.getItem(DISMISSED_KEY); diff --git a/ui/src/pages/Inbox.tsx b/ui/src/pages/Inbox.tsx index e9f0ec8d6e4..19d2a3917e4 100644 --- a/ui/src/pages/Inbox.tsx +++ b/ui/src/pages/Inbox.tsx @@ -110,6 +110,7 @@ import { groupInboxWorkItems, isInboxEntityDismissed, isMineInboxTab, + loadCollapsedInboxGroupKeys, loadInboxFilterPreferences, loadInboxIssueColumns, loadInboxNesting, @@ -120,6 +121,7 @@ import { resolveIssueWorkspaceName, resolveInboxSelectionIndex, saveInboxFilterPreferences, + saveCollapsedInboxGroupKeys, saveInboxIssueColumns, saveInboxNesting, saveInboxWorkItemGroupBy, @@ -724,6 +726,7 @@ export function Inbox() { if (previousSelectedCompanyIdRef.current !== selectedCompanyId) { previousSelectedCompanyIdRef.current = selectedCompanyId; setFilterPreferences(loadInboxFilterPreferences(selectedCompanyId)); + setCollapsedGroupKeys(loadCollapsedInboxGroupKeys(selectedCompanyId)); } }, [selectedCompanyId]); @@ -1107,15 +1110,16 @@ export function Inbox() { }); }, []); const [collapsedInboxParents, setCollapsedInboxParents] = useState>(new Set()); - const [collapsedGroupKeys, setCollapsedGroupKeys] = useState>(new Set()); + const [collapsedGroupKeys, setCollapsedGroupKeys] = useState>(() => loadCollapsedInboxGroupKeys(selectedCompanyId)); const toggleGroupCollapse = useCallback((groupKey: string) => { setCollapsedGroupKeys((prev) => { const next = new Set(prev); if (next.has(groupKey)) next.delete(groupKey); else next.add(groupKey); + saveCollapsedInboxGroupKeys(selectedCompanyId, next); return next; }); - }, []); + }, [selectedCompanyId]); const groupedSections = useMemo(() => [ ...buildGroupedInboxSections(effectiveWorkItems, groupBy, nestingEnabled, inboxWorkspaceGrouping), ...buildGroupedInboxSections(