mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-18 11:40:39 +09:00
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:
parent
097f30b138
commit
d3e66c789e
2 changed files with 48 additions and 2 deletions
|
|
@ -7,6 +7,7 @@ export const DISMISSED_KEY = "paperclip:inbox:dismissed";
|
||||||
export const READ_ITEMS_KEY = "paperclip:inbox:read-items";
|
export const READ_ITEMS_KEY = "paperclip:inbox:read-items";
|
||||||
export const INBOX_LAST_TAB_KEY = "paperclip:inbox:last-tab";
|
export const INBOX_LAST_TAB_KEY = "paperclip:inbox:last-tab";
|
||||||
export const INBOX_ISSUE_COLUMNS_KEY = "paperclip:inbox:issue-columns";
|
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 InboxTab = "mine" | "recent" | "unread" | "all";
|
||||||
export type InboxApprovalFilter = "all" | "actionable" | "resolved";
|
export type InboxApprovalFilter = "all" | "actionable" | "resolved";
|
||||||
export const inboxIssueColumns = ["status", "id", "assignee", "project", "workspace", "parent", "labels", "updated"] as const;
|
export const inboxIssueColumns = ["status", "id", "assignee", "project", "workspace", "parent", "labels", "updated"] as const;
|
||||||
|
|
@ -151,6 +152,23 @@ export function resolveIssueWorkspaceName(
|
||||||
return null;
|
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 {
|
export function loadLastInboxTab(): InboxTab {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(INBOX_LAST_TAB_KEY);
|
const raw = localStorage.getItem(INBOX_LAST_TAB_KEY);
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ import {
|
||||||
RotateCcw,
|
RotateCcw,
|
||||||
UserPlus,
|
UserPlus,
|
||||||
Search,
|
Search,
|
||||||
|
ListTree,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { PageTabBar } from "../components/PageTabBar";
|
import { PageTabBar } from "../components/PageTabBar";
|
||||||
|
|
@ -84,10 +85,12 @@ import {
|
||||||
getRecentTouchedIssues,
|
getRecentTouchedIssues,
|
||||||
isMineInboxTab,
|
isMineInboxTab,
|
||||||
loadInboxIssueColumns,
|
loadInboxIssueColumns,
|
||||||
|
loadInboxNesting,
|
||||||
normalizeInboxIssueColumns,
|
normalizeInboxIssueColumns,
|
||||||
resolveIssueWorkspaceName,
|
resolveIssueWorkspaceName,
|
||||||
resolveInboxSelectionIndex,
|
resolveInboxSelectionIndex,
|
||||||
saveInboxIssueColumns,
|
saveInboxIssueColumns,
|
||||||
|
saveInboxNesting,
|
||||||
InboxApprovalFilter,
|
InboxApprovalFilter,
|
||||||
type InboxIssueColumn,
|
type InboxIssueColumn,
|
||||||
saveLastInboxTab,
|
saveLastInboxTab,
|
||||||
|
|
@ -110,6 +113,11 @@ type SectionKey =
|
||||||
| "work_items"
|
| "work_items"
|
||||||
| "alerts";
|
| "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 {
|
function firstNonEmptyLine(value: string | null | undefined): string | null {
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
const line = value.split("\n").map((chunk) => chunk.trim()).find(Boolean);
|
const line = value.split("\n").map((chunk) => chunk.trim()).find(Boolean);
|
||||||
|
|
@ -903,10 +911,20 @@ export function Inbox() {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// --- Parent-child nesting for inbox issues ---
|
// --- 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 [collapsedInboxParents, setCollapsedInboxParents] = useState<Set<string>>(new Set());
|
||||||
const { displayItems: nestedWorkItems, childrenByIssueId } = useMemo(
|
const { displayItems: nestedWorkItems, childrenByIssueId } = useMemo(
|
||||||
() => buildInboxNesting(filteredWorkItems),
|
() => nestingEnabled
|
||||||
[filteredWorkItems],
|
? buildInboxNesting(filteredWorkItems)
|
||||||
|
: { displayItems: filteredWorkItems, childrenByIssueId: new Map<string, Issue[]>() },
|
||||||
|
[filteredWorkItems, nestingEnabled],
|
||||||
);
|
);
|
||||||
const toggleInboxParentCollapse = useCallback((parentId: string) => {
|
const toggleInboxParentCollapse = useCallback((parentId: string) => {
|
||||||
setCollapsedInboxParents((prev) => {
|
setCollapsedInboxParents((prev) => {
|
||||||
|
|
@ -1429,6 +1447,16 @@ export function Inbox() {
|
||||||
className="h-8 w-[220px] pl-8 text-xs"
|
className="h-8 w-[220px] pl-8 text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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
|
<IssueColumnPicker
|
||||||
availableColumns={availableIssueColumns}
|
availableColumns={availableIssueColumns}
|
||||||
visibleColumnSet={visibleIssueColumnSet}
|
visibleColumnSet={visibleIssueColumnSet}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue