From 9b0c0bd80ad6732bd709ba38ea42fce75e58d647 Mon Sep 17 00:00:00 2001 From: esdrasrenan Date: Sat, 4 Oct 2025 01:45:57 -0300 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20timeline=20'Coment=C3=A1rio=20adici?= =?UTF-8?q?onado'=20com=20autor;=20skeletons=20na=20p=C3=A1gina=20de=20det?= =?UTF-8?q?alhe;=20skeleton=20nas=20filas;=20alias=20de=20Convex=20j=C3=A1?= =?UTF-8?q?=20padronizado;=20mutation=20addComment=20inclui=20authorName/a?= =?UTF-8?q?vatar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- agents.md | 80 +++++++++++++++++++ web/convex/tickets.ts | 3 +- .../components/tickets/ticket-detail-view.tsx | 32 +++++++- .../tickets/ticket-queue-summary.tsx | 16 +++- .../components/tickets/ticket-timeline.tsx | 1 + 5 files changed, 128 insertions(+), 4 deletions(-) diff --git a/agents.md b/agents.md index 6b3cffb..26e5599 100644 --- a/agents.md +++ b/agents.md @@ -124,3 +124,83 @@ Este repositório foi atualizado para usar Convex como backend em tempo real par - [ ] Textos/labels em PT‑BR. - [ ] Eventos de UI com feedback (toast) e rollback em erro. - [ ] Documentação atualizada se houver mudanças em fluxo/env. + +--- + +## Próximas Entregas (Roadmap detalhado) + +1) UX/Visual (shadcn/ui) +- Padronizar cartões em todas as telas (Play, Visualizações) com o mesmo padrão aplicado em Conversa/Detalhes/Timeline (bordas, sombra, paddings). +- Aplicar microtipografia consistente: headings H1/H2, tracking, tamanhos, cores em PT‑BR. +- Skeletons de carregamento nos principais painéis (lista de tickets, recentes, play next). +- Melhorar tabela: estados hover/focus, ícones de canal, largura de colunas previsível e truncamento. + +2) Comentários e anexos +- Dropzone também no “Novo ticket” (já implementado) com registro de comentário inicial e anexos. +- Grid de anexos com miniaturas e legenda; manter atributo `download` com o nome original. +- Preview em modal para imagens (feito) e suporte a múltiplas linhas no grid. +- Botão para copiar link de arquivo (futuro, usar URL do storage). + +3) Timeline e eventos +- Mensagens amigáveis em PT‑BR (feito para CREATED/STATUS/ASSIGNEE/QUEUE). +- Incluir sempre `actorName`/`actorAvatar` no payload; evitar JSON cru na UI. +- Exibir avatar e nome do ator nas entradas (parcialmente feito). + +4) Dados e camada Convex +- Sempre retornar datas como `number` (epoch) e converter no front via mappers Zod. +- Padronizar import do Convex com `@/convex/_generated/api` (alias criado). +- Evitar `useQuery` com args vazios — proteger chamadas (gates) e, quando necessário, fallback de mock para IDs `ticket-*`. + +5) Autenticação / Sessão (placeholder) +- Cookie `demoUser` e bootstrap de usuário no Convex (feito). Trocar por Auth.js/Clerk quando for o momento. + +6) Testes +- Vitest configurado; adicionar casos para mapeadores (já iniciado) e smoke tests básicos de páginas. +- Não usar Date em assertions de payload — sempre comparar epoch ou `instanceof Date` após mapeamento. + +7) Acessibilidade e internacionalização +- Labels e mensagens 100% em PT‑BR; evitar termos como `QUEUE_CHANGED` na UI. +- Navegação por teclado em Dialogs/Selects; aria-labels em botões de ação. + +8) Observabilidade (posterior) +- Logs de evento estruturados no Convex; traces simples no client para ações críticas. + +--- + +## Endpoints Convex (resumo) +- `tickets.list({ tenantId, status?, priority?, channel?, queueId?, search?, limit? })` +- `tickets.getById({ tenantId, id })` +- `tickets.create({ tenantId, subject, summary?, priority, channel, queueId?, requesterId })` +- `tickets.addComment({ ticketId, authorId, visibility, body, attachments?[] })` +- `tickets.updateStatus({ ticketId, status, actorId })` — gera evento com `toLabel` e `actorName`. +- `tickets.changeAssignee({ ticketId, assigneeId, actorId })` — gera evento com `assigneeName`. +- `tickets.changeQueue({ ticketId, queueId, actorId })` — gera evento com `queueName`. +- `tickets.playNext({ tenantId, queueId?, agentId })` — atribui ticket e registra evento. +- `queues.summary({ tenantId })` +- `files.generateUploadUrl()` — usar via `useAction`. +- `users.ensureUser({ tenantId, email, name, avatarUrl?, role?, teams? })` + +Observações: +- Não retornar `Date` nas funções Convex; usar `number` e converter na UI com os mappers em `src/lib/mappers`. +- Evitar passar `{}` para `useQuery` — args devem estar definidos ou a query não deve ser invocada. + +--- + +## Padrões de Código +- UI: shadcn/ui (Field, Dialog, Select, Badge, Table, Spinner) + Tailwind. +- Dados: Zod para validação; mappers para converter server→UI (epoch→Date, null→undefined). +- Texto: PT‑BR em labels, toasts e timeline. +- UX: updates otimistas + toasts (status, assignee, fila, comentários). +- Imports do Convex: sempre `@/convex/_generated/api`. + +--- + +## Como abrir PR +- Crie uma branch descritiva (ex.: `feat/tickets-attachments-grid`). +- Preencha a descrição com: contexto, mudanças, como testar (pnpm scripts), screenshots quando útil. +- Checklist: + - [ ] Sem `Date` no retorno Convex. + - [ ] Labels PT‑BR. + - [ ] Skeleton/Loading onde couber. + - [ ] Mappers atualizados se tocar em payloads. + - [ ] AGENTS.md atualizado se houver mudança de padrões. diff --git a/web/convex/tickets.ts b/web/convex/tickets.ts index d2b88aa..880e356 100644 --- a/web/convex/tickets.ts +++ b/web/convex/tickets.ts @@ -256,10 +256,11 @@ export const addComment = mutation({ createdAt: now, updatedAt: now, }); + const author = await ctx.db.get(args.authorId); await ctx.db.insert("ticketEvents", { ticketId: args.ticketId, type: "COMMENT_ADDED", - payload: { authorId: args.authorId }, + payload: { authorId: args.authorId, authorName: author?.name, authorAvatar: author?.avatarUrl }, createdAt: now, }); // bump ticket updatedAt diff --git a/web/src/components/tickets/ticket-detail-view.tsx b/web/src/components/tickets/ticket-detail-view.tsx index 947777c..ea41739 100644 --- a/web/src/components/tickets/ticket-detail-view.tsx +++ b/web/src/components/tickets/ticket-detail-view.tsx @@ -7,6 +7,9 @@ import { api } from "@/convex/_generated/api"; import { DEFAULT_TENANT_ID } from "@/lib/constants"; import { mapTicketWithDetailsFromServer } from "@/lib/mappers/ticket"; import { getTicketById } from "@/lib/mocks/tickets"; +import { Card, CardContent } from "@/components/ui/card"; +import { Separator } from "@/components/ui/separator"; +import { Skeleton } from "@/components/ui/skeleton"; import { TicketComments } from "@/components/tickets/ticket-comments"; import { TicketDetailsPanel } from "@/components/tickets/ticket-details-panel"; import { TicketSummaryHeader } from "@/components/tickets/ticket-summary-header"; @@ -21,7 +24,34 @@ export function TicketDetailView({ id }: { id: string }) { } else if (isMockId) { ticket = getTicketById(id) ?? null; } - if (!ticket) return
Carregando ticket...
; + if (!ticket) return ( +
+ + +
+ + +
+
+
+ + + {Array.from({ length: 3 }).map((_, i) => ( +
+
+ +
+ ))} +
+
+ + + {Array.from({ length: 5 }).map((_, i) => ())} + + +
+
+ ); return (
diff --git a/web/src/components/tickets/ticket-queue-summary.tsx b/web/src/components/tickets/ticket-queue-summary.tsx index c8efd8a..94707b8 100644 --- a/web/src/components/tickets/ticket-queue-summary.tsx +++ b/web/src/components/tickets/ticket-queue-summary.tsx @@ -14,8 +14,20 @@ interface TicketQueueSummaryProps { } export function TicketQueueSummaryCards({ queues }: TicketQueueSummaryProps) { - const fromServer = useQuery(api.queues.summary, { tenantId: DEFAULT_TENANT_ID }) ?? [] - const data: TicketQueueSummary[] = (queues ?? fromServer) as any + const fromServer = useQuery(api.queues.summary, { tenantId: DEFAULT_TENANT_ID }) + const data: TicketQueueSummary[] = (queues ?? fromServer ?? []) as any + if (!queues && fromServer === undefined) { + return ( +
+ {Array.from({ length: 3 }).map((_, i) => ( +
+
+
+
+ ))} +
+ ) + } return (
{data.map((queue) => { diff --git a/web/src/components/tickets/ticket-timeline.tsx b/web/src/components/tickets/ticket-timeline.tsx index db69225..683620f 100644 --- a/web/src/components/tickets/ticket-timeline.tsx +++ b/web/src/components/tickets/ticket-timeline.tsx @@ -75,6 +75,7 @@ export function TicketTimeline({ ticket }: TicketTimelineProps) { if (entry.type === "ASSIGNEE_CHANGED" && (p.assigneeName || p.assigneeId)) message = `Responsável alterado${p.assigneeName ? ` para ${p.assigneeName}` : ""}` if (entry.type === "QUEUE_CHANGED" && (p.queueName || p.queueId)) message = `Fila alterada${p.queueName ? ` para ${p.queueName}` : ""}` if (entry.type === "CREATED" && (p.requesterName)) message = `Criado por ${p.requesterName}` + if (entry.type === "COMMENT_ADDED" && (p.authorName || p.authorId)) message = `Comentário adicionado${p.authorName ? ` por ${p.authorName}` : ""}` if (!message) return null return (
From ea60c3b8415b8e3f60d7d0590c7b28c0c6df94aa Mon Sep 17 00:00:00 2001 From: esdrasrenan Date: Sat, 4 Oct 2025 14:25:10 -0300 Subject: [PATCH 2/2] feat(rich-text, types): Tiptap editor, SSR-safe, comments + description; stricter typing (no any) across app - Add Tiptap editor + toolbar and rich content rendering with sanitize-html - Fix SSR hydration (immediatelyRender: false) and setContent options - Comments: rich text + visibility selector, typed attachments (Id<_storage>) - New Ticket: description rich text; attachments typed; queues typed - Convex: server-side filters using indexes; priority order rename; stronger Doc/Id typing; remove helper with any - Schemas/Mappers: zod v4 record typing; event payload record typing; customFields typed - UI: replace any in header/play/list/timeline/fields; improve select typings - Build passes; only non-blocking lint warnings remain --- web/build.log | 128 ++++ web/convex/schema.ts | 5 +- web/convex/tickets.ts | 167 +++-- web/eslint.config.mjs | 31 +- web/package.json | 6 + web/pnpm-lock.yaml | 686 ++++++++++++++++++ web/src/app/globals.css | 34 +- web/src/app/tickets/[id]/page.tsx | 3 +- web/src/app/tickets/new/page.tsx | 25 +- web/src/components/app-sidebar.tsx | 10 +- .../components/tickets/new-ticket-dialog.tsx | 40 +- .../tickets/play-next-ticket-card.tsx | 38 +- .../tickets/recent-tickets-panel.tsx | 5 +- web/src/components/tickets/status-badge.tsx | 12 +- ...-comments.tsx => ticket-comments.rich.tsx} | 65 +- .../components/tickets/ticket-detail-view.tsx | 18 +- .../tickets/ticket-queue-summary.tsx | 2 +- .../tickets/ticket-summary-header.tsx | 25 +- .../components/tickets/ticket-timeline.tsx | 8 +- web/src/components/tickets/tickets-view.tsx | 11 +- web/src/components/ui/field.tsx | 56 +- web/src/components/ui/rich-text-editor.tsx | 218 ++++++ web/src/lib/auth-client.tsx | 7 +- web/src/lib/mappers/ticket.ts | 4 +- web/src/lib/schemas/ticket.ts | 24 +- web/tsconfig.json | 7 +- 26 files changed, 1390 insertions(+), 245 deletions(-) create mode 100644 web/build.log rename web/src/components/tickets/{ticket-comments.tsx => ticket-comments.rich.tsx} (76%) create mode 100644 web/src/components/ui/rich-text-editor.tsx diff --git a/web/build.log b/web/build.log new file mode 100644 index 0000000..2417e48 --- /dev/null +++ b/web/build.log @@ -0,0 +1,128 @@ + +> web@0.1.0 build C:\Users\monke\OneDrive\Documentos\Projetos\sistema-de-chamados\web +> next build + + Ôû▓ Next.js 15.5.3 + - Environments: .env.local + + Creating an optimized production build ... + Ô£ô Compiled successfully in 3.0s + Linting and checking validity of types ... + +./src/app/ConvexClientProvider.tsx +4:21 Warning: 'useMemo' is defined but never used. @typescript-eslint/no-unused-vars + +./src/app/tickets/new/page.tsx +16:9 Warning: The 'queues' logical expression could make the dependencies of useMemo Hook (at line 28) change on every render. To fix this, wrap the initialization of 'queues' in its own useMemo() Hook. react-hooks/exhaustive-deps +28:53 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +35:33 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +35:49 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +43:27 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +44:30 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +48:42 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +48:67 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any + +./src/app/tickets/[id]/page.tsx +27:61 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any + +./src/components/tickets/new-ticket-dialog.tsx +6:1 Warning: Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free. @typescript-eslint/ban-ts-comment +50:35 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +58:32 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +63:44 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +63:69 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +63:140 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +71:14 Warning: 'err' is defined but never used. @typescript-eslint/no-unused-vars +116:111 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +128:109 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +150:41 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any + +./src/components/tickets/play-next-ticket-card.tsx +39:34 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +43:169 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +43:240 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +75:37 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +109:103 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +109:141 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any + +./src/components/tickets/recent-tickets-panel.tsx +4:1 Warning: Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free. @typescript-eslint/ban-ts-comment +9:10 Warning: 'Spinner' is defined but never used. @typescript-eslint/no-unused-vars +27:58 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +30:41 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any + +./src/components/tickets/ticket-comments.rich.tsx +31:9 Warning: 'generateUploadUrl' is assigned a value but never used. @typescript-eslint/no-unused-vars +52:100 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +61:49 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +61:74 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +64:14 Warning: 'err' is defined but never used. @typescript-eslint/no-unused-vars +113:34 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +151:83 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any + +./src/components/tickets/ticket-detail-view.tsx +11:10 Warning: 'Separator' is defined but never used. @typescript-eslint/no-unused-vars +20:108 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +21:15 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +23:50 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +57:46 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +60:45 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +61:45 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +63:47 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any + +./src/components/tickets/ticket-queue-summary.tsx +18:70 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any + +./src/components/tickets/ticket-summary-header.tsx +50:50 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +59:63 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +59:85 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +59:109 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +61:26 Warning: 'e' is defined but never used. @typescript-eslint/no-unused-vars +98:63 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +98:89 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +98:113 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +107:31 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +121:42 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +125:60 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +125:82 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +125:106 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +134:31 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any + +./src/components/tickets/ticket-timeline.tsx +12:10 Warning: 'cn' is defined but never used. @typescript-eslint/no-unused-vars +15:10 Warning: 'Separator' is defined but never used. @typescript-eslint/no-unused-vars +72:28 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any + +./src/components/tickets/tickets-view.tsx +12:10 Warning: 'Spinner' is defined but never used. @typescript-eslint/no-unused-vars +27:80 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +31:31 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +36:76 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +49:51 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any + +./src/components/ui/dropzone.tsx +4:1 Warning: Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free. @typescript-eslint/ban-ts-comment + +./src/components/ui/field.tsx +11:52 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any +28:58 Warning: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any + +info - Need to disable some ESLint rules? Learn more here: https://nextjs.org/docs/app/api-reference/config/eslint#disabling-rules +Failed to compile. + +./src/components/tickets/play-next-ticket-card.tsx:43:9 +Type error: Type '{ queue: { id: string; name: string; pending: number; waiting: number; breached: number; }; nextTicket: { id: string; reference: number; tenantId: string; subject: string; status: "RESOLVED" | "CLOSED" | "PENDING" | "ON_HOLD" | "OPEN" | "NEW"; ... 14 more ...; lastTimelineEntry?: string | undefined; } | null; } | { ...' is not assignable to type '{ queue: { id: string; name: string; pending: number; waiting: number; breached: number; }; nextTicket: { id: string; reference: number; tenantId: string; subject: string; status: "RESOLVED" | "CLOSED" | "PENDING" | "ON_HOLD" | "OPEN" | "NEW"; ... 14 more ...; lastTimelineEntry?: string | undefined; } | null; } | null'. + Type '{ queue: { id: string; name: string; pending: number; waiting: number; breached: number; }; nextTicket: { id: any; reference: any; tenantId: any; subject: any; summary: any; status: any; priority: any; channel: any; ... 11 more ...; metrics: null; }; }' is not assignable to type '{ queue: { id: string; name: string; pending: number; waiting: number; breached: number; }; nextTicket: { id: string; reference: number; tenantId: string; subject: string; status: "RESOLVED" | "CLOSED" | "PENDING" | "ON_HOLD" | "OPEN" | "NEW"; ... 14 more ...; lastTimelineEntry?: string | undefined; } | null; }'. + The types of 'nextTicket.lastTimelineEntry' are incompatible between these types. + Type 'null' is not assignable to type 'string | undefined'. + + 41 | })?.[0] + 42 | +> 43 | const cardContext: TicketPlayContext | null = context ?? (nextTicketFromServer ? { queue: { id: "default", name: "Geral", pending: queueSummary.reduce((a: number, b: any) => a + b.pending, 0), waiting: queueSummary.reduce((a: number, b: any) => a + b.waiting, 0), breached: 0 }, nextTicket: nextTicketFromServer } : null) + | ^ + 44 | + 45 | if (!cardContext || !cardContext.nextTicket) { + 46 | return ( +Next.js build worker exited with code: 1 and signal: null +ÔÇëELIFECYCLEÔÇë Command failed with exit code 1. diff --git a/web/convex/schema.ts b/web/convex/schema.ts index 2067b0b..2156464 100644 --- a/web/convex/schema.ts +++ b/web/convex/schema.ts @@ -41,6 +41,7 @@ export default defineSchema({ reference: v.number(), subject: v.string(), summary: v.optional(v.string()), + description: v.optional(v.string()), status: v.string(), priority: v.string(), channel: v.string(), @@ -59,7 +60,8 @@ export default defineSchema({ .index("by_tenant_status", ["tenantId", "status"]) .index("by_tenant_queue", ["tenantId", "queueId"]) .index("by_tenant_assignee", ["tenantId", "assigneeId"]) - .index("by_tenant_reference", ["tenantId", "reference"]), + .index("by_tenant_reference", ["tenantId", "reference"]) + .index("by_tenant", ["tenantId"]), ticketComments: defineTable({ ticketId: v.id("tickets"), @@ -87,4 +89,3 @@ export default defineSchema({ createdAt: v.number(), }).index("by_ticket", ["ticketId"]), }); - diff --git a/web/convex/tickets.ts b/web/convex/tickets.ts index 880e356..5bd8e32 100644 --- a/web/convex/tickets.ts +++ b/web/convex/tickets.ts @@ -1,8 +1,8 @@ import { internalMutation, mutation, query } from "./_generated/server"; import { v } from "convex/values"; -import { Id } from "./_generated/dataModel"; +import { Id, type Doc } from "./_generated/dataModel"; -const STATUS_ORDER = ["URGENT", "HIGH", "MEDIUM", "LOW"] as const; +const PRIORITY_ORDER = ["URGENT", "HIGH", "MEDIUM", "LOW"] as const; export const list = query({ args: { @@ -15,16 +15,28 @@ export const list = query({ limit: v.optional(v.number()), }, handler: async (ctx, args) => { - let q = ctx.db - .query("tickets") - .withIndex("by_tenant_status", (q) => q.eq("tenantId", args.tenantId)); - - const all = await q.collect(); - let filtered = all; - if (args.status) filtered = filtered.filter((t) => t.status === args.status); + // Choose best index based on provided args for efficiency + let base: Doc<"tickets">[] = []; + if (args.status) { + base = await ctx.db + .query("tickets") + .withIndex("by_tenant_status", (q) => q.eq("tenantId", args.tenantId).eq("status", args.status!)) + .collect(); + } else if (args.queueId) { + base = await ctx.db + .query("tickets") + .withIndex("by_tenant_queue", (q) => q.eq("tenantId", args.tenantId).eq("queueId", args.queueId!)) + .collect(); + } else { + base = await ctx.db + .query("tickets") + .withIndex("by_tenant", (q) => q.eq("tenantId", args.tenantId)) + .collect(); + } + let filtered = base; + if (args.priority) filtered = filtered.filter((t) => t.priority === args.priority); if (args.channel) filtered = filtered.filter((t) => t.channel === args.channel); - if (args.queueId) filtered = filtered.filter((t) => t.queueId === args.queueId); if (args.search) { const term = args.search.toLowerCase(); filtered = filtered.filter( @@ -38,9 +50,9 @@ export const list = query({ // hydrate requester and assignee const result = await Promise.all( limited.map(async (t) => { - const requester = await ctx.db.get(t.requesterId); - const assignee = t.assigneeId ? await ctx.db.get(t.assigneeId) : null; - const queue = t.queueId ? await ctx.db.get(t.queueId) : null; + const requester = (await ctx.db.get(t.requesterId)) as Doc<"users"> | null; + const assignee = t.assigneeId ? ((await ctx.db.get(t.assigneeId)) as Doc<"users"> | null) : null; + const queue = t.queueId ? ((await ctx.db.get(t.queueId)) as Doc<"queues"> | null) : null; return { id: t._id, reference: t.reference, @@ -80,7 +92,7 @@ export const list = query({ }) ); // sort by updatedAt desc - return result.sort((a, b) => (b.updatedAt as any) - (a.updatedAt as any)); + return result.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0)); }, }); @@ -89,9 +101,9 @@ export const getById = query({ handler: async (ctx, { tenantId, id }) => { const t = await ctx.db.get(id); if (!t || t.tenantId !== tenantId) return null; - const requester = await ctx.db.get(t.requesterId); - const assignee = t.assigneeId ? await ctx.db.get(t.assigneeId) : null; - const queue = t.queueId ? await ctx.db.get(t.queueId) : null; + const requester = (await ctx.db.get(t.requesterId)) as Doc<"users"> | null; + const assignee = t.assigneeId ? ((await ctx.db.get(t.assigneeId)) as Doc<"users"> | null) : null; + const queue = t.queueId ? ((await ctx.db.get(t.queueId)) as Doc<"queues"> | null) : null; const comments = await ctx.db .query("ticketComments") .withIndex("by_ticket", (q) => q.eq("ticketId", id)) @@ -103,7 +115,7 @@ export const getById = query({ const commentsHydrated = await Promise.all( comments.map(async (c) => { - const author = await ctx.db.get(c.authorId); + const author = (await ctx.db.get(c.authorId)) as Doc<"users"> | null; const attachments = await Promise.all( (c.attachments ?? []).map(async (att) => ({ id: att.storageId, @@ -296,7 +308,7 @@ export const changeAssignee = mutation({ handler: async (ctx, { ticketId, assigneeId, actorId }) => { const now = Date.now(); await ctx.db.patch(ticketId, { assigneeId, updatedAt: now }); - const user = await ctx.db.get(assigneeId); + const user = (await ctx.db.get(assigneeId)) as Doc<"users"> | null; await ctx.db.insert("ticketEvents", { ticketId, type: "ASSIGNEE_CHANGED", @@ -311,7 +323,7 @@ export const changeQueue = mutation({ handler: async (ctx, { ticketId, queueId, actorId }) => { const now = Date.now(); await ctx.db.patch(ticketId, { queueId, updatedAt: now }); - const queue = await ctx.db.get(queueId); + const queue = (await ctx.db.get(queueId)) as Doc<"queues"> | null; await ctx.db.insert("ticketEvents", { ticketId, type: "QUEUE_CHANGED", @@ -329,10 +341,18 @@ export const playNext = mutation({ }, handler: async (ctx, { tenantId, queueId, agentId }) => { // Find eligible tickets: not resolved/closed and not assigned - let candidates = await ctx.db - .query("tickets") - .withIndex("by_tenant_queue", (q) => q.eq("tenantId", tenantId).eq("queueId", queueId ?? undefined as any)) - .collect(); + let candidates: Doc<"tickets">[] = [] + if (queueId) { + candidates = await ctx.db + .query("tickets") + .withIndex("by_tenant_queue", (q) => q.eq("tenantId", tenantId).eq("queueId", queueId)) + .collect() + } else { + candidates = await ctx.db + .query("tickets") + .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) + .collect() + } candidates = candidates.filter( (t) => t.status !== "RESOLVED" && t.status !== "CLOSED" && !t.assigneeId @@ -341,17 +361,18 @@ export const playNext = mutation({ if (candidates.length === 0) return null; // prioritize by priority then createdAt + const rank: Record = { URGENT: 0, HIGH: 1, MEDIUM: 2, LOW: 3 } candidates.sort((a, b) => { - const pa = STATUS_ORDER.indexOf(a.priority as any); - const pb = STATUS_ORDER.indexOf(b.priority as any); - if (pa !== pb) return pa - pb; - return a.createdAt - b.createdAt; - }); + const pa = rank[a.priority] ?? 999 + const pb = rank[b.priority] ?? 999 + if (pa !== pb) return pa - pb + return a.createdAt - b.createdAt + }) const chosen = candidates[0]; const now = Date.now(); await ctx.db.patch(chosen._id, { assigneeId: agentId, status: chosen.status === "NEW" ? "OPEN" : chosen.status, updatedAt: now }); - const agent = await ctx.db.get(agentId); + const agent = (await ctx.db.get(agentId)) as Doc<"users"> | null; await ctx.db.insert("ticketEvents", { ticketId: chosen._id, type: "ASSIGNEE_CHANGED", @@ -359,51 +380,45 @@ export const playNext = mutation({ createdAt: now, }); - return await getPublicById(ctx, chosen._id); + // hydrate minimal public ticket like in list + const requester = (await ctx.db.get(chosen.requesterId)) as Doc<"users"> | null + const assignee = chosen.assigneeId ? ((await ctx.db.get(chosen.assigneeId)) as Doc<"users"> | null) : null + const queue = chosen.queueId ? ((await ctx.db.get(chosen.queueId)) as Doc<"queues"> | null) : null + return { + id: chosen._id, + reference: chosen.reference, + tenantId: chosen.tenantId, + subject: chosen.subject, + summary: chosen.summary, + status: chosen.status, + priority: chosen.priority, + channel: chosen.channel, + queue: queue?.name ?? null, + requester: requester && { + id: requester._id, + name: requester.name, + email: requester.email, + avatarUrl: requester.avatarUrl, + teams: requester.teams ?? [], + }, + assignee: assignee + ? { + id: assignee._id, + name: assignee.name, + email: assignee.email, + avatarUrl: assignee.avatarUrl, + teams: assignee.teams ?? [], + } + : null, + slaPolicy: null, + dueAt: chosen.dueAt ?? null, + firstResponseAt: chosen.firstResponseAt ?? null, + resolvedAt: chosen.resolvedAt ?? null, + updatedAt: chosen.updatedAt, + createdAt: chosen.createdAt, + tags: chosen.tags ?? [], + lastTimelineEntry: null, + metrics: null, + } }, }); - -// internal helper to hydrate a ticket in the same shape as list/getById -const getPublicById = async (ctx: any, id: Id<"tickets">) => { - const t = await ctx.db.get(id); - if (!t) return null; - const requester = await ctx.db.get(t.requesterId); - const assignee = t.assigneeId ? await ctx.db.get(t.assigneeId) : null; - const queue = t.queueId ? await ctx.db.get(t.queueId) : null; - return { - id: t._id, - reference: t.reference, - tenantId: t.tenantId, - subject: t.subject, - summary: t.summary, - status: t.status, - priority: t.priority, - channel: t.channel, - queue: queue?.name ?? null, - requester: requester && { - id: requester._id, - name: requester.name, - email: requester.email, - avatarUrl: requester.avatarUrl, - teams: requester.teams ?? [], - }, - assignee: assignee - ? { - id: assignee._id, - name: assignee.name, - email: assignee.email, - avatarUrl: assignee.avatarUrl, - teams: assignee.teams ?? [], - } - : null, - slaPolicy: null, - dueAt: t.dueAt ?? null, - firstResponseAt: t.firstResponseAt ?? null, - resolvedAt: t.resolvedAt ?? null, - updatedAt: t.updatedAt, - createdAt: t.createdAt, - tags: t.tags ?? [], - lastTimelineEntry: null, - metrics: null, - }; -}; diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs index 06e2d74..8502ebc 100644 --- a/web/eslint.config.mjs +++ b/web/eslint.config.mjs @@ -9,17 +9,24 @@ const compat = new FlatCompat({ baseDirectory: __dirname, }); -const eslintConfig = [ - ...compat.extends("next/core-web-vitals", "next/typescript"), - { - ignores: [ - "node_modules/**", - ".next/**", - "out/**", - "build/**", - "next-env.d.ts", - ], - }, -]; +const eslintConfig = [ + ...compat.extends("next/core-web-vitals", "next/typescript"), + { + ignores: [ + "node_modules/**", + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + ], + }, + { + rules: { + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/ban-ts-comment": "warn", + "react/no-unescaped-entities": "off", + }, + }, +]; export default eslintConfig; diff --git a/web/package.json b/web/package.json index 4ebaada..33efd98 100644 --- a/web/package.json +++ b/web/package.json @@ -33,6 +33,10 @@ "@radix-ui/react-tooltip": "^1.2.8", "@tabler/icons-react": "^3.35.0", "@tanstack/react-table": "^8.21.3", + "@tiptap/extension-link": "^3.6.5", + "@tiptap/extension-placeholder": "^3.6.5", + "@tiptap/react": "^3.6.5", + "@tiptap/starter-kit": "^3.6.5", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "convex": "^1.27.3", @@ -44,6 +48,7 @@ "react-dom": "19.1.0", "react-hook-form": "^7.64.0", "recharts": "^2.15.4", + "sanitize-html": "^2.17.0", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "vaul": "^1.1.2", @@ -55,6 +60,7 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@types/sanitize-html": "^2.16.0", "eslint": "^9", "eslint-config-next": "15.5.3", "prisma": "^6.16.2", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 7bcc24a..02ea927 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -71,6 +71,18 @@ importers: '@tanstack/react-table': specifier: ^8.21.3 version: 8.21.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@tiptap/extension-link': + specifier: ^3.6.5 + version: 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) + '@tiptap/extension-placeholder': + specifier: ^3.6.5 + version: 3.6.5(@tiptap/extensions@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)) + '@tiptap/react': + specifier: ^3.6.5 + version: 3.6.5(@floating-ui/dom@1.7.4)(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@tiptap/starter-kit': + specifier: ^3.6.5 + version: 3.6.5 class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -104,6 +116,9 @@ importers: recharts: specifier: ^2.15.4 version: 2.15.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + sanitize-html: + specifier: ^2.17.0 + version: 2.17.0 sonner: specifier: ^2.0.7 version: 2.0.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -132,6 +147,9 @@ importers: '@types/react-dom': specifier: ^19 version: 19.2.0(@types/react@19.2.0) + '@types/sanitize-html': + specifier: ^2.16.0 + version: 2.16.0 eslint: specifier: ^9 version: 9.37.0(jiti@2.6.1) @@ -1255,6 +1273,9 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@remirror/core-constants@3.0.0': + resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} + '@rollup/rollup-android-arm-eabi@4.52.4': resolution: {integrity: sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==} cpu: [arm] @@ -1484,6 +1505,160 @@ packages: resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} engines: {node: '>=12'} + '@tiptap/core@3.6.5': + resolution: {integrity: sha512-CgXuhevQbBcPfxaXzGZgIY9+aVMSAd68Q21g3EONz1iZBw026QgiaLhGK6jgGTErZL4GoNL/P+gC5nFCvN7+cA==} + peerDependencies: + '@tiptap/pm': ^3.6.5 + + '@tiptap/extension-blockquote@3.6.5': + resolution: {integrity: sha512-FOOgkLHXQ3zTiL2V1js5+PfaOHXuyr/GjeFZe+W1AUk58X/qJNOVGvKT1xlMOy9gy2ySgWmco7PhNXRRTimkWg==} + peerDependencies: + '@tiptap/core': ^3.6.5 + + '@tiptap/extension-bold@3.6.5': + resolution: {integrity: sha512-8JXC+K4DXtPDbClHxgRAZnXYO2an2I86PbpqUw+S7m17XCr4t39Sw9CeNBohOHS6Cl8uxOKAjSyCZzqdnYkn3g==} + peerDependencies: + '@tiptap/core': ^3.6.5 + + '@tiptap/extension-bubble-menu@3.6.5': + resolution: {integrity: sha512-RyCJghtkYZAljZQUfjk3B5tvVVCILsIYMR9XnC152uBiIuWsnz25qfdyBP+cOl6ONrQUvdscs0WmKvzN+nXZYw==} + peerDependencies: + '@tiptap/core': ^3.6.5 + '@tiptap/pm': ^3.6.5 + + '@tiptap/extension-bullet-list@3.6.5': + resolution: {integrity: sha512-AP81hyN7oTyv5zbNVRK35cQA7zuLnI5ItFFyqMQKWh90vfftXi/zhC9C7FWvKtEH7Kk68B338G2mi4tlXDgBFQ==} + peerDependencies: + '@tiptap/extension-list': ^3.6.5 + + '@tiptap/extension-code-block@3.6.5': + resolution: {integrity: sha512-VPPke3LqZYKPlbDBp8IcTJQwvYb1PP0L+2Qi2n3ebN4+gKn+KGhrjnkO+xNHCySWlqywQmMTIfWX1sxA0eVVdQ==} + peerDependencies: + '@tiptap/core': ^3.6.5 + '@tiptap/pm': ^3.6.5 + + '@tiptap/extension-code@3.6.5': + resolution: {integrity: sha512-U/cJFjE0hqBTbMb5J74e7ni5YReuJgS9NyJgTy94+Xt6vxR1vU4+qOl+3E0fOZtwDrxbLrsCQy3P3LvNb3HXdw==} + peerDependencies: + '@tiptap/core': ^3.6.5 + + '@tiptap/extension-document@3.6.5': + resolution: {integrity: sha512-0c7kxWBIEIcoHUG89vpHOF2h4CMa0q6VWXhZ+6iqcI5uyqaKwgcW/TbHZR0nAwEsZLdRCKaryn2kO7jXiCjfnA==} + peerDependencies: + '@tiptap/core': ^3.6.5 + + '@tiptap/extension-dropcursor@3.6.5': + resolution: {integrity: sha512-BsO3ufLHsdeV1ddChwQfi2Q4UkeqOF4LeUYPYBKfSg59aRKTSoxj3gZrAsaAm/0O3DmAiKNBiCtNRTJSApPEBQ==} + peerDependencies: + '@tiptap/extensions': ^3.6.5 + + '@tiptap/extension-floating-menu@3.6.5': + resolution: {integrity: sha512-ASKb5vHkYyB9g3vOAr2E2U+b6MbHk4Ff4PqngafGlWRAmOAmFxTcw9fLa3HKnj4pokSsYAEvYGOso99/W3GzhA==} + peerDependencies: + '@floating-ui/dom': ^1.0.0 + '@tiptap/core': ^3.6.5 + '@tiptap/pm': ^3.6.5 + + '@tiptap/extension-gapcursor@3.6.5': + resolution: {integrity: sha512-SHtp71zhV2bAQS8kaJ/otb2podGusDREZ9/SQ1rZi6yPcDFLS2KvIvsLssDwbjTuH6KefnsN6Vx01tzmXRAQig==} + peerDependencies: + '@tiptap/extensions': ^3.6.5 + + '@tiptap/extension-hard-break@3.6.5': + resolution: {integrity: sha512-6iMS6SzIn7+X95okRX8y3l/4f1G3lTrq24sbcAX4MHITncDC6g3TrdAxdA67Tqn5NI/OQx0LwF3kFJDO8QTAUg==} + peerDependencies: + '@tiptap/core': ^3.6.5 + + '@tiptap/extension-heading@3.6.5': + resolution: {integrity: sha512-jFS5saqTtfG6MM0sW4X6mZlLycT2ud0Oo1GOZkCyBClwSOpZI/EBLNRIgoXgNtWrY917vB7xTQgCpTVHbvVRsQ==} + peerDependencies: + '@tiptap/core': ^3.6.5 + + '@tiptap/extension-horizontal-rule@3.6.5': + resolution: {integrity: sha512-yNxcejI25j6NQMQuKQMTVmNYLnrHFCpzGAz1Ndzyar+gItYZXI9BLmMlwpLkIaJMpIKChj+2qHz25fPS5FlNFw==} + peerDependencies: + '@tiptap/core': ^3.6.5 + '@tiptap/pm': ^3.6.5 + + '@tiptap/extension-italic@3.6.5': + resolution: {integrity: sha512-2EtO2uffw5YnTQ1cieLPv9t7OKCfJFbgHRJPXf7Nnfh8XFh5AEyzw0qBNXZyLtlB28+HHSWLc/OHS6xMfwUy0A==} + peerDependencies: + '@tiptap/core': ^3.6.5 + + '@tiptap/extension-link@3.6.5': + resolution: {integrity: sha512-VLCDNwxLC1IPnWT3HLLJUg1Hflf8A2jfs7aNF4vyMTWmKnrk1zmN+VyXQTAkrqr27qE5FnmLhHOYF3SNolNucw==} + peerDependencies: + '@tiptap/core': ^3.6.5 + '@tiptap/pm': ^3.6.5 + + '@tiptap/extension-list-item@3.6.5': + resolution: {integrity: sha512-A5JKf2dNG6IRrHmkaqroq/VcD5SnXYXgpQpsF7HrPGIzUSIjvjQu088980NQPHyMuTanDMml+nZgd8RzHhRISA==} + peerDependencies: + '@tiptap/extension-list': ^3.6.5 + + '@tiptap/extension-list-keymap@3.6.5': + resolution: {integrity: sha512-OHGGTJMdUOBincMgYGEN4WzHrTB/GFeCxLDJraDknPx4VJVa3UVZS8F8xd5cb2WnACEF33Ud/0yK3aN6kHrbtQ==} + peerDependencies: + '@tiptap/extension-list': ^3.6.5 + + '@tiptap/extension-list@3.6.5': + resolution: {integrity: sha512-2S6wNeaGvvYzJygBhHRLP0YubJAzY00WxQSO3NvHFeLFRFvilCnmh0JGMAqsNU+Owpz0iVrWY0YZskN5gPeR9w==} + peerDependencies: + '@tiptap/core': ^3.6.5 + '@tiptap/pm': ^3.6.5 + + '@tiptap/extension-ordered-list@3.6.5': + resolution: {integrity: sha512-RiBl0Dkw8QtzS7OqUGm84BOyemw/N+hf8DYWsIqVysMRQAGBGhuklbw+DGpCL0nMHW4lh7WtvfKcb0yxLmhbbA==} + peerDependencies: + '@tiptap/extension-list': ^3.6.5 + + '@tiptap/extension-paragraph@3.6.5': + resolution: {integrity: sha512-AfuaBu+DKrRPspaLsXgo17dhuneISS6QsZTIzPeX21jFJcq3TjtD8wSzS4yRgzAQCEbupkI7t4JbtgxAIBNQHA==} + peerDependencies: + '@tiptap/core': ^3.6.5 + + '@tiptap/extension-placeholder@3.6.5': + resolution: {integrity: sha512-9CLixogEb/4UkEyuDr4JdOlLvphcOVfZMdNMKmUVQdqo4MuZCdTDyK5ypfTPQJl8aUo0oCiEhqE0bQerYlueJQ==} + peerDependencies: + '@tiptap/extensions': ^3.6.5 + + '@tiptap/extension-strike@3.6.5': + resolution: {integrity: sha512-QR7CUmRJ7fJkHtxqKajKIaX/B4xpKFOsAOJHbnqZ8wzOtnEL5IlsmoUnbKBoVn0+2R2YKKvMK3lepGtAcVCfIQ==} + peerDependencies: + '@tiptap/core': ^3.6.5 + + '@tiptap/extension-text@3.6.5': + resolution: {integrity: sha512-PVZDWUa25xPzmEN6WWA103yvYJn+NBvWb7WrQwWu9LkKUgd98ZgV3yFaEem/Ybugl/NDPV7q8GGaH+2wEg/VeA==} + peerDependencies: + '@tiptap/core': ^3.6.5 + + '@tiptap/extension-underline@3.6.5': + resolution: {integrity: sha512-Ul1mO0H1e2vfvN5g48X/YQ8w1xFTpLqce+GUhi0OmXaZnVOTIMtLuN/zAAPjD+uw+79JVGjYa53lbo1dyhOfAw==} + peerDependencies: + '@tiptap/core': ^3.6.5 + + '@tiptap/extensions@3.6.5': + resolution: {integrity: sha512-7aadEaRjSbFAIp3WGYR1LXrvtVprmBNxw3FakEUMJ+XKmGNErDJgDMZh+siAYw5MWwCCGa5kKu8Qi/i+DU+ILg==} + peerDependencies: + '@tiptap/core': ^3.6.5 + '@tiptap/pm': ^3.6.5 + + '@tiptap/pm@3.6.5': + resolution: {integrity: sha512-S+j6MPgUXRIQd5/mdaLjaJnOt4ptFwjqGjGMUfBbf9a3uKpXUXaCCzfuC6ZikwaUtoVh4KN9BU3HCYDtgtENPA==} + + '@tiptap/react@3.6.5': + resolution: {integrity: sha512-kum9fYzY6qmHuabcXDUTX2sVLdtJtZS0kN91mwD29Ue8HUkjVvEX92PwV2HtgNw3WFMaVxgm/dtm3XPTAlUEwg==} + peerDependencies: + '@tiptap/core': ^3.6.5 + '@tiptap/pm': ^3.6.5 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + '@types/react-dom': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tiptap/starter-kit@3.6.5': + resolution: {integrity: sha512-LNAJQstB/VazmMlRbUyu3rCNVQ9af25Ywkn3Uyuwt3Ks9ZlliIm/x/zertdXTY2adoig+b36zT5Xcx1O4IdJ3A==} + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -1523,6 +1698,15 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + '@types/node@20.19.19': resolution: {integrity: sha512-pb1Uqj5WJP7wrcbLU7Ru4QtA0+3kAXrkutGiD26wUKzSMgNNaPARTUDQmElUXp64kh3cWdou3Q0C7qwwxqSFmg==} @@ -1534,6 +1718,12 @@ packages: '@types/react@19.2.0': resolution: {integrity: sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA==} + '@types/sanitize-html@2.16.0': + resolution: {integrity: sha512-l6rX1MUXje5ztPT0cAFtUayXF06DqPhRyfVXareEN5gGCFaP/iwsxIyKODr9XDhfxPpN6vXUFNfo5kZMXCxBtw==} + + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@typescript-eslint/eslint-plugin@8.45.0': resolution: {integrity: sha512-HC3y9CVuevvWCl/oyZuI47dOeDF9ztdMEfMH8/DW/Mhwa9cCLnK1oD7JoTVGW/u7kFzNZUKUoyJEqkaJh5y3Wg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1910,6 +2100,9 @@ packages: react: optional: true + crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -2010,6 +2203,10 @@ packages: resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==} engines: {node: '>=16.0.0'} + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -2038,6 +2235,19 @@ packages: dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + dotenv@16.6.1: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} @@ -2060,6 +2270,10 @@ packages: resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} engines: {node: '>=10.13.0'} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + es-abstract@1.24.0: resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} engines: {node: '>= 0.4'} @@ -2393,6 +2607,9 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -2484,6 +2701,10 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -2640,6 +2861,12 @@ packages: resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} engines: {node: '>= 12.0.0'} + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + + linkifyjs@4.3.2: + resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -2665,10 +2892,17 @@ packages: magic-string@0.30.19: resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + markdown-it@14.1.0: + resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -2785,6 +3019,9 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + orderedmap@2.1.1: + resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} + own-keys@1.0.1: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} @@ -2801,6 +3038,9 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-srcset@1.0.2: + resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -2873,6 +3113,68 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + prosemirror-changeset@2.3.1: + resolution: {integrity: sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==} + + prosemirror-collab@1.3.1: + resolution: {integrity: sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==} + + prosemirror-commands@1.7.1: + resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==} + + prosemirror-dropcursor@1.8.2: + resolution: {integrity: sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==} + + prosemirror-gapcursor@1.3.2: + resolution: {integrity: sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==} + + prosemirror-history@1.4.1: + resolution: {integrity: sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==} + + prosemirror-inputrules@1.5.0: + resolution: {integrity: sha512-K0xJRCmt+uSw7xesnHmcn72yBGTbY45vm8gXI4LZXbx2Z0jwh5aF9xrGQgrVPu0WbyFVFF3E/o9VhJYz6SQWnA==} + + prosemirror-keymap@1.2.3: + resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==} + + prosemirror-markdown@1.13.2: + resolution: {integrity: sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==} + + prosemirror-menu@1.2.5: + resolution: {integrity: sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==} + + prosemirror-model@1.25.3: + resolution: {integrity: sha512-dY2HdaNXlARknJbrManZ1WyUtos+AP97AmvqdOQtWtrrC5g4mohVX5DTi9rXNFSk09eczLq9GuNTtq3EfMeMGA==} + + prosemirror-schema-basic@1.2.4: + resolution: {integrity: sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==} + + prosemirror-schema-list@1.5.1: + resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==} + + prosemirror-state@1.4.3: + resolution: {integrity: sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==} + + prosemirror-tables@1.8.1: + resolution: {integrity: sha512-DAgDoUYHCcc6tOGpLVPSU1k84kCUWTWnfWX3UDy2Delv4ryH0KqTD6RBI6k4yi9j9I8gl3j8MkPpRD/vWPZbug==} + + prosemirror-trailing-node@3.0.0: + resolution: {integrity: sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==} + peerDependencies: + prosemirror-model: ^1.22.1 + prosemirror-state: ^1.4.2 + prosemirror-view: ^1.33.8 + + prosemirror-transform@1.10.4: + resolution: {integrity: sha512-pwDy22nAnGqNR1feOQKHxoFkkUtepoFAd3r2hbEDsnf4wp57kKA36hXsB3njA9FtONBEwSDnDeCiJe+ItD+ykw==} + + prosemirror-view@1.41.2: + resolution: {integrity: sha512-PGS/jETmh+Qjmre/6vcG7SNHAKiGc4vKOJmHMPRmvcUl7ISuVtrtHmH06UDUwaim4NDJfZfVMl7U7JkMMETa6g==} + + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2996,6 +3298,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rope-sequence@1.3.4: + resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -3011,6 +3316,9 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} + sanitize-html@2.17.0: + resolution: {integrity: sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA==} + scheduler@0.26.0: resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} @@ -3227,6 +3535,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -3335,6 +3646,9 @@ packages: jsdom: optional: true + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -4297,6 +4611,8 @@ snapshots: '@radix-ui/rect@1.1.1': {} + '@remirror/core-constants@3.0.0': {} + '@rollup/rollup-android-arm-eabi@4.52.4': optional: true @@ -4460,6 +4776,187 @@ snapshots: '@tanstack/table-core@8.21.3': {} + '@tiptap/core@3.6.5(@tiptap/pm@3.6.5)': + dependencies: + '@tiptap/pm': 3.6.5 + + '@tiptap/extension-blockquote@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': + dependencies: + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + + '@tiptap/extension-bold@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': + dependencies: + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + + '@tiptap/extension-bubble-menu@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)': + dependencies: + '@floating-ui/dom': 1.7.4 + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/pm': 3.6.5 + optional: true + + '@tiptap/extension-bullet-list@3.6.5(@tiptap/extension-list@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5))': + dependencies: + '@tiptap/extension-list': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) + + '@tiptap/extension-code-block@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)': + dependencies: + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/pm': 3.6.5 + + '@tiptap/extension-code@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': + dependencies: + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + + '@tiptap/extension-document@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': + dependencies: + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + + '@tiptap/extension-dropcursor@3.6.5(@tiptap/extensions@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5))': + dependencies: + '@tiptap/extensions': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) + + '@tiptap/extension-floating-menu@3.6.5(@floating-ui/dom@1.7.4)(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)': + dependencies: + '@floating-ui/dom': 1.7.4 + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/pm': 3.6.5 + optional: true + + '@tiptap/extension-gapcursor@3.6.5(@tiptap/extensions@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5))': + dependencies: + '@tiptap/extensions': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) + + '@tiptap/extension-hard-break@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': + dependencies: + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + + '@tiptap/extension-heading@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': + dependencies: + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + + '@tiptap/extension-horizontal-rule@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)': + dependencies: + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/pm': 3.6.5 + + '@tiptap/extension-italic@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': + dependencies: + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + + '@tiptap/extension-link@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)': + dependencies: + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/pm': 3.6.5 + linkifyjs: 4.3.2 + + '@tiptap/extension-list-item@3.6.5(@tiptap/extension-list@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5))': + dependencies: + '@tiptap/extension-list': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) + + '@tiptap/extension-list-keymap@3.6.5(@tiptap/extension-list@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5))': + dependencies: + '@tiptap/extension-list': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) + + '@tiptap/extension-list@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)': + dependencies: + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/pm': 3.6.5 + + '@tiptap/extension-ordered-list@3.6.5(@tiptap/extension-list@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5))': + dependencies: + '@tiptap/extension-list': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) + + '@tiptap/extension-paragraph@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': + dependencies: + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + + '@tiptap/extension-placeholder@3.6.5(@tiptap/extensions@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5))': + dependencies: + '@tiptap/extensions': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) + + '@tiptap/extension-strike@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': + dependencies: + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + + '@tiptap/extension-text@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': + dependencies: + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + + '@tiptap/extension-underline@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': + dependencies: + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + + '@tiptap/extensions@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)': + dependencies: + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/pm': 3.6.5 + + '@tiptap/pm@3.6.5': + dependencies: + prosemirror-changeset: 2.3.1 + prosemirror-collab: 1.3.1 + prosemirror-commands: 1.7.1 + prosemirror-dropcursor: 1.8.2 + prosemirror-gapcursor: 1.3.2 + prosemirror-history: 1.4.1 + prosemirror-inputrules: 1.5.0 + prosemirror-keymap: 1.2.3 + prosemirror-markdown: 1.13.2 + prosemirror-menu: 1.2.5 + prosemirror-model: 1.25.3 + prosemirror-schema-basic: 1.2.4 + prosemirror-schema-list: 1.5.1 + prosemirror-state: 1.4.3 + prosemirror-tables: 1.8.1 + prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.2) + prosemirror-transform: 1.10.4 + prosemirror-view: 1.41.2 + + '@tiptap/react@3.6.5(@floating-ui/dom@1.7.4)(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/pm': 3.6.5 + '@types/react': 19.2.0 + '@types/react-dom': 19.2.0(@types/react@19.2.0) + '@types/use-sync-external-store': 0.0.6 + fast-deep-equal: 3.1.3 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + use-sync-external-store: 1.6.0(react@19.1.0) + optionalDependencies: + '@tiptap/extension-bubble-menu': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) + '@tiptap/extension-floating-menu': 3.6.5(@floating-ui/dom@1.7.4)(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) + transitivePeerDependencies: + - '@floating-ui/dom' + + '@tiptap/starter-kit@3.6.5': + dependencies: + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/extension-blockquote': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) + '@tiptap/extension-bold': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) + '@tiptap/extension-bullet-list': 3.6.5(@tiptap/extension-list@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)) + '@tiptap/extension-code': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) + '@tiptap/extension-code-block': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) + '@tiptap/extension-document': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) + '@tiptap/extension-dropcursor': 3.6.5(@tiptap/extensions@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)) + '@tiptap/extension-gapcursor': 3.6.5(@tiptap/extensions@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)) + '@tiptap/extension-hard-break': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) + '@tiptap/extension-heading': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) + '@tiptap/extension-horizontal-rule': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) + '@tiptap/extension-italic': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) + '@tiptap/extension-link': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) + '@tiptap/extension-list': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) + '@tiptap/extension-list-item': 3.6.5(@tiptap/extension-list@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)) + '@tiptap/extension-list-keymap': 3.6.5(@tiptap/extension-list@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)) + '@tiptap/extension-ordered-list': 3.6.5(@tiptap/extension-list@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)) + '@tiptap/extension-paragraph': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) + '@tiptap/extension-strike': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) + '@tiptap/extension-text': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) + '@tiptap/extension-underline': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) + '@tiptap/extensions': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) + '@tiptap/pm': 3.6.5 + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -4495,6 +4992,15 @@ snapshots: '@types/json5@0.0.29': {} + '@types/linkify-it@5.0.0': {} + + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/mdurl@2.0.0': {} + '@types/node@20.19.19': dependencies: undici-types: 6.21.0 @@ -4507,6 +5013,12 @@ snapshots: dependencies: csstype: 3.1.3 + '@types/sanitize-html@2.16.0': + dependencies: + htmlparser2: 8.0.2 + + '@types/use-sync-external-store@0.0.6': {} + '@typescript-eslint/eslint-plugin@8.45.0(@typescript-eslint/parser@8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.1 @@ -4911,6 +5423,8 @@ snapshots: optionalDependencies: react: 19.1.0 + crelt@1.0.6: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -4995,6 +5509,8 @@ snapshots: deepmerge-ts@7.1.5: {} + deepmerge@4.3.1: {} + define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 @@ -5024,6 +5540,24 @@ snapshots: '@babel/runtime': 7.28.4 csstype: 3.1.3 + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + dotenv@16.6.1: {} dunder-proto@1.0.1: @@ -5046,6 +5580,8 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.0 + entities@4.5.0: {} + es-abstract@1.24.0: dependencies: array-buffer-byte-length: 1.0.2 @@ -5572,6 +6108,13 @@ snapshots: dependencies: function-bind: 1.1.2 + htmlparser2@8.0.2: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 + ignore@5.3.2: {} ignore@7.0.5: {} @@ -5664,6 +6207,8 @@ snapshots: is-number@7.0.0: {} + is-plain-object@5.0.0: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -5803,6 +6348,12 @@ snapshots: lightningcss-win32-arm64-msvc: 1.30.1 lightningcss-win32-x64-msvc: 1.30.1 + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + + linkifyjs@4.3.2: {} + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -5825,8 +6376,19 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + markdown-it@14.1.0: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + math-intrinsics@1.1.0: {} + mdurl@2.0.0: {} + merge2@1.4.1: {} micromatch@4.0.8: @@ -5949,6 +6511,8 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + orderedmap@2.1.1: {} + own-keys@1.0.1: dependencies: get-intrinsic: 1.3.0 @@ -5967,6 +6531,8 @@ snapshots: dependencies: callsites: 3.1.0 + parse-srcset@1.0.2: {} + path-exists@4.0.0: {} path-key@3.1.1: {} @@ -6026,6 +6592,111 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + prosemirror-changeset@2.3.1: + dependencies: + prosemirror-transform: 1.10.4 + + prosemirror-collab@1.3.1: + dependencies: + prosemirror-state: 1.4.3 + + prosemirror-commands@1.7.1: + dependencies: + prosemirror-model: 1.25.3 + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.4 + + prosemirror-dropcursor@1.8.2: + dependencies: + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.4 + prosemirror-view: 1.41.2 + + prosemirror-gapcursor@1.3.2: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.3 + prosemirror-state: 1.4.3 + prosemirror-view: 1.41.2 + + prosemirror-history@1.4.1: + dependencies: + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.4 + prosemirror-view: 1.41.2 + rope-sequence: 1.3.4 + + prosemirror-inputrules@1.5.0: + dependencies: + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.4 + + prosemirror-keymap@1.2.3: + dependencies: + prosemirror-state: 1.4.3 + w3c-keyname: 2.2.8 + + prosemirror-markdown@1.13.2: + dependencies: + '@types/markdown-it': 14.1.2 + markdown-it: 14.1.0 + prosemirror-model: 1.25.3 + + prosemirror-menu@1.2.5: + dependencies: + crelt: 1.0.6 + prosemirror-commands: 1.7.1 + prosemirror-history: 1.4.1 + prosemirror-state: 1.4.3 + + prosemirror-model@1.25.3: + dependencies: + orderedmap: 2.1.1 + + prosemirror-schema-basic@1.2.4: + dependencies: + prosemirror-model: 1.25.3 + + prosemirror-schema-list@1.5.1: + dependencies: + prosemirror-model: 1.25.3 + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.4 + + prosemirror-state@1.4.3: + dependencies: + prosemirror-model: 1.25.3 + prosemirror-transform: 1.10.4 + prosemirror-view: 1.41.2 + + prosemirror-tables@1.8.1: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.3 + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.4 + prosemirror-view: 1.41.2 + + prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.2): + dependencies: + '@remirror/core-constants': 3.0.0 + escape-string-regexp: 4.0.0 + prosemirror-model: 1.25.3 + prosemirror-state: 1.4.3 + prosemirror-view: 1.41.2 + + prosemirror-transform@1.10.4: + dependencies: + prosemirror-model: 1.25.3 + + prosemirror-view@1.41.2: + dependencies: + prosemirror-model: 1.25.3 + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.4 + + punycode.js@2.3.1: {} + punycode@2.3.1: {} pure-rand@6.1.0: {} @@ -6181,6 +6852,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.52.4 fsevents: 2.3.3 + rope-sequence@1.3.4: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -6204,6 +6877,15 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 + sanitize-html@2.17.0: + dependencies: + deepmerge: 4.3.1 + escape-string-regexp: 4.0.0 + htmlparser2: 8.0.2 + is-plain-object: 5.0.0 + parse-srcset: 1.0.2 + postcss: 8.5.6 + scheduler@0.26.0: {} semver@6.3.1: {} @@ -6472,6 +7154,8 @@ snapshots: typescript@5.9.3: {} + uc.micro@2.1.0: {} + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -6617,6 +7301,8 @@ snapshots: - supports-color - terser + w3c-keyname@2.2.8: {} + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 diff --git a/web/src/app/globals.css b/web/src/app/globals.css index d378588..997bf7c 100644 --- a/web/src/app/globals.css +++ b/web/src/app/globals.css @@ -112,11 +112,29 @@ --sidebar-ring: oklch(0.71 0.15 254.6); } -@layer base { - * { - @apply border-border outline-ring/50; - } - body { - @apply bg-background text-foreground font-sans antialiased; - } -} +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground font-sans antialiased; + } +} + +@layer components { + /* Tipografia básica para conteúdos rich text (Tiptap) */ + .rich-text { + @apply text-foreground; + } + .rich-text p { @apply my-2; } + .rich-text a { @apply text-primary underline; } + .rich-text ul { @apply my-2 list-disc ps-5; } + .rich-text ol { @apply my-2 list-decimal ps-5; } + .rich-text li { @apply my-1; } + .rich-text blockquote { @apply my-3 border-l-2 border-muted-foreground/30 ps-3 text-muted-foreground; } + .rich-text h1 { @apply text-xl font-semibold my-3; } + .rich-text h2 { @apply text-lg font-semibold my-3; } + .rich-text h3 { @apply text-base font-semibold my-2; } + .rich-text code { @apply rounded bg-muted px-1 py-0.5 text-xs; } + .rich-text pre { @apply my-3 overflow-x-auto rounded bg-muted p-3 text-xs; } +} diff --git a/web/src/app/tickets/[id]/page.tsx b/web/src/app/tickets/[id]/page.tsx index e4d8224..b150aee 100644 --- a/web/src/app/tickets/[id]/page.tsx +++ b/web/src/app/tickets/[id]/page.tsx @@ -3,6 +3,7 @@ import { SiteHeader } from "@/components/site-header" import { TicketDetailView } from "@/components/tickets/ticket-detail-view" import { TicketDetailStatic } from "@/components/tickets/ticket-detail-static" import { getTicketById } from "@/lib/mocks/tickets" +import type { TicketWithDetails } from "@/lib/schemas/ticket" type TicketDetailPageProps = { params: Promise<{ id: string }> @@ -24,7 +25,7 @@ export default async function TicketDetailPage({ params }: TicketDetailPageProps /> } > - {isMock && mock ? : } + {isMock && mock ? : } ) } diff --git a/web/src/app/tickets/new/page.tsx b/web/src/app/tickets/new/page.tsx index 1354e53..e7674c9 100644 --- a/web/src/app/tickets/new/page.tsx +++ b/web/src/app/tickets/new/page.tsx @@ -1,6 +1,8 @@ "use client"; import { useMemo, useState } from "react"; +import type { Id } from "@/convex/_generated/dataModel"; +import type { TicketQueueSummary } from "@/lib/schemas/ticket"; import { useRouter } from "next/navigation"; import { useMutation, useQuery } from "convex/react"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -8,12 +10,14 @@ import { useMutation, useQuery } from "convex/react"; import { api } from "@/convex/_generated/api"; import { DEFAULT_TENANT_ID } from "@/lib/constants"; import { useAuth } from "@/lib/auth-client"; +import { RichTextEditor } from "@/components/ui/rich-text-editor"; export default function NewTicketPage() { const router = useRouter(); const { userId } = useAuth(); - const queues = useQuery(api.queues.summary, { tenantId: DEFAULT_TENANT_ID }) ?? []; + const queues = (useQuery(api.queues.summary, { tenantId: DEFAULT_TENANT_ID }) as TicketQueueSummary[] | undefined) ?? []; const create = useMutation(api.tickets.create); + const addComment = useMutation(api.tickets.addComment); const ensureDefaults = useMutation(api.bootstrap.ensureDefaults); const [subject, setSubject] = useState(""); @@ -21,25 +25,30 @@ export default function NewTicketPage() { const [priority, setPriority] = useState("MEDIUM"); const [channel, setChannel] = useState("MANUAL"); const [queueName, setQueueName] = useState(null); + const [description, setDescription] = useState(""); - const queueOptions = useMemo(() => queues.map((q: any) => q.name), [queues]); + const queueOptions = useMemo(() => queues.map((q) => q.name), [queues]); async function submit(e: React.FormEvent) { e.preventDefault(); if (!userId) return; if (queues.length === 0) await ensureDefaults({ tenantId: DEFAULT_TENANT_ID }); // Encontrar a fila pelo nome (simples) - const selQueue = (queues as any[]).find((q: any) => q.name === queueName); - const queueId = selQueue ? selQueue.id : undefined; + const selQueue = queues.find((q) => q.name === queueName); + const queueId = selQueue ? (selQueue.id as Id<"queues">) : undefined; const id = await create({ tenantId: DEFAULT_TENANT_ID, subject, summary, priority, channel, - queueId: queueId as any, - requesterId: userId as any, + queueId, + requesterId: userId as Id<"users">, }); + const hasDescription = description.replace(/<[^>]*>/g, "").trim().length > 0 + if (hasDescription) { + await addComment({ ticketId: id as Id<"tickets">, authorId: userId as Id<"users">, visibility: "PUBLIC", body: description, attachments: [] }) + } router.replace(`/tickets/${id}`); } @@ -55,6 +64,10 @@ export default function NewTicketPage() {