Split TicketDetail.tsx (775 lines) into focused sub-components

Extracted TicketComments, TicketAuditLog, and TicketSidebar into
client/src/pages/ticket-detail/. The main TicketDetail.tsx remains
as the page orchestrator. Router import unchanged via index.ts
re-export.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-21 20:41:27 -04:00
parent c0ff063023
commit 6c93a8c466
7 changed files with 864 additions and 775 deletions
@@ -0,0 +1,104 @@
import { useState } from 'react';
import { format, formatDistanceToNow } from 'date-fns';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { AUDIT_LABELS, AUDIT_COLORS } from '../../../../shared/constants/labels';
import { injectMentionLinks } from '../../lib/mentions';
import type { AuditLog, User } from '../../types';
const COMMENT_ACTIONS = new Set(['COMMENT_ADDED', 'COMMENT_DELETED']);
interface TicketAuditLogProps {
auditLogs: AuditLog[];
users: User[];
}
export default function TicketAuditLog({ auditLogs, users }: TicketAuditLogProps) {
const [expandedLogs, setExpandedLogs] = useState<Set<string>>(new Set());
const toggleLog = (logId: string) => {
setExpandedLogs((prev) => {
const next = new Set(prev);
if (next.has(logId)) next.delete(logId);
else next.add(logId);
return next;
});
};
if (auditLogs.length === 0) {
return (
<div className="p-6">
<div className="py-10 text-center text-sm text-gray-600">No activity yet</div>
</div>
);
}
return (
<div className="p-6">
<div>
{auditLogs.map((log, i) => {
const hasDetail = !!log.detail;
const isExpanded = expandedLogs.has(log.id);
const isComment = COMMENT_ACTIONS.has(log.action);
return (
<div key={log.id} className="flex gap-4">
<div className="flex flex-col items-center w-5 flex-shrink-0">
<div
className={`w-2.5 h-2.5 rounded-full mt-1 flex-shrink-0 ${AUDIT_COLORS[log.action] ?? 'bg-gray-500'}`}
/>
{i < auditLogs.length - 1 && (
<div className="w-px flex-1 bg-gray-800 my-1" />
)}
</div>
<div className="flex-1 pb-4">
<div
className={`flex items-baseline justify-between gap-4 ${hasDetail ? 'cursor-pointer select-none' : ''}`}
onClick={() => hasDetail && toggleLog(log.id)}
>
<p className="text-sm text-gray-300">
<span className="font-medium text-gray-100">
{log.user.displayName}
</span>{' '}
{AUDIT_LABELS[log.action] ?? log.action.toLowerCase()}
{hasDetail && (
<span className="ml-1 inline-flex items-center text-gray-600">
{isExpanded ? (
<ChevronDown size={13} />
) : (
<ChevronRight size={13} />
)}
</span>
)}
</p>
<span
className="text-xs text-gray-600 flex-shrink-0"
title={format(new Date(log.createdAt), 'MMM d, yyyy HH:mm:ss')}
>
{formatDistanceToNow(new Date(log.createdAt), { addSuffix: true })}
</span>
</div>
{hasDetail && isExpanded && (
<div className="mt-2 ml-0 bg-gray-800 border border-gray-700 rounded-lg px-4 py-3">
{isComment ? (
<div className="prose text-sm text-gray-300">
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{injectMentionLinks(log.detail!, users)}
</ReactMarkdown>
</div>
) : (
<p className="text-sm text-gray-400">{log.detail}</p>
)}
</div>
)}
</div>
</div>
);
})}
</div>
</div>
);
}