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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user