Normalize ticket mentions in editor and server

This commit is contained in:
codex-bot 2025-10-24 16:35:55 -03:00
parent cf11ac9bcb
commit 296e02cf0c
5 changed files with 447 additions and 40 deletions

View file

@ -84,13 +84,29 @@ function truncateSubject(subject: string) {
return `${subject.slice(0, 57)}`
}
const TICKET_MENTION_ANCHOR_CLASSES =
"ticket-mention inline-flex items-center gap-2 rounded-full border border-slate-200 bg-slate-100 px-2.5 py-1 text-xs font-semibold text-neutral-800 no-underline transition hover:bg-slate-200"
const TICKET_MENTION_REF_CLASSES = "ticket-mention-ref text-neutral-900"
const TICKET_MENTION_SEP_CLASSES = "ticket-mention-sep text-neutral-400"
const TICKET_MENTION_SUBJECT_CLASSES = "ticket-mention-subject max-w-[220px] truncate text-neutral-700"
const TICKET_MENTION_DOT_BASE_CLASSES = "ticket-mention-dot inline-flex size-2 rounded-full"
const TICKET_MENTION_STATUS_TONE: Record<TicketStatusNormalized, string> = {
PENDING: "bg-amber-400",
AWAITING_ATTENDANCE: "bg-sky-500",
PAUSED: "bg-violet-500",
RESOLVED: "bg-emerald-500",
}
function buildTicketMentionAnchor(ticket: Doc<"tickets">): string {
const reference = ticket.reference
const subject = escapeHtml(ticket.subject ?? "")
const truncated = truncateSubject(subject)
const status = (ticket.status ?? "PENDING").toString().toUpperCase()
const priority = (ticket.priority ?? "MEDIUM").toString().toUpperCase()
return `<a data-ticket-mention="true" data-ticket-id="${String(ticket._id)}" data-ticket-reference="${reference}" data-ticket-status="${status}" data-ticket-priority="${priority}" data-ticket-subject="${subject}" href="/tickets/${String(ticket._id)}" class="ticket-mention" title="Chamado #${reference}${subject ? `${subject}` : ""}"><span class="ticket-mention-dot"></span><span class="ticket-mention-ref">#${reference}</span><span class="ticket-mention-sep">•</span><span class="ticket-mention-subject">${truncated}</span></a>`
const normalizedStatus = normalizeStatus(status)
const dotTone = TICKET_MENTION_STATUS_TONE[normalizedStatus] ?? "bg-slate-400"
const dotClass = `${TICKET_MENTION_DOT_BASE_CLASSES} ${dotTone}`
return `<a data-ticket-mention="true" data-ticket-id="${String(ticket._id)}" data-ticket-reference="${reference}" data-ticket-status="${status}" data-ticket-priority="${priority}" data-ticket-subject="${subject}" status="${normalizedStatus}" href="/tickets/${String(ticket._id)}" class="${TICKET_MENTION_ANCHOR_CLASSES}" rel="noopener noreferrer" target="_self" title="Chamado #${reference}${subject ? `${subject}` : ""}"><span class="${dotClass}"></span><span class="${TICKET_MENTION_REF_CLASSES}">#${reference}</span><span class="${TICKET_MENTION_SEP_CLASSES}">•</span><span class="${TICKET_MENTION_SUBJECT_CLASSES}">${truncated}</span></a>`
}
function canMentionTicket(viewerRole: string, viewerId: Id<"users">, ticket: Doc<"tickets">) {
@ -111,11 +127,11 @@ async function normalizeTicketMentions(
viewer: { user: Doc<"users">; role: string },
tenantId: string,
): Promise<string> {
if (!html || html.indexOf("data-ticket-mention") === -1) {
if (!html || (html.indexOf("data-ticket-mention") === -1 && html.indexOf("ticket-mention") === -1)) {
return html
}
const mentionPattern = /<a\b[^>]*data-ticket-mention="true"[^>]*>[\s\S]*?<\/a>/gi
const mentionPattern = /<a\b[^>]*(?:data-ticket-mention="true"|class="[^"]*ticket-mention[^"]*")[^>]*>[\s\S]*?<\/a>/gi
const matches = Array.from(html.matchAll(mentionPattern))
if (!matches.length) {
return html
@ -123,10 +139,23 @@ async function normalizeTicketMentions(
let output = html
const attributePattern = /(data-[\w-]+|class|href)="([^"]*)"/gi
for (const match of matches) {
const full = match[0]
const idMatch = /data-ticket-id="([^"]+)"/i.exec(full)
const ticketIdRaw = idMatch?.[1]
attributePattern.lastIndex = 0
const attributes: Record<string, string> = {}
let attrMatch: RegExpExecArray | null
while ((attrMatch = attributePattern.exec(full)) !== null) {
attributes[attrMatch[1]] = attrMatch[2]
}
let ticketIdRaw: string | null = attributes["data-ticket-id"] ?? null
if (!ticketIdRaw && attributes.href) {
const hrefPath = attributes.href.split("?")[0]
const segments = hrefPath.split("/").filter(Boolean)
ticketIdRaw = segments.pop() ?? null
}
let replacement = ""
if (ticketIdRaw) {