fix: use CSS table layout for column alignment
Replaced flex containers with display: table/table-row/table-cell. This guarantees column widths are shared between header and all rows, fixing the misalignment. Subject column gets remaining width, all others use fixed pixel widths. Resize handles on header cells still work. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -882,132 +882,143 @@ function TicketWorkbenchContent() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Column header */}
|
{/* Table layout for consistent column alignment */}
|
||||||
<div className={cn(
|
<div style={{ display: "table", width: "100%" }}>
|
||||||
"sticky top-0 z-10 flex border-b border-border bg-muted/50",
|
{/* Column header */}
|
||||||
density === "compact" ? "min-h-7" : "min-h-8"
|
<div className={cn(
|
||||||
)}>
|
"sticky top-0 z-10 border-b border-border bg-muted/50",
|
||||||
<div className="w-9 shrink-0" />
|
density === "compact" ? "min-h-7" : "min-h-8"
|
||||||
{availableColumns.filter((c) => c.visible).map((col) => (
|
)} style={{ display: "table-row" }}>
|
||||||
<div
|
<div style={{ display: "table-cell", width: 36 }} />
|
||||||
key={col.key}
|
{availableColumns.filter((c) => c.visible).map((col) => (
|
||||||
className="relative flex shrink-0 items-center gap-1 border-r border-border/60 px-3 last:border-r-0"
|
|
||||||
style={{ width: col.key === "subject" ? undefined : col.width, flex: col.key === "subject" ? 1 : undefined, minWidth: col.key === "subject" ? 200 : undefined }}
|
|
||||||
>
|
|
||||||
<span className="text-[11px] font-semibold uppercase text-muted-foreground truncate">
|
|
||||||
{col.label}
|
|
||||||
</span>
|
|
||||||
{/* Resize handle */}
|
|
||||||
<div
|
<div
|
||||||
className="absolute right-0 top-0 h-full w-1 cursor-col-resize hover:bg-primary/40"
|
key={col.key}
|
||||||
onMouseDown={(e) => handleColumnResize(col.key, e)}
|
className="relative border-r border-border/60 px-3 align-middle last:border-r-0"
|
||||||
/>
|
style={{ display: "table-cell", width: col.key === "subject" ? undefined : col.width }}
|
||||||
</div>
|
>
|
||||||
))}
|
<span className="text-[10px] font-semibold uppercase text-muted-foreground/60 truncate block">
|
||||||
<div className="w-12 shrink-0" />
|
{col.label}
|
||||||
</div>
|
</span>
|
||||||
|
<div
|
||||||
{filteredTickets.map((ticket) => {
|
className="absolute right-0 top-0 h-full w-1 cursor-col-resize hover:bg-primary/40"
|
||||||
const selected = ticket.id === selectedId;
|
onMouseDown={(e) => handleColumnResize(col.key, e)}
|
||||||
const ownerName = ticket.owner_id
|
|
||||||
? users.find((u) => u.id === ticket.owner_id)?.username ?? "assigned"
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={ticket.id}
|
|
||||||
type="button"
|
|
||||||
onClick={() => setSelectedId(ticket.id)}
|
|
||||||
onDoubleClick={() => router.push(`/tickets/${ticket.id}`)}
|
|
||||||
className={cn(
|
|
||||||
"flex items-center border-b border-border/80 text-left transition-colors",
|
|
||||||
density === "compact" ? "min-h-9" : "min-h-12",
|
|
||||||
selected
|
|
||||||
? "bg-accent/80 shadow-[inset_3px_0_0_var(--primary)]"
|
|
||||||
: "hover:bg-accent/45"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{/* Checkbox */}
|
|
||||||
<span className="flex w-9 shrink-0 items-center justify-center" onClick={(e) => e.stopPropagation()}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={batchIds.has(ticket.id)}
|
|
||||||
onChange={() => toggleBatchId(ticket.id)}
|
|
||||||
className="h-3.5 w-3.5 rounded border-border accent-primary"
|
|
||||||
/>
|
/>
|
||||||
</span>
|
</div>
|
||||||
{availableColumns.filter((c) => c.visible).map((col) => {
|
))}
|
||||||
if (col.key.startsWith("cf.")) {
|
<div style={{ display: "table-cell", width: 48 }} />
|
||||||
const cfKey = col.key.slice(3);
|
</div>
|
||||||
const cfValue = ticket.custom_fields?.find((v) => v.custom_field?.key === cfKey || v.custom_field?.name === cfKey)?.value;
|
|
||||||
return (
|
{filteredTickets.map((ticket) => {
|
||||||
<span key={col.key} className="shrink-0 truncate px-3 text-sm text-foreground" style={{ width: col.width }}>
|
const selected = ticket.id === selectedId;
|
||||||
{cfValue ?? "—"}
|
const ownerName = ticket.owner_id
|
||||||
</span>
|
? users.find((u) => u.id === ticket.owner_id)?.username ?? "assigned"
|
||||||
);
|
: null;
|
||||||
}
|
|
||||||
switch (col.key) {
|
return (
|
||||||
case "id":
|
<div
|
||||||
|
key={ticket.id}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => setSelectedId(ticket.id)}
|
||||||
|
onDoubleClick={() => router.push(`/tickets/${ticket.id}`)}
|
||||||
|
onKeyDown={(e) => { if (e.key === "Enter") router.push(`/tickets/${ticket.id}`); }}
|
||||||
|
className={cn(
|
||||||
|
"cursor-pointer border-b border-border/80",
|
||||||
|
density === "compact" ? "" : "",
|
||||||
|
selected
|
||||||
|
? "bg-accent/80 shadow-[inset_3px_0_0_var(--primary)]"
|
||||||
|
: "hover:bg-accent/45"
|
||||||
|
)}
|
||||||
|
style={{ display: "table-row" }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center" style={{ display: "table-cell", width: 36, verticalAlign: "middle" }} onClick={(e) => e.stopPropagation()}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={batchIds.has(ticket.id)}
|
||||||
|
onChange={() => toggleBatchId(ticket.id)}
|
||||||
|
className="h-3.5 w-3.5 rounded border-border accent-primary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{availableColumns.filter((c) => c.visible).map((col) => {
|
||||||
|
const cellStyle = {
|
||||||
|
display: "table-cell" as const,
|
||||||
|
width: col.key === "subject" ? undefined : col.width,
|
||||||
|
verticalAlign: "middle" as const,
|
||||||
|
padding: density === "compact" ? "4px 12px" : "8px 12px",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (col.key.startsWith("cf.")) {
|
||||||
|
const cfKey = col.key.slice(3);
|
||||||
|
const cfValue = ticket.custom_fields?.find((v) => v.custom_field?.key === cfKey || v.custom_field?.name === cfKey)?.value;
|
||||||
return (
|
return (
|
||||||
<span key={col.key} className="shrink-0 px-3 font-mono text-xs font-semibold text-muted-foreground" style={{ width: col.width }}>
|
<div key={col.key} className="truncate text-sm text-foreground" style={cellStyle}>
|
||||||
{formatTicketId(ticket.id)}
|
{cfValue ?? "—"}
|
||||||
</span>
|
</div>
|
||||||
);
|
);
|
||||||
case "subject":
|
}
|
||||||
return (
|
switch (col.key) {
|
||||||
<span key={col.key} className="min-w-0 flex-1 px-3" style={{ minWidth: 200 }}>
|
case "id":
|
||||||
<span className="block truncate text-sm font-semibold text-foreground">
|
return (
|
||||||
{ticket.subject}
|
<div key={col.key} className="font-mono text-xs font-semibold text-muted-foreground" style={cellStyle}>
|
||||||
</span>
|
{formatTicketId(ticket.id)}
|
||||||
{density === "comfortable" && (
|
</div>
|
||||||
<span className="mt-0.5 flex items-center gap-2 text-xs text-muted-foreground">
|
);
|
||||||
{ownerName ?? "Unassigned"}
|
case "subject":
|
||||||
<span className="h-1 w-1 rounded-full bg-border" />
|
return (
|
||||||
Created {relativeTime(ticket.created_at)}
|
<div key={col.key} className="min-w-[200px]" style={cellStyle}>
|
||||||
|
<span className="block truncate text-sm font-semibold text-foreground">
|
||||||
|
{ticket.subject}
|
||||||
</span>
|
</span>
|
||||||
)}
|
{density === "comfortable" && (
|
||||||
</span>
|
<span className="mt-0.5 flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
);
|
{ownerName ?? "Unassigned"}
|
||||||
case "status":
|
<span className="h-1 w-1 rounded-full bg-border" />
|
||||||
return (
|
Created {relativeTime(ticket.created_at)}
|
||||||
<span key={col.key} className="shrink-0 px-3" style={{ width: col.width }}>
|
</span>
|
||||||
<TicketStatusBadge status={ticket.status} />
|
)}
|
||||||
</span>
|
</div>
|
||||||
);
|
);
|
||||||
case "queue":
|
case "status":
|
||||||
return (
|
return (
|
||||||
<span key={col.key} className="shrink-0 truncate px-3 text-sm font-medium text-muted-foreground" style={{ width: col.width }}>
|
<div key={col.key} style={cellStyle}>
|
||||||
{queueName(queues, ticket.queue_id)}
|
<TicketStatusBadge status={ticket.status} />
|
||||||
</span>
|
</div>
|
||||||
);
|
);
|
||||||
case "owner":
|
case "queue":
|
||||||
return (
|
return (
|
||||||
<span key={col.key} className="shrink-0 truncate px-3 text-sm text-foreground" style={{ width: col.width }}>
|
<div key={col.key} className="truncate text-sm font-medium text-muted-foreground" style={cellStyle}>
|
||||||
{ownerName ?? "—"}
|
{queueName(queues, ticket.queue_id)}
|
||||||
</span>
|
</div>
|
||||||
);
|
);
|
||||||
case "created":
|
case "owner":
|
||||||
return (
|
return (
|
||||||
<span key={col.key} className="shrink-0 px-3 text-xs text-muted-foreground" style={{ width: col.width }}>
|
<div key={col.key} className="truncate text-sm text-foreground" style={cellStyle}>
|
||||||
{relativeTime(ticket.created_at)}
|
{ownerName ?? "—"}
|
||||||
</span>
|
</div>
|
||||||
);
|
);
|
||||||
case "updated":
|
case "created":
|
||||||
return (
|
return (
|
||||||
<span key={col.key} className="shrink-0 px-3 text-xs text-muted-foreground" style={{ width: col.width }}>
|
<div key={col.key} className="text-xs text-muted-foreground" style={cellStyle}>
|
||||||
{relativeTime(ticket.updated_at)}
|
{relativeTime(ticket.created_at)}
|
||||||
</span>
|
</div>
|
||||||
);
|
);
|
||||||
default:
|
case "updated":
|
||||||
return <span key={col.key} className="shrink-0 px-3" style={{ width: col.width }} />;
|
return (
|
||||||
}
|
<div key={col.key} className="text-xs text-muted-foreground" style={cellStyle}>
|
||||||
})}
|
{relativeTime(ticket.updated_at)}
|
||||||
<span className="flex w-12 shrink-0 justify-end px-2 text-muted-foreground">
|
</div>
|
||||||
<ChevronRightIcon className="h-4 w-4" />
|
);
|
||||||
</span>
|
default:
|
||||||
</button>
|
return <div key={col.key} style={cellStyle} />;
|
||||||
);
|
}
|
||||||
})}
|
})}
|
||||||
|
<div className="flex justify-end px-2 text-muted-foreground" style={{ display: "table-cell", width: 48, verticalAlign: "middle" }}>
|
||||||
|
<ChevronRightIcon className="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user