fix: URL-to-link rendering, fix horizontal overflow in timeline
- LinkedText component splits on newlines and converts raw URLs to clickable links showing just the hostname (e.g. 'ct.sendgrid.net' instead of full URL) - [overflow-wrap:anywhere] on all text content (more aggressive than break-words) - w-0 flex-1 on timeline item content divs to prevent flex children expanding past container boundary - Applied to description, resolution, note bodies, time entry notes
This commit is contained in:
parent
df50c3868d
commit
21a41ef8ef
1 changed files with 63 additions and 10 deletions
|
|
@ -34,6 +34,55 @@ import {
|
|||
// noteType values that are system/automated (workflow rules, monitoring, auto-close, etc.)
|
||||
const SYSTEM_NOTE_TYPES = new Set([13, 91, 93, 94, 99, 101]);
|
||||
|
||||
const URL_REGEX = /https?:\/\/[^\s<>"')]+/g;
|
||||
|
||||
function renderTextWithLinks(text: string): React.ReactNode[] {
|
||||
const parts: React.ReactNode[] = [];
|
||||
let lastIndex = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
URL_REGEX.lastIndex = 0;
|
||||
while ((match = URL_REGEX.exec(text)) !== null) {
|
||||
if (match.index > lastIndex) {
|
||||
parts.push(text.slice(lastIndex, match.index));
|
||||
}
|
||||
const url = match[0].replace(/[.,;:!?)]+$/, '');
|
||||
let label: string;
|
||||
try {
|
||||
label = new URL(url).hostname.replace(/^www\./, '');
|
||||
} catch {
|
||||
label = 'link';
|
||||
}
|
||||
parts.push(
|
||||
<a
|
||||
key={match.index}
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-500 hover:underline"
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
);
|
||||
lastIndex = match.index + match[0].length;
|
||||
}
|
||||
if (lastIndex < text.length) parts.push(text.slice(lastIndex));
|
||||
return parts;
|
||||
}
|
||||
|
||||
function LinkedText({ text, className }: { text: string; className?: string }) {
|
||||
const lines = text.split('\n');
|
||||
return (
|
||||
<span className={className}>
|
||||
{lines.map((line, i) => (
|
||||
<span key={i}>
|
||||
{renderTextWithLinks(line)}
|
||||
{i < lines.length - 1 && <br />}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
interface TicketDetailModalProps {
|
||||
ticketNumber: string;
|
||||
open: boolean;
|
||||
|
|
@ -179,7 +228,7 @@ export function TicketDetailModal({ ticketNumber, open, onOpenChange }: TicketDe
|
|||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-4xl w-[90vw] max-h-[85vh] overflow-y-auto overflow-x-hidden">
|
||||
<DialogContent className="max-w-4xl w-[90vw] max-h-[85vh] overflow-y-auto">
|
||||
<DialogHeader className="pb-0">
|
||||
<DialogTitle className="flex items-center gap-2 text-base">
|
||||
<TicketIcon className="h-4 w-4 shrink-0" />
|
||||
|
|
@ -202,7 +251,7 @@ export function TicketDetailModal({ ticketNumber, open, onOpenChange }: TicketDe
|
|||
)}
|
||||
|
||||
{ticket && !loading && (
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-3 min-w-0 overflow-hidden">
|
||||
{/* Metadata */}
|
||||
<div className="flex flex-wrap items-center gap-2 text-sm">
|
||||
{getStatusBadge(ticket.status, ticket.statusLabel)}
|
||||
|
|
@ -250,8 +299,8 @@ export function TicketDetailModal({ ticketNumber, open, onOpenChange }: TicketDe
|
|||
{ticket.description && (
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-1">Description</p>
|
||||
<div className="p-3 bg-muted rounded-lg whitespace-pre-wrap break-words text-sm">
|
||||
{ticket.description}
|
||||
<div className="p-3 bg-muted rounded-lg text-sm [overflow-wrap:anywhere]">
|
||||
<LinkedText text={ticket.description} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -260,8 +309,8 @@ export function TicketDetailModal({ ticketNumber, open, onOpenChange }: TicketDe
|
|||
{ticket.resolution && (
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-1">Resolution</p>
|
||||
<div className="p-3 bg-muted rounded-lg whitespace-pre-wrap break-words text-sm">
|
||||
{ticket.resolution}
|
||||
<div className="p-3 bg-muted rounded-lg text-sm [overflow-wrap:anywhere]">
|
||||
<LinkedText text={ticket.resolution} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -340,7 +389,7 @@ export function TicketDetailModal({ ticketNumber, open, onOpenChange }: TicketDe
|
|||
</div>
|
||||
{!isLast && <div className="w-px flex-1 bg-border my-1" />}
|
||||
</div>
|
||||
<div className={`pb-4 min-w-0 flex-1 ${isLast ? '' : ''}`}>
|
||||
<div className={`pb-4 min-w-0 w-0 flex-1 ${isLast ? '' : ''}`}>
|
||||
<div className="flex items-center justify-between gap-2 mb-0.5">
|
||||
<span className="text-xs font-medium">{e.resourceName || 'Unknown'}</span>
|
||||
<div className="flex items-center gap-1.5 shrink-0">
|
||||
|
|
@ -349,7 +398,9 @@ export function TicketDetailModal({ ticketNumber, open, onOpenChange }: TicketDe
|
|||
</div>
|
||||
</div>
|
||||
{e.summaryNotes && (
|
||||
<p className="text-xs text-muted-foreground whitespace-pre-wrap break-words">{e.summaryNotes}</p>
|
||||
<p className="text-xs text-muted-foreground [overflow-wrap:anywhere]">
|
||||
<LinkedText text={e.summaryNotes} />
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -370,14 +421,16 @@ export function TicketDetailModal({ ticketNumber, open, onOpenChange }: TicketDe
|
|||
</div>
|
||||
{!isLast && <div className="w-px flex-1 bg-border my-1" />}
|
||||
</div>
|
||||
<div className={`pb-4 min-w-0 flex-1 ${isSystem ? 'opacity-60' : ''}`}>
|
||||
<div className={`pb-4 min-w-0 w-0 flex-1 ${isSystem ? 'opacity-60' : ''}`}>
|
||||
<div className="flex items-center justify-between gap-2 mb-0.5">
|
||||
<span className="text-xs font-medium">{n.creatorName || (isSystem ? 'System' : 'Unknown')}</span>
|
||||
<span className="text-xs text-muted-foreground shrink-0">{formatDate(n.createDateTime)}</span>
|
||||
</div>
|
||||
{n.title && <p className="text-xs font-medium text-muted-foreground mb-0.5">{n.title}</p>}
|
||||
{n.description && (
|
||||
<p className="text-xs whitespace-pre-wrap break-words">{n.description}</p>
|
||||
<p className="text-xs [overflow-wrap:anywhere]">
|
||||
<LinkedText text={n.description} />
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue