cleanup: remove side panel and broken inline expansion
Stripped back to clean table before redesign. Removed selectedId tracking, transactions fetching, and all side-panel related code. Row click now navigates directly to ticket detail. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import React, { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import {
|
||||
@@ -18,8 +18,8 @@ import {
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { createTicket, getCustomFields, getLifecycles, getQueueCustomFields, getQueues, getTickets, getUsers, getViews, createView, deleteView, getDashboards, getTicketTransactions, updateTicket } from "@/lib/api";
|
||||
import type { CustomField, Lifecycle, Queue, QueueCustomField, SavedView, Ticket, Transaction, User } from "@/lib/types";
|
||||
import { createTicket, getCustomFields, getLifecycles, getQueueCustomFields, getQueues, getTickets, getUsers, getViews, createView, deleteView, getDashboards, updateTicket } from "@/lib/api";
|
||||
import type { CustomField, Lifecycle, Queue, QueueCustomField, SavedView, Ticket, User } from "@/lib/types";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -183,8 +183,6 @@ function TicketWorkbenchContent() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selectedId, setSelectedId] = useState<number | null>(null);
|
||||
const [selectedTxs, setSelectedTxs] = useState<Transaction[]>([]);
|
||||
const [batchIds, setBatchIds] = useState<Set<number>>(new Set());
|
||||
const [batchSaving, setBatchSaving] = useState(false);
|
||||
|
||||
@@ -524,20 +522,12 @@ function TicketWorkbenchContent() {
|
||||
});
|
||||
}, [clock, filters, queues, routeQueue, searchQuery, sortKey, tickets, view]);
|
||||
|
||||
const selectedTicket =
|
||||
filteredTickets.find((ticket) => ticket.id === selectedId) ?? null;
|
||||
|
||||
// Fetch transactions when selection changes
|
||||
useEffect(() => {
|
||||
if (!selectedTicket) { setSelectedTxs([]); return; }
|
||||
getTicketTransactions(selectedTicket.id).then(({ data }) => setSelectedTxs(data ?? []));
|
||||
}, [selectedTicket?.id]);
|
||||
|
||||
const handleQuickStatus = async (ticketId: number, newStatus: string) => {
|
||||
const { data } = await updateTicket(ticketId, { status: newStatus });
|
||||
if (data) {
|
||||
setTickets((prev) => prev.map((t) => (t.id === ticketId ? data.ticket : t)));
|
||||
getTicketTransactions(ticketId).then(({ data: txs }) => setSelectedTxs(txs ?? []));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -872,8 +862,8 @@ function TicketWorkbenchContent() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid min-h-0 flex-1 grid-cols-1 xl:grid-cols-[minmax(0,1fr)_372px]">
|
||||
<section className="min-w-0 overflow-auto bg-card/48">
|
||||
<div className="min-h-0 flex-1">
|
||||
<section className="min-w-0 overflow-auto bg-card/48 h-full">
|
||||
<div className="min-w-[760px]">
|
||||
{filteredTickets.length === 0 ? (
|
||||
<div className="flex min-h-80 flex-col items-center justify-center px-5 text-center">
|
||||
@@ -921,7 +911,7 @@ function TicketWorkbenchContent() {
|
||||
</div>
|
||||
|
||||
{filteredTickets.map((ticket) => {
|
||||
const selected = ticket.id === selectedId;
|
||||
const selected = false;
|
||||
const ownerName = ticket.owner_id
|
||||
? users.find((u) => u.id === ticket.owner_id)?.username ?? "assigned"
|
||||
: null;
|
||||
@@ -931,7 +921,7 @@ function TicketWorkbenchContent() {
|
||||
key={ticket.id}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setSelectedId(ticket.id)}
|
||||
onClick={() => router.push(`/tickets/${ticket.id}`)}
|
||||
onDoubleClick={() => router.push(`/tickets/${ticket.id}`)}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") router.push(`/tickets/${ticket.id}`); }}
|
||||
className={cn(
|
||||
@@ -1035,160 +1025,6 @@ function TicketWorkbenchContent() {
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Floating batch action bar */}
|
||||
{batchIds.size > 0 && (
|
||||
<div className="sticky bottom-0 z-20 flex items-center gap-3 border-t border-border bg-card/95 px-5 py-3 shadow-lg backdrop-blur">
|
||||
<span className="text-sm font-semibold tabular-nums text-foreground">
|
||||
{batchIds.size} selected
|
||||
</span>
|
||||
<button type="button" onClick={() => setBatchIds(new Set())} className="text-xs text-muted-foreground hover:text-foreground">
|
||||
Clear
|
||||
</button>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground">Status:</span>
|
||||
{statusOptions.filter((s) => s.key !== "all").slice(0, 5).map((s) => (
|
||||
<button
|
||||
key={s.key}
|
||||
type="button"
|
||||
disabled={batchSaving}
|
||||
onClick={() => handleBatchStatus(s.key)}
|
||||
className="rounded bg-muted/60 px-2.5 py-1 text-xs font-semibold text-foreground hover:bg-accent disabled:opacity-50"
|
||||
>
|
||||
{s.label}
|
||||
</button>
|
||||
))}
|
||||
<div className="mx-2 h-5 w-px bg-border" />
|
||||
<button
|
||||
type="button"
|
||||
disabled={batchSaving}
|
||||
onClick={handleBatchAssign}
|
||||
className="rounded bg-primary px-3 py-1 text-xs font-semibold text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
||||
>
|
||||
{batchSaving ? "Saving..." : "Assign to me"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<aside className="hidden min-h-0 border-l border-border bg-card/82 xl:flex xl:flex-col">
|
||||
{selectedTicket ? (
|
||||
<>
|
||||
{/* Compact header */}
|
||||
<div className="border-b border-border px-4 py-3">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span
|
||||
className="h-3 w-3 shrink-0 rounded-full ring-2 ring-offset-1 ring-offset-card"
|
||||
style={{
|
||||
backgroundColor: STATUS_META[selectedTicket.status]?.color ?? "#71717a",
|
||||
boxShadow: `0 0 0 2px color-mix(in srgb, ${STATUS_META[selectedTicket.status]?.color ?? "#71717a"} 20%, transparent)`,
|
||||
}}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-xs font-semibold leading-tight text-foreground line-clamp-2">
|
||||
{selectedTicket.subject}
|
||||
</p>
|
||||
<p className="mt-0.5 flex items-center gap-1 text-[10px] text-muted-foreground">
|
||||
<span className="font-mono">{formatTicketId(selectedTicket.id)}</span>
|
||||
<span>·</span>
|
||||
<span>{queueName(queues, selectedTicket.queue_id)}</span>
|
||||
{selectedTicket.owner_id && (
|
||||
<>
|
||||
<span>·</span>
|
||||
<span>{users.find((u) => u.id === selectedTicket.owner_id)?.username}</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => router.push(`/tickets/${selectedTicket.id}`)}
|
||||
className="shrink-0 rounded p-1 text-muted-foreground/50 hover:bg-accent hover:text-foreground"
|
||||
title="Open full view"
|
||||
>
|
||||
<ChevronRightIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Status dots — visual, clickable */}
|
||||
<div className="mt-3 flex items-center gap-1.5">
|
||||
{["new", "open", "in_progress", "resolved", "closed"].map((s) => {
|
||||
const isCurrent = selectedTicket.status === s;
|
||||
const color = STATUS_META[s]?.color ?? "#71717a";
|
||||
return (
|
||||
<button
|
||||
key={s}
|
||||
type="button"
|
||||
disabled={isCurrent}
|
||||
onClick={() => handleQuickStatus(selectedTicket.id, s)}
|
||||
title={statusLabel(s)}
|
||||
className={cn(
|
||||
"h-3.5 w-3.5 rounded-full transition-all",
|
||||
isCurrent
|
||||
? "ring-2 ring-offset-1 ring-offset-card scale-125"
|
||||
: "opacity-40 hover:opacity-80 hover:scale-110",
|
||||
)}
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{!selectedTicket.owner_id && (
|
||||
<button
|
||||
onClick={() => handleQuickAssign(selectedTicket.id)}
|
||||
className="ml-auto rounded border border-border px-2 py-0.5 text-[10px] font-medium text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
|
||||
>
|
||||
Take it
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Activity feed — compact timeline */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
{selectedTxs.length === 0 ? (
|
||||
<div className="p-4 text-center text-xs text-muted-foreground">No activity</div>
|
||||
) : (
|
||||
<div className="py-1">
|
||||
{selectedTxs.slice(0, 6).map((tx) => (
|
||||
<div key={tx.id} className="flex gap-3 px-4 py-2">
|
||||
<div className="relative flex flex-col items-center pt-0.5">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-border" />
|
||||
<div className="mt-1 flex-1 w-px bg-border/30" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 pb-1">
|
||||
<p className="text-xs text-foreground/90">
|
||||
{tx.transaction_type === "Create" ? "Created" :
|
||||
tx.transaction_type === "StatusChange" ? `Status → ${tx.new_value ? statusLabel(tx.new_value) : "?"}` :
|
||||
tx.transaction_type === "SetOwner" ? `Owner → ${tx.new_value ? users.find((u) => u.id === tx.new_value)?.username ?? "assigned" : "unassigned"}` :
|
||||
tx.transaction_type === "Correspond" ? "Reply" :
|
||||
tx.transaction_type === "Comment" ? "Internal note" :
|
||||
tx.transaction_type === "SetTeam" ? `Team changed` :
|
||||
tx.transaction_type === "CustomFieldChange" ? `${tx.field} → ${tx.new_value}` :
|
||||
tx.transaction_type}
|
||||
</p>
|
||||
{tx.data && (tx.data as any).body && (
|
||||
<p className="mt-0.5 line-clamp-2 text-[11px] leading-relaxed text-muted-foreground">
|
||||
{(tx.data as any).body}
|
||||
</p>
|
||||
)}
|
||||
<p className="mt-0.5 text-[10px] text-muted-foreground/50">
|
||||
{formatDistanceToNow(new Date(tx.created_at), { addSuffix: true })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-1 flex-col items-center justify-center gap-2 p-6 text-center">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-muted/30">
|
||||
<ChevronRightIcon className="h-5 w-5 text-muted-foreground/30" />
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">Select a ticket to triage</p>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
|
||||
Reference in New Issue
Block a user