mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 18:19:05 +02:00
Rebalance issue filter popover layout
This commit is contained in:
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");
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user