mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 18:19:05 +02:00
Add mobile collapse for inbox workspace groups
This commit is contained in:
1 parent
dca6823dee
commit
f270f30799
2 files changed
+109
-8
No files matched your search
@@ -5,7 +5,7 @@ import type { ComponentProps } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import type { Issue } from "@paperclipai/shared";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { FailedRunInboxRow, InboxIssueMetaLeading, InboxIssueTrailingColumns } from "./Inbox";
|
||||
import { FailedRunInboxRow, InboxGroupHeader, InboxIssueMetaLeading, InboxIssueTrailingColumns } from "./Inbox";
|
||||
|
||||
vi.mock("@/lib/router", () => ({
|
||||
Link: ({ children, className, ...props }: ComponentProps<"a">) => (
|
||||
@@ -245,3 +245,52 @@ describe("InboxIssueTrailingColumns", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("InboxGroupHeader", () => {
|
||||
let container: HTMLDivElement;
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
container.remove();
|
||||
});
|
||||
|
||||
it("shows a left caret and expanded state for collapsible mobile headers", () => {
|
||||
const root = createRoot(container);
|
||||
|
||||
act(() => {
|
||||
root.render(<InboxGroupHeader label="Primary workspace (default)" collapsible collapsed={false} />);
|
||||
});
|
||||
|
||||
const button = container.querySelector("button");
|
||||
expect(button).not.toBeNull();
|
||||
expect(button?.getAttribute("aria-expanded")).toBe("true");
|
||||
expect(button?.textContent).toContain("Primary workspace (default)");
|
||||
const caret = container.querySelector("svg");
|
||||
expect(caret?.className.baseVal).toContain("rotate-90");
|
||||
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps the caret collapsed when the mobile group is hidden", () => {
|
||||
const root = createRoot(container);
|
||||
|
||||
act(() => {
|
||||
root.render(<InboxGroupHeader label="Feature Branch" collapsible collapsed />);
|
||||
});
|
||||
|
||||
const button = container.querySelector("button");
|
||||
expect(button?.getAttribute("aria-expanded")).toBe("false");
|
||||
const caret = container.querySelector("svg");
|
||||
expect(caret?.className.baseVal).not.toContain("rotate-90");
|
||||
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
+59
-7
@@ -149,6 +149,47 @@ type InboxGroupedSection = {
|
||||
isArchivedSearch: boolean;
|
||||
};
|
||||
|
||||
export function InboxGroupHeader({
|
||||
label,
|
||||
collapsible = false,
|
||||
collapsed = false,
|
||||
onToggle,
|
||||
}: {
|
||||
label: string;
|
||||
collapsible?: boolean;
|
||||
collapsed?: boolean;
|
||||
onToggle?: () => void;
|
||||
}) {
|
||||
if (collapsible) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center gap-2 text-left"
|
||||
aria-expanded={!collapsed}
|
||||
onClick={onToggle}
|
||||
>
|
||||
<ChevronRight
|
||||
className={cn("h-3.5 w-3.5 shrink-0 transition-transform", !collapsed && "rotate-90")}
|
||||
/>
|
||||
<span className="max-w-[75vw] truncate rounded-full border border-border/70 bg-background/80 px-2.5 py-0.5 text-[11px] font-medium text-muted-foreground">
|
||||
{label}
|
||||
</span>
|
||||
<span className="h-px flex-1 bg-border/80" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<div className="h-px flex-1 bg-border/80" />
|
||||
<span className="max-w-[75vw] truncate rounded-full border border-border/70 bg-background/80 px-2.5 py-0.5 text-[11px] font-medium text-muted-foreground sm:max-w-none">
|
||||
{label}
|
||||
</span>
|
||||
<div className="h-px flex-1 bg-border/80" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function buildGroupedInboxSections(
|
||||
items: InboxWorkItem[],
|
||||
groupBy: InboxWorkItemGroupBy,
|
||||
@@ -1101,6 +1142,15 @@ export function Inbox() {
|
||||
});
|
||||
}, []);
|
||||
const [collapsedInboxParents, setCollapsedInboxParents] = useState<Set<string>>(new Set());
|
||||
const [collapsedGroupKeys, setCollapsedGroupKeys] = useState<Set<string>>(new Set());
|
||||
const toggleGroupCollapse = useCallback((groupKey: string) => {
|
||||
setCollapsedGroupKeys((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(groupKey)) next.delete(groupKey);
|
||||
else next.add(groupKey);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
const groupedSections = useMemo<InboxGroupedSection[]>(() => [
|
||||
...buildGroupedInboxSections(effectiveWorkItems, groupBy, nestingEnabled, inboxWorkspaceGrouping),
|
||||
...buildGroupedInboxSections(
|
||||
@@ -2063,6 +2113,8 @@ export function Inbox() {
|
||||
let previousTimestamp = Number.POSITIVE_INFINITY;
|
||||
return groupedSections.flatMap((group, groupIndex) => {
|
||||
const elements: ReactNode[] = [];
|
||||
const mobileWorkspaceGrouping = isMobile && groupBy === "workspace";
|
||||
const isGroupCollapsed = mobileWorkspaceGrouping && collapsedGroupKeys.has(group.key);
|
||||
if (group.isArchivedSearch && (groupIndex === 0 || !groupedSections[groupIndex - 1]?.isArchivedSearch)) {
|
||||
elements.push(
|
||||
<div
|
||||
@@ -2086,16 +2138,16 @@ export function Inbox() {
|
||||
groupIndex > 0 && "border-t border-border",
|
||||
)}
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<div className="h-px flex-1 bg-border/80" />
|
||||
<span className="max-w-[75vw] truncate rounded-full border border-border/70 bg-background/80 px-2.5 py-0.5 text-[11px] font-medium text-muted-foreground sm:max-w-none">
|
||||
{group.label}
|
||||
</span>
|
||||
<div className="h-px flex-1 bg-border/80" />
|
||||
</div>
|
||||
<InboxGroupHeader
|
||||
label={group.label}
|
||||
collapsible={mobileWorkspaceGrouping}
|
||||
collapsed={isGroupCollapsed}
|
||||
onToggle={mobileWorkspaceGrouping ? () => toggleGroupCollapse(group.key) : undefined}
|
||||
/>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
if (isGroupCollapsed) return elements;
|
||||
|
||||
for (let index = 0; index < group.displayItems.length; index += 1) {
|
||||
const item = group.displayItems[index]!;
|
||||
|
||||
Reference in new issue
Block a user