diff --git a/docs/DEV.md b/docs/DEV.md index 1d1eef0..c28d8b1 100644 --- a/docs/DEV.md +++ b/docs/DEV.md @@ -39,6 +39,13 @@ Este documento consolida o estado atual do ambiente de desenvolvimento, descreve - **Turbopack** segue como bundler padrão, sem flags experimentais adicionais. - **Whitelist de hosts**: o release estável 15.5 não aceita `server.allowedHosts` (vide [`invalid-next-config`](https://nextjs.org/docs/messages/invalid-next-config)), portanto bloqueamos domínios exclusivamente via `middleware.ts`. +### Editor rich text (TipTap) — menções de ticket + +- Menções (`ticketMention`) agora têm prioridade maior (`priority: 1000`) e um Link seguro (`SafeLinkExtension`) foi introduzido para ignorar ``. Isso evita que o `Link` do StarterKit capture as âncoras na hidratação, garantindo que as menções continuem como nodes (não como marks) durante a edição. +- O mesmo helper `normalizeTicketMentionHtml` é aplicado ao carregar/atualizar conteúdo no editor, dentro dos fluxos de comentários e no Convex. Esse helper reescreve qualquer HTML legado (`#123•Assunto`) no formato de chip completo (datasets, spans, dot). +- Resultado: o chip mantém layout e comportamento ao editar (Backspace/Delete removem o node inteiro, node view continua ativo) sem exigir reload. +- Se precisar adicionar novos comportamentos, importe `SafeLinkExtension` e mantenha a ordem `[TicketMentionExtension, StarterKit (link:false), SafeLinkExtension, Placeholder]` para que o parser continue estável. + ## Comandos de qualidade - `pnpm lint`: executa ESLint (flat config) sobre os arquivos do projeto. diff --git a/src/components/tickets/ticket-queue-summary.tsx b/src/components/tickets/ticket-queue-summary.tsx index 5abe568..b9171d0 100644 --- a/src/components/tickets/ticket-queue-summary.tsx +++ b/src/components/tickets/ticket-queue-summary.tsx @@ -46,20 +46,22 @@ export function TicketQueueSummaryCards({ queues }: TicketQueueSummaryProps) { Fila {queue.name} - -
- Pendentes - {queue.pending} + +
+
+

Pendentes

+

{queue.pending}

+
+
+

Aguardando resposta

+

{queue.waiting}

+
+
+

Violados

+

{queue.breached}

+
-
- Aguardando resposta - {queue.waiting} -
-
- Violados - {queue.breached} -
-
+
{breachPercent}% com SLA violado nesta fila diff --git a/src/components/tickets/tickets-board.tsx b/src/components/tickets/tickets-board.tsx index 1164f41..41278aa 100644 --- a/src/components/tickets/tickets-board.tsx +++ b/src/components/tickets/tickets-board.tsx @@ -14,13 +14,14 @@ import { getTicketStatusChipClass, getTicketStatusLabel } from "@/lib/ticket-sta type TicketsBoardProps = { tickets: Ticket[] + enteringIds?: Set } function formatUpdated(date: Date) { return formatDistanceToNowStrict(date, { addSuffix: true, locale: ptBR }) } -export function TicketsBoard({ tickets }: TicketsBoardProps) { +export function TicketsBoard({ tickets, enteringIds }: TicketsBoardProps) { if (!tickets.length) { return (
@@ -41,11 +42,16 @@ export function TicketsBoard({ tickets }: TicketsBoardProps) { return (
- {tickets.map((ticket) => ( + {tickets.map((ticket) => { + const isEntering = enteringIds?.has(ticket.id) + return (
@@ -103,7 +109,8 @@ export function TicketsBoard({ tickets }: TicketsBoardProps) {
- ))} + ) + })}
) } diff --git a/src/components/tickets/tickets-table.tsx b/src/components/tickets/tickets-table.tsx index b6c7bf4..2723554 100644 --- a/src/components/tickets/tickets-table.tsx +++ b/src/components/tickets/tickets-table.tsx @@ -95,9 +95,10 @@ function AssigneeCell({ ticket }: { ticket: Ticket }) { export type TicketsTableProps = { tickets?: Ticket[] + enteringIds?: Set } -export function TicketsTable({ tickets }: TicketsTableProps) { +export function TicketsTable({ tickets, enteringIds }: TicketsTableProps) { const safeTickets = tickets ?? [] const [now, setNow] = useState(() => Date.now()) const serverOffsetRef = useRef(0) @@ -175,11 +176,15 @@ export function TicketsTable({ tickets }: TicketsTableProps) { {safeTickets.map((ticket) => { const ChannelIcon = channelIcon[ticket.channel] ?? MessageCircle + const rowClass = cn( + `${tableRowClass} cursor-pointer`, + enteringIds?.has(ticket.id) ? "recent-ticket-enter" : undefined, + ) return ( - router.push(`/tickets/${ticket.id}`)} diff --git a/src/components/tickets/tickets-view.tsx b/src/components/tickets/tickets-view.tsx index bbfb7b8..63604a9 100644 --- a/src/components/tickets/tickets-view.tsx +++ b/src/components/tickets/tickets-view.tsx @@ -1,6 +1,6 @@ "use client" -import { useEffect, useMemo, useState } from "react" +import { useEffect, useMemo, useRef, useState } from "react" import { toast } from "sonner" import { useQuery } from "convex/react" import { api } from "@/convex/_generated/api" @@ -165,6 +165,27 @@ export function TicketsView({ initialFilters }: TicketsViewProps = {}) { return working }, [tickets, filters.queue, filters.status, filters.view, filters.company]) + const previousIdsRef = useRef([]) + const [enteringIds, setEnteringIds] = useState>(new Set()) + + useEffect(() => { + if (ticketsRaw === undefined) { + previousIdsRef.current = [] + setEnteringIds(new Set()) + return + } + const ids = filteredTickets.map((ticket) => ticket.id) + const previous = previousIdsRef.current + previousIdsRef.current = ids + if (!previous.length) return + const newIds = ids.filter((id) => !previous.includes(id)) + if (newIds.length === 0) return + const highlight = new Set(newIds) + setEnteringIds(highlight) + const timeout = window.setTimeout(() => setEnteringIds(new Set()), 600) + return () => window.clearTimeout(timeout) + }, [filteredTickets, ticketsRaw]) + return (
) : viewMode === "board" ? ( - + ) : ( - + )}
) diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx index 40061d6..b7219c1 100644 --- a/src/components/ui/sidebar.tsx +++ b/src/components/ui/sidebar.tsx @@ -254,15 +254,31 @@ function Sidebar({ ) } -function SidebarTrigger({ - className, - onClick, - ...props -}: React.ComponentProps) { - const { toggleSidebar } = useSidebar() - - return ( -