Rebalance issue filter popover layout

This commit is contained in:
Dotta committed 2026-04-14 11:51:38 -05:00
1 parent 3a02a7de49
commit 84f0ffb9dd
2 files changed
+124 -34

No files matched your search

@@ -0,0 +1,83 @@
// @vitest-environment jsdom
import { act } from "react";
import type { ReactNode } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { IssueFiltersPopover } from "./IssueFiltersPopover";
import { defaultIssueFilterState } from "../lib/issue-filters";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
vi.mock("@/components/ui/popover", () => ({
Popover: ({ children }: { children: ReactNode }) => <div>{children}</div>,
PopoverTrigger: ({ children }: { children: ReactNode }) => <>{children}</>,
PopoverContent: ({ children, className }: { children: ReactNode; className?: string }) => (
<div data-testid="popover-content" className={className}>
{children}
</div>
),
}));
vi.mock("@/components/ui/button", () => ({
Button: ({ children, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement>) => (
<button type="button" {...props}>
{children}
</button>
),
}));
vi.mock("@/components/ui/checkbox", () => ({
Checkbox: ({ checked }: { checked?: boolean }) => <input type="checkbox" checked={checked} readOnly />,
}));
vi.mock("./StatusIcon", () => ({
StatusIcon: ({ status }: { status: string }) => <span>{status}</span>,
}));
vi.mock("./PriorityIcon", () => ({
PriorityIcon: ({ priority }: { priority: string }) => <span>{priority}</span>,
}));
describe("IssueFiltersPopover", () => {
let container: HTMLDivElement;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
});
afterEach(() => {
document.body.innerHTML = "";
});
it("uses a scrollable popover and a three-column desktop grid", () => {
const root = createRoot(container);
act(() => {
root.render(
<IssueFiltersPopover
state={defaultIssueFilterState}
onChange={vi.fn()}
activeFilterCount={0}
agents={[{ id: "agent-1", name: "Agent One" }]}
projects={[{ id: "project-1", name: "Project One" }]}
labels={[{ id: "label-1", name: "Bug", color: "#ff0000" }]}
workspaces={[{ id: "workspace-1", name: "Workspace One" }]}
enableRoutineVisibilityFilter
/>,
);
});
const popoverContent = container.querySelector("[data-testid='popover-content']");
expect(popoverContent).not.toBeNull();
expect(popoverContent?.className).toContain("overflow-y-auto");
expect(popoverContent?.className).toContain("max-h-[min(80vh,42rem)]");
const layoutGrid = Array.from(popoverContent?.querySelectorAll("div") ?? []).find((element) =>
element.className.includes("md:grid-cols-3"),
);
expect(layoutGrid?.className).toContain("grid-cols-1");
});
});
+41 -34
View File
@@ -80,7 +80,10 @@ export function IssueFiltersPopover({
) : null}
</Button>
</PopoverTrigger>
<PopoverContent align="end" className="w-[min(480px,calc(100vw-2rem))] p-0">
<PopoverContent
align="end"
className="w-[min(780px,calc(100vw-2rem))] max-h-[min(80vh,42rem)] overflow-y-auto overscroll-contain p-0"
>
<div className="space-y-3 p-3">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Filters</span>
@@ -120,24 +123,24 @@ export function IssueFiltersPopover({
<div className="border-t border-border" />
<div className="grid grid-cols-1 gap-x-4 gap-y-3 sm:grid-cols-2">
<div className="space-y-1">
<span className="text-xs text-muted-foreground">Status</span>
<div className="space-y-0.5">
{issueStatusOrder.map((status) => (
<label key={status} className="flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1 hover:bg-accent/50">
<Checkbox
checked={state.statuses.includes(status)}
onCheckedChange={() => onChange({ statuses: toggleIssueFilterValue(state.statuses, status) })}
/>
<StatusIcon status={status} />
<span className="text-sm">{issueFilterLabel(status)}</span>
</label>
))}
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
<div className="min-w-0 space-y-3">
<div className="space-y-1">
<span className="text-xs text-muted-foreground">Status</span>
<div className="space-y-0.5">
{issueStatusOrder.map((status) => (
<label key={status} className="flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1 hover:bg-accent/50">
<Checkbox
checked={state.statuses.includes(status)}
onCheckedChange={() => onChange({ statuses: toggleIssueFilterValue(state.statuses, status) })}
/>
<StatusIcon status={status} />
<span className="text-sm">{issueFilterLabel(status)}</span>
</label>
))}
</div>
</div>
</div>
<div className="space-y-3">
<div className="space-y-1">
<span className="text-xs text-muted-foreground">Priority</span>
<div className="space-y-0.5">
@@ -153,7 +156,9 @@ export function IssueFiltersPopover({
))}
</div>
</div>
</div>
<div className="min-w-0 space-y-3">
<div className="space-y-1">
<span className="text-xs text-muted-foreground">Assignee</span>
<div className="max-h-32 space-y-0.5 overflow-y-auto">
@@ -186,6 +191,25 @@ export function IssueFiltersPopover({
</div>
</div>
{projects && projects.length > 0 ? (
<div className="space-y-1">
<span className="text-xs text-muted-foreground">Project</span>
<div className="max-h-32 space-y-0.5 overflow-y-auto">
{projects.map((project) => (
<label key={project.id} className="flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1 hover:bg-accent/50">
<Checkbox
checked={state.projects.includes(project.id)}
onCheckedChange={() => onChange({ projects: toggleIssueFilterValue(state.projects, project.id) })}
/>
<span className="text-sm">{project.name}</span>
</label>
))}
</div>
</div>
) : null}
</div>
<div className="min-w-0 space-y-3">
{labels && labels.length > 0 ? (
<div className="space-y-1">
<span className="text-xs text-muted-foreground">Labels</span>
@@ -204,23 +228,6 @@ export function IssueFiltersPopover({
</div>
) : null}
{projects && projects.length > 0 ? (
<div className="space-y-1">
<span className="text-xs text-muted-foreground">Project</span>
<div className="max-h-32 space-y-0.5 overflow-y-auto">
{projects.map((project) => (
<label key={project.id} className="flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1 hover:bg-accent/50">
<Checkbox
checked={state.projects.includes(project.id)}
onCheckedChange={() => onChange({ projects: toggleIssueFilterValue(state.projects, project.id) })}
/>
<span className="text-sm">{project.name}</span>
</label>
))}
</div>
</div>
) : null}
{workspaces && workspaces.length > 0 ? (
<div className="space-y-1">
<span className="text-xs text-muted-foreground">Workspace</span>