From ee82a4f2432ec865b3ad6648b8a3e898e91a5db6 Mon Sep 17 00:00:00 2001 From: dotta Date: Tue, 7 Apr 2026 16:45:57 -0500 Subject: [PATCH] Reuse inbox issue column controls in issues lists --- ui/src/components/IssueColumns.tsx | 343 +++++++++++++++++++++++ ui/src/components/IssuesList.test.tsx | 71 ++++- ui/src/components/IssuesList.tsx | 387 ++++++++++++++++---------- ui/src/pages/Inbox.tsx | 322 ++------------------- ui/src/pages/ProjectDetail.tsx | 6 + 5 files changed, 680 insertions(+), 449 deletions(-) create mode 100644 ui/src/components/IssueColumns.tsx diff --git a/ui/src/components/IssueColumns.tsx b/ui/src/components/IssueColumns.tsx new file mode 100644 index 00000000..277b1e06 --- /dev/null +++ b/ui/src/components/IssueColumns.tsx @@ -0,0 +1,343 @@ +import type { ReactNode } from "react"; +import type { Issue } from "@paperclipai/shared"; +import { Columns3 } from "lucide-react"; +import { pickTextColorForPillBg } from "@/lib/color-contrast"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { formatAssigneeUserLabel } from "../lib/assignees"; +import type { InboxIssueColumn } from "../lib/inbox"; +import { cn } from "../lib/utils"; +import { timeAgo } from "../lib/timeAgo"; +import { Identity } from "./Identity"; +import { StatusIcon } from "./StatusIcon"; + +export const issueTrailingColumns: InboxIssueColumn[] = ["assignee", "project", "workspace", "parent", "labels", "updated"]; + +const issueColumnLabels: Record = { + status: "Status", + id: "ID", + assignee: "Assignee", + project: "Project", + workspace: "Workspace", + parent: "Parent issue", + labels: "Tags", + updated: "Last updated", +}; + +const issueColumnDescriptions: Record = { + status: "Issue state chip on the left edge.", + id: "Ticket identifier like PAP-1009.", + assignee: "Assigned agent or board user.", + project: "Linked project pill with its color.", + workspace: "Execution or project workspace used for the issue.", + parent: "Parent issue identifier and title.", + labels: "Issue labels and tags.", + updated: "Latest visible activity time.", +}; + +export function issueActivityText(issue: Issue): string { + return `Updated ${timeAgo(issue.lastActivityAt ?? issue.lastExternalCommentAt ?? issue.updatedAt)}`; +} + +function issueTrailingGridTemplate(columns: InboxIssueColumn[]): string { + return columns + .map((column) => { + if (column === "assignee") return "minmax(7.5rem, 9.5rem)"; + if (column === "project") return "minmax(6.5rem, 8.5rem)"; + if (column === "workspace") return "minmax(9rem, 12rem)"; + if (column === "parent") return "minmax(5rem, 7rem)"; + if (column === "labels") return "minmax(8rem, 10rem)"; + return "minmax(4rem, 5.5rem)"; + }) + .join(" "); +} + +export function IssueColumnPicker({ + availableColumns, + visibleColumnSet, + onToggleColumn, + onResetColumns, + title, +}: { + availableColumns: InboxIssueColumn[]; + visibleColumnSet: ReadonlySet; + onToggleColumn: (column: InboxIssueColumn, enabled: boolean) => void; + onResetColumns: () => void; + title: string; +}) { + return ( + + + + + + +
+
+ Desktop issue rows +
+
+ {title} +
+
+
+ + {availableColumns.map((column) => ( + event.preventDefault()} + onCheckedChange={(checked) => onToggleColumn(column, checked === true)} + className="items-start rounded-lg px-3 py-2.5 pl-8" + > + + + {issueColumnLabels[column]} + + + {issueColumnDescriptions[column]} + + + + ))} + + + Reset defaults + status, id, updated + +
+
+ ); +} + +export function InboxIssueMetaLeading({ + issue, + isLive, + showStatus = true, + showIdentifier = true, + statusSlot, +}: { + issue: Issue; + isLive: boolean; + showStatus?: boolean; + showIdentifier?: boolean; + statusSlot?: ReactNode; +}) { + return ( + <> + {showStatus ? ( + + {statusSlot ?? } + + ) : null} + {showIdentifier ? ( + + {issue.identifier ?? issue.id.slice(0, 8)} + + ) : null} + {isLive && ( + + + + + + + Live + + + )} + + ); +} + +export function InboxIssueTrailingColumns({ + issue, + columns, + projectName, + projectColor, + workspaceName, + assigneeName, + currentUserId, + parentIdentifier, + parentTitle, + assigneeContent, +}: { + issue: Issue; + columns: InboxIssueColumn[]; + projectName: string | null; + projectColor: string | null; + workspaceName: string | null; + assigneeName: string | null; + currentUserId: string | null; + parentIdentifier: string | null; + parentTitle: string | null; + assigneeContent?: ReactNode; +}) { + const activityText = timeAgo(issue.lastActivityAt ?? issue.lastExternalCommentAt ?? issue.updatedAt); + const userLabel = formatAssigneeUserLabel(issue.assigneeUserId, currentUserId) ?? "User"; + + return ( + + {columns.map((column) => { + if (column === "assignee") { + if (assigneeContent) { + return {assigneeContent}; + } + + if (issue.assigneeAgentId) { + return ( + + + + ); + } + + if (issue.assigneeUserId) { + return ( + + {userLabel} + + ); + } + + return ( + + Unassigned + + ); + } + + if (column === "project") { + if (projectName) { + const accentColor = projectColor ?? "#64748b"; + return ( + + + {projectName} + + ); + } + + return ( + + No project + + ); + } + + if (column === "labels") { + if ((issue.labels ?? []).length > 0) { + return ( + + {(issue.labels ?? []).slice(0, 2).map((label) => ( + + {label.name} + + ))} + {(issue.labels ?? []).length > 2 ? ( + + +{(issue.labels ?? []).length - 2} + + ) : null} + + ); + } + + return