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
This commit is contained in:
parent
9b0c0bd80a
commit
ea60c3b841
26 changed files with 1390 additions and 245 deletions
128
web/build.log
Normal file
128
web/build.log
Normal file
|
|
@ -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.
|
||||
|
|
@ -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"]),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// 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));
|
||||
.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;
|
||||
|
||||
const all = await q.collect();
|
||||
let filtered = all;
|
||||
if (args.status) filtered = filtered.filter((t) => t.status === args.status);
|
||||
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
|
||||
let candidates: Doc<"tickets">[] = []
|
||||
if (queueId) {
|
||||
candidates = await ctx.db
|
||||
.query("tickets")
|
||||
.withIndex("by_tenant_queue", (q) => q.eq("tenantId", tenantId).eq("queueId", queueId ?? undefined as any))
|
||||
.collect();
|
||||
.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<string, number> = { 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,26 +380,19 @@ export const playNext = mutation({
|
|||
createdAt: now,
|
||||
});
|
||||
|
||||
return await getPublicById(ctx, chosen._id);
|
||||
},
|
||||
});
|
||||
|
||||
// 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;
|
||||
// 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: t._id,
|
||||
reference: t.reference,
|
||||
tenantId: t.tenantId,
|
||||
subject: t.subject,
|
||||
summary: t.summary,
|
||||
status: t.status,
|
||||
priority: t.priority,
|
||||
channel: t.channel,
|
||||
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,
|
||||
|
|
@ -397,13 +411,14 @@ const getPublicById = async (ctx: any, id: Id<"tickets">) => {
|
|||
}
|
||||
: null,
|
||||
slaPolicy: null,
|
||||
dueAt: t.dueAt ?? null,
|
||||
firstResponseAt: t.firstResponseAt ?? null,
|
||||
resolvedAt: t.resolvedAt ?? null,
|
||||
updatedAt: t.updatedAt,
|
||||
createdAt: t.createdAt,
|
||||
tags: t.tags ?? [],
|
||||
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,
|
||||
};
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -20,6 +20,13 @@ const eslintConfig = [
|
|||
"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;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
686
web/pnpm-lock.yaml
generated
686
web/pnpm-lock.yaml
generated
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -120,3 +120,21 @@
|
|||
@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; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ? <TicketDetailStatic ticket={mock as any} /> : <TicketDetailView id={id} />}
|
||||
{isMock && mock ? <TicketDetailStatic ticket={mock as TicketWithDetails} /> : <TicketDetailView id={id} />}
|
||||
</AppShell>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string | null>(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() {
|
|||
<label className="text-sm">Resumo</label>
|
||||
<textarea className="w-full rounded-md border bg-background px-3 py-2" value={summary} onChange={(e) => setSummary(e.target.value)} rows={3} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm">Descrição</label>
|
||||
<RichTextEditor value={description} onChange={setDescription} placeholder="Detalhe o problema, passos para reproduzir, links, etc." />
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm">Prioridade</label>
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||
<SidebarHeader className="gap-3">
|
||||
<VersionSwitcher
|
||||
label="Release"
|
||||
versions={navigation.versions}
|
||||
versions={[...navigation.versions]}
|
||||
defaultVersion={navigation.versions[0]}
|
||||
/>
|
||||
<SearchForm placeholder="Buscar tickets, macros ou artigos" />
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
import { z } from "zod"
|
||||
import { useState } from "react"
|
||||
import type { Id } from "@/convex/_generated/dataModel"
|
||||
import type { TicketQueueSummary } from "@/lib/schemas/ticket"
|
||||
import { useMutation, useQuery } from "convex/react"
|
||||
// @ts-ignore
|
||||
import { api } from "@/convex/_generated/api"
|
||||
|
|
@ -17,10 +19,12 @@ import { zodResolver } from "@hookform/resolvers/zod"
|
|||
import { toast } from "sonner"
|
||||
import { Spinner } from "@/components/ui/spinner"
|
||||
import { Dropzone } from "@/components/ui/dropzone"
|
||||
import { RichTextEditor } from "@/components/ui/rich-text-editor"
|
||||
|
||||
const schema = z.object({
|
||||
subject: z.string().min(3, "Informe um assunto"),
|
||||
summary: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
priority: z.enum(["LOW", "MEDIUM", "HIGH", "URGENT"]).default("MEDIUM"),
|
||||
channel: z.enum(["EMAIL", "WHATSAPP", "CHAT", "PHONE", "API", "MANUAL"]).default("MANUAL"),
|
||||
queueName: z.string().nullable().optional(),
|
||||
|
|
@ -31,11 +35,11 @@ export function NewTicketDialog() {
|
|||
const [loading, setLoading] = useState(false)
|
||||
const form = useForm<z.infer<typeof schema>>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: { subject: "", summary: "", priority: "MEDIUM", channel: "MANUAL", queueName: null },
|
||||
defaultValues: { subject: "", summary: "", description: "", priority: "MEDIUM", channel: "MANUAL", queueName: null },
|
||||
mode: "onTouched",
|
||||
})
|
||||
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 [attachments, setAttachments] = useState<Array<{ storageId: string; name: string; size?: number; type?: string }>>([])
|
||||
|
|
@ -45,18 +49,26 @@ export function NewTicketDialog() {
|
|||
setLoading(true)
|
||||
toast.loading("Criando ticket…", { id: "new-ticket" })
|
||||
try {
|
||||
const sel = queues.find((q: any) => q.name === values.queueName)
|
||||
const sel = queues.find((q) => q.name === values.queueName)
|
||||
const id = await create({
|
||||
tenantId: DEFAULT_TENANT_ID,
|
||||
subject: values.subject,
|
||||
summary: values.summary,
|
||||
priority: values.priority,
|
||||
channel: values.channel,
|
||||
queueId: sel?.id,
|
||||
requesterId: userId as any,
|
||||
queueId: sel?.id as Id<"queues"> | undefined,
|
||||
requesterId: userId as Id<"users">,
|
||||
})
|
||||
if (attachments.length > 0 || (values.summary && values.summary.trim().length > 0)) {
|
||||
await addComment({ ticketId: id as any, authorId: userId as any, visibility: "PUBLIC", body: values.summary || "", attachments })
|
||||
const hasDescription = (values.description ?? "").replace(/<[^>]*>/g, "").trim().length > 0
|
||||
const bodyHtml = hasDescription ? (values.description as string) : (values.summary || "")
|
||||
if (attachments.length > 0 || bodyHtml.trim().length > 0) {
|
||||
const typedAttachments = attachments.map((a) => ({
|
||||
storageId: a.storageId as unknown as Id<"_storage">,
|
||||
name: a.name,
|
||||
size: a.size,
|
||||
type: a.type,
|
||||
}))
|
||||
await addComment({ ticketId: id as Id<"tickets">, authorId: userId as Id<"users">, visibility: "PUBLIC", body: bodyHtml, attachments: typedAttachments })
|
||||
}
|
||||
toast.success("Ticket criado!", { id: "new-ticket" })
|
||||
setOpen(false)
|
||||
|
|
@ -93,6 +105,14 @@ export function NewTicketDialog() {
|
|||
<FieldLabel htmlFor="summary">Resumo</FieldLabel>
|
||||
<textarea id="summary" className="w-full rounded-md border bg-background p-2 text-sm" rows={3} {...form.register("summary")} />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel>Descrição</FieldLabel>
|
||||
<RichTextEditor
|
||||
value={form.watch("description") || ""}
|
||||
onChange={(html) => form.setValue("description", html)}
|
||||
placeholder="Detalhe o problema, passos para reproduzir, links, etc."
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel>Anexos</FieldLabel>
|
||||
<Dropzone onUploaded={(files) => setAttachments((prev) => [...prev, ...files])} />
|
||||
|
|
@ -101,7 +121,7 @@ export function NewTicketDialog() {
|
|||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
<Field>
|
||||
<FieldLabel>Prioridade</FieldLabel>
|
||||
<Select value={form.watch("priority")} onValueChange={(v) => form.setValue("priority", v as any)}>
|
||||
<Select value={form.watch("priority")} onValueChange={(v) => form.setValue("priority", v as z.infer<typeof schema>["priority"])}>
|
||||
<SelectTrigger><SelectValue placeholder="Prioridade" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="LOW">Baixa</SelectItem>
|
||||
|
|
@ -113,7 +133,7 @@ export function NewTicketDialog() {
|
|||
</Field>
|
||||
<Field>
|
||||
<FieldLabel>Canal</FieldLabel>
|
||||
<Select value={form.watch("channel")} onValueChange={(v) => form.setValue("channel", v as any)}>
|
||||
<Select value={form.watch("channel")} onValueChange={(v) => form.setValue("channel", v as z.infer<typeof schema>["channel"])}>
|
||||
<SelectTrigger><SelectValue placeholder="Canal" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="EMAIL">E-mail</SelectItem>
|
||||
|
|
@ -135,7 +155,7 @@ export function NewTicketDialog() {
|
|||
<SelectTrigger><SelectValue placeholder="Sem fila" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={NONE}>Sem fila</SelectItem>
|
||||
{queues.map((q: any) => (
|
||||
{queues.map((q) => (
|
||||
<SelectItem key={q.id} value={q.name}>{q.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { IconArrowRight, IconPlayerPlayFilled } from "@tabler/icons-react"
|
||||
import { useMutation, useQuery } from "convex/react"
|
||||
|
|
@ -9,7 +10,9 @@ 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 type { TicketPlayContext } from "@/lib/schemas/ticket"
|
||||
import type { TicketPlayContext, TicketQueueSummary } from "@/lib/schemas/ticket"
|
||||
import type { Id } from "@/convex/_generated/dataModel"
|
||||
import { mapTicketFromServer } from "@/lib/mappers/ticket"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
|
|
@ -17,6 +20,7 @@ import { Separator } from "@/components/ui/separator"
|
|||
import { TicketPriorityPill } from "@/components/tickets/priority-pill"
|
||||
import { TicketStatusBadge } from "@/components/tickets/status-badge"
|
||||
import { Spinner } from "@/components/ui/spinner"
|
||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select"
|
||||
|
||||
interface PlayNextTicketCardProps {
|
||||
context?: TicketPlayContext
|
||||
|
|
@ -25,19 +29,21 @@ interface PlayNextTicketCardProps {
|
|||
export function PlayNextTicketCard({ context }: PlayNextTicketCardProps) {
|
||||
const router = useRouter()
|
||||
const { userId } = useAuth()
|
||||
const queueSummary = useQuery(api.queues.summary, { tenantId: DEFAULT_TENANT_ID }) ?? []
|
||||
const queueSummary = (useQuery(api.queues.summary, { tenantId: DEFAULT_TENANT_ID }) as TicketQueueSummary[] | undefined) ?? []
|
||||
const playNext = useMutation(api.tickets.playNext)
|
||||
const [selectedQueueId, setSelectedQueueId] = useState<string | undefined>(undefined)
|
||||
|
||||
const nextTicketFromServer = useQuery(api.tickets.list, {
|
||||
tenantId: DEFAULT_TENANT_ID,
|
||||
status: undefined,
|
||||
priority: undefined,
|
||||
channel: undefined,
|
||||
queueId: undefined,
|
||||
queueId: (selectedQueueId as Id<"queues">) || undefined,
|
||||
limit: 1,
|
||||
})?.[0]
|
||||
const nextTicketUi = nextTicketFromServer ? mapTicketFromServer(nextTicketFromServer as unknown) : null
|
||||
|
||||
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)
|
||||
const cardContext: TicketPlayContext | null = context ?? (nextTicketUi ? { queue: { id: "default", name: "Geral", pending: queueSummary.reduce((a, b) => a + b.pending, 0), waiting: queueSummary.reduce((a, b) => a + b.waiting, 0), breached: 0 }, nextTicket: nextTicketUi } : null)
|
||||
|
||||
if (!cardContext || !cardContext.nextTicket) {
|
||||
return (
|
||||
|
|
@ -63,6 +69,18 @@ export function PlayNextTicketCard({ context }: PlayNextTicketCardProps) {
|
|||
<TicketPriorityPill priority={ticket.priority} />
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<span className="text-sm text-muted-foreground">Fila:</span>
|
||||
<Select value={selectedQueueId ?? "ALL"} onValueChange={(v) => setSelectedQueueId(v === "ALL" ? undefined : v)}>
|
||||
<SelectTrigger className="h-8 w-[180px]"><SelectValue placeholder="Todas" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">Todas</SelectItem>
|
||||
{queueSummary.map((q) => (
|
||||
<SelectItem key={q.id} value={q.id}>{q.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-xl font-medium text-foreground">{ticket.subject}</h2>
|
||||
<p className="text-sm text-muted-foreground">{ticket.summary}</p>
|
||||
|
|
@ -91,7 +109,7 @@ export function PlayNextTicketCard({ context }: PlayNextTicketCardProps) {
|
|||
className="gap-2"
|
||||
onClick={async () => {
|
||||
if (!userId) return
|
||||
const chosen = await playNext({ tenantId: DEFAULT_TENANT_ID, queueId: undefined, agentId: userId as any })
|
||||
const chosen = await playNext({ tenantId: DEFAULT_TENANT_ID, queueId: (selectedQueueId as Id<"queues">) || undefined, agentId: userId as Id<"users"> })
|
||||
if (chosen?.id) router.push(`/tickets/${chosen.id}`)
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { DEFAULT_TENANT_ID } from "@/lib/constants";
|
|||
import { mapTicketsFromServerList } from "@/lib/mappers/ticket";
|
||||
import { TicketsTable } from "@/components/tickets/tickets-table";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import type { Ticket } from "@/lib/schemas/ticket";
|
||||
|
||||
export function RecentTicketsPanel() {
|
||||
const ticketsRaw = useQuery(api.tickets.list, { tenantId: DEFAULT_TENANT_ID, limit: 10 });
|
||||
|
|
@ -24,10 +25,10 @@ export function RecentTicketsPanel() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
const tickets = mapTicketsFromServerList(ticketsRaw as any[]);
|
||||
const tickets = mapTicketsFromServerList((ticketsRaw ?? []) as unknown[]);
|
||||
return (
|
||||
<div className="rounded-xl border bg-card">
|
||||
<TicketsTable tickets={tickets as any} />
|
||||
<TicketsTable tickets={tickets} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client"
|
||||
|
||||
import { ticketStatusSchema } from "@/lib/schemas/ticket"
|
||||
import { ticketStatusSchema, type TicketStatus } from "@/lib/schemas/ticket"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
|
||||
const statusConfig = {
|
||||
|
|
@ -10,11 +10,9 @@ const statusConfig = {
|
|||
ON_HOLD: { label: "Em espera", className: "bg-purple-100 text-purple-700 border-transparent" },
|
||||
RESOLVED: { label: "Resolvido", className: "bg-emerald-100 text-emerald-700 border-transparent" },
|
||||
CLOSED: { label: "Fechado", className: "bg-slate-100 text-slate-700 border-transparent" },
|
||||
} satisfies Record<(typeof ticketStatusSchema)["_type"], { label: string; className: string }>
|
||||
} satisfies Record<TicketStatus, { label: string; className: string }>
|
||||
|
||||
type TicketStatusBadgeProps = {
|
||||
status: (typeof ticketStatusSchema)["_type"]
|
||||
}
|
||||
type TicketStatusBadgeProps = { status: TicketStatus }
|
||||
|
||||
export function TicketStatusBadge({ status }: TicketStatusBadgeProps) {
|
||||
const config = statusConfig[status]
|
||||
|
|
|
|||
|
|
@ -4,12 +4,13 @@ import { useMemo, useState } from "react"
|
|||
import { formatDistanceToNow } from "date-fns"
|
||||
import { ptBR } from "date-fns/locale"
|
||||
import { IconLock, IconMessage } from "@tabler/icons-react"
|
||||
import { Download, ImageIcon, FileIcon } from "lucide-react"
|
||||
import { Download, FileIcon } from "lucide-react"
|
||||
import { useAction, useMutation } from "convex/react"
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
import { api } from "@/convex/_generated/api"
|
||||
import { useAuth } from "@/lib/auth-client"
|
||||
import type { Id } from "@/convex/_generated/dataModel"
|
||||
import type { TicketWithDetails } from "@/lib/schemas/ticket"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
|
|
@ -17,7 +18,9 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|||
import { Button } from "@/components/ui/button"
|
||||
import { toast } from "sonner"
|
||||
import { Dropzone } from "@/components/ui/dropzone"
|
||||
import { RichTextEditor, RichTextContent, sanitizeEditorHtml } from "@/components/ui/rich-text-editor"
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog"
|
||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select"
|
||||
|
||||
interface TicketCommentsProps {
|
||||
ticket: TicketWithDetails
|
||||
|
|
@ -31,6 +34,7 @@ export function TicketComments({ ticket }: TicketCommentsProps) {
|
|||
const [attachmentsToSend, setAttachmentsToSend] = useState<Array<{ storageId: string; name: string; size?: number; type?: string; previewUrl?: string }>>([])
|
||||
const [preview, setPreview] = useState<string | null>(null)
|
||||
const [pending, setPending] = useState<Pick<TicketWithDetails["comments"][number], "id"|"author"|"visibility"|"body"|"attachments"|"createdAt"|"updatedAt">[]>([])
|
||||
const [visibility, setVisibility] = useState<"PUBLIC" | "INTERNAL">("PUBLIC")
|
||||
|
||||
const commentsAll = useMemo(() => {
|
||||
return [...pending, ...ticket.comments]
|
||||
|
|
@ -43,19 +47,25 @@ export function TicketComments({ ticket }: TicketCommentsProps) {
|
|||
const now = new Date()
|
||||
const optimistic = {
|
||||
id: `temp-${now.getTime()}`,
|
||||
author: ticket.requester, // placeholder; poderia buscar o próprio usuário se necessário
|
||||
visibility: "PUBLIC" as const,
|
||||
body,
|
||||
attachments: attachments.map((a) => ({ id: a.storageId, name: a.name, url: a.previewUrl } as any)),
|
||||
author: ticket.requester,
|
||||
visibility,
|
||||
body: sanitizeEditorHtml(body),
|
||||
attachments: attachments.map((a) => ({ id: a.storageId, name: a.name, url: a.previewUrl })),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}
|
||||
setPending((p) => [optimistic, ...p])
|
||||
setBody("")
|
||||
setAttachmentsToSend([])
|
||||
toast.loading("Enviando comentário…", { id: "comment" })
|
||||
toast.loading("Enviando comentário.", { id: "comment" })
|
||||
try {
|
||||
await addComment({ ticketId: ticket.id as any, authorId: userId as any, visibility: "PUBLIC", body: optimistic.body, attachments })
|
||||
const typedAttachments = attachments.map((a) => ({
|
||||
storageId: a.storageId as unknown as Id<"_storage">,
|
||||
name: a.name,
|
||||
size: a.size,
|
||||
type: a.type,
|
||||
}))
|
||||
await addComment({ ticketId: ticket.id as Id<"tickets">, authorId: userId as Id<"users">, visibility, body: optimistic.body, attachments: typedAttachments })
|
||||
setPending([])
|
||||
toast.success("Comentário enviado!", { id: "comment" })
|
||||
} catch (err) {
|
||||
|
|
@ -74,7 +84,7 @@ export function TicketComments({ ticket }: TicketCommentsProps) {
|
|||
<CardContent className="space-y-6 px-4 pb-6">
|
||||
{commentsAll.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Ainda sem comentarios. Que tal registrar o proximo passo?
|
||||
Ainda sem comentários. Que tal registrar o próximo passo?
|
||||
</p>
|
||||
) : (
|
||||
commentsAll.map((comment) => {
|
||||
|
|
@ -101,20 +111,19 @@ export function TicketComments({ ticket }: TicketCommentsProps) {
|
|||
{formatDistanceToNow(comment.createdAt, { addSuffix: true, locale: ptBR })}
|
||||
</span>
|
||||
</div>
|
||||
<div className="rounded-lg border border-dashed bg-card px-3 py-2 text-sm leading-relaxed text-muted-foreground break-words whitespace-pre-wrap">
|
||||
{comment.body}
|
||||
<div className="rounded-lg border border-dashed bg-card px-3 py-2 text-sm leading-relaxed text-muted-foreground break-words">
|
||||
<RichTextContent html={comment.body} />
|
||||
</div>
|
||||
{comment.attachments?.length ? (
|
||||
<div className="grid max-w-xl grid-cols-[repeat(auto-fill,minmax(96px,1fr))] gap-3">
|
||||
{comment.attachments.map((a) => {
|
||||
const att = a as any
|
||||
{comment.attachments.map((att) => {
|
||||
const isImg = (att?.url ?? "").match(/\.(png|jpe?g|gif|webp|svg)$/i)
|
||||
if (isImg && att.url) {
|
||||
return (
|
||||
<button
|
||||
key={att.id}
|
||||
type="button"
|
||||
onClick={() => setPreview(att.url)}
|
||||
onClick={() => setPreview(att.url || null)}
|
||||
className="group overflow-hidden rounded-lg border bg-card p-0.5 hover:shadow"
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
|
|
@ -140,15 +149,19 @@ export function TicketComments({ ticket }: TicketCommentsProps) {
|
|||
})
|
||||
)}
|
||||
<form onSubmit={handleSubmit} className="mt-4 space-y-3">
|
||||
<textarea
|
||||
className="w-full rounded-md border bg-background p-3 text-sm"
|
||||
placeholder="Escreva um comentario..."
|
||||
rows={3}
|
||||
value={body}
|
||||
onChange={(e) => setBody(e.target.value)}
|
||||
/>
|
||||
<RichTextEditor value={body} onChange={setBody} placeholder="Escreva um comentário..." />
|
||||
<Dropzone onUploaded={(files) => setAttachmentsToSend((prev) => [...prev, ...files])} />
|
||||
<div className="flex items-center justify-end">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
Visibilidade:
|
||||
<Select value={visibility} onValueChange={(v) => setVisibility(v as "PUBLIC" | "INTERNAL")}>
|
||||
<SelectTrigger className="h-8 w-[140px]"><SelectValue placeholder="Visibilidade" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="PUBLIC">Pública</SelectItem>
|
||||
<SelectItem value="INTERNAL">Interna</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button type="submit" size="sm">Enviar</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -6,21 +6,23 @@ import { useQuery } from "convex/react";
|
|||
import { api } from "@/convex/_generated/api";
|
||||
import { DEFAULT_TENANT_ID } from "@/lib/constants";
|
||||
import { mapTicketWithDetailsFromServer } from "@/lib/mappers/ticket";
|
||||
import type { Id } from "@/convex/_generated/dataModel";
|
||||
import type { TicketWithDetails } from "@/lib/schemas/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 { TicketComments } from "@/components/tickets/ticket-comments.rich";
|
||||
import { TicketDetailsPanel } from "@/components/tickets/ticket-details-panel";
|
||||
import { TicketSummaryHeader } from "@/components/tickets/ticket-summary-header";
|
||||
import { TicketTimeline } from "@/components/tickets/ticket-timeline";
|
||||
|
||||
export function TicketDetailView({ id }: { id: string }) {
|
||||
const isMockId = id.startsWith("ticket-");
|
||||
const t = useQuery(api.tickets.getById, isMockId ? undefined : ({ tenantId: DEFAULT_TENANT_ID, id: id as any }));
|
||||
let ticket: any | null = null;
|
||||
const t = useQuery(api.tickets.getById, isMockId ? "skip" : ({ tenantId: DEFAULT_TENANT_ID, id: id as Id<"tickets"> }));
|
||||
let ticket: TicketWithDetails | null = null;
|
||||
if (t) {
|
||||
ticket = mapTicketWithDetailsFromServer(t as any);
|
||||
ticket = mapTicketWithDetailsFromServer(t as unknown);
|
||||
} else if (isMockId) {
|
||||
ticket = getTicketById(id) ?? null;
|
||||
}
|
||||
|
|
@ -54,13 +56,13 @@ export function TicketDetailView({ id }: { id: string }) {
|
|||
);
|
||||
return (
|
||||
<div className="flex flex-col gap-6 px-4 lg:px-6">
|
||||
<TicketSummaryHeader ticket={ticket as any} />
|
||||
<TicketSummaryHeader ticket={ticket} />
|
||||
<div className="grid gap-6 lg:grid-cols-[2fr_1fr]">
|
||||
<div className="space-y-6">
|
||||
<TicketComments ticket={ticket as any} />
|
||||
<TicketTimeline ticket={ticket as any} />
|
||||
<TicketComments ticket={ticket} />
|
||||
<TicketTimeline ticket={ticket} />
|
||||
</div>
|
||||
<TicketDetailsPanel ticket={ticket as any} />
|
||||
<TicketDetailsPanel ticket={ticket} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ 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 data: TicketQueueSummary[] = (queues ?? (fromServer as TicketQueueSummary[] | undefined) ?? [])
|
||||
if (!queues && fromServer === undefined) {
|
||||
return (
|
||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ import { toast } from "sonner"
|
|||
import { api } from "@/convex/_generated/api"
|
||||
|
||||
import { useAuth } from "@/lib/auth-client"
|
||||
import type { TicketWithDetails } from "@/lib/schemas/ticket"
|
||||
import type { Doc, Id } from "@/convex/_generated/dataModel"
|
||||
import type { TicketWithDetails, TicketQueueSummary, TicketStatus } from "@/lib/schemas/ticket"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { TicketPriorityPill } from "@/components/tickets/priority-pill"
|
||||
|
|
@ -27,9 +28,9 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
const updateStatus = useMutation(api.tickets.updateStatus)
|
||||
const changeAssignee = useMutation(api.tickets.changeAssignee)
|
||||
const changeQueue = useMutation(api.tickets.changeQueue)
|
||||
const agents = useQuery(api.users.listAgents, { tenantId: ticket.tenantId }) ?? []
|
||||
const queues = useQuery(api.queues.summary, { tenantId: ticket.tenantId }) ?? []
|
||||
const [status, setStatus] = useState(ticket.status)
|
||||
const agents = (useQuery(api.users.listAgents, { tenantId: ticket.tenantId }) as Doc<"users">[] | undefined) ?? []
|
||||
const queues = (useQuery(api.queues.summary, { tenantId: ticket.tenantId }) as TicketQueueSummary[] | undefined) ?? []
|
||||
const [status, setStatus] = useState<TicketStatus>(ticket.status)
|
||||
const statusPt: Record<string, string> = {
|
||||
NEW: "Novo",
|
||||
OPEN: "Aberto",
|
||||
|
|
@ -47,16 +48,16 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
#{ticket.reference}
|
||||
</Badge>
|
||||
<TicketPriorityPill priority={ticket.priority} />
|
||||
<TicketStatusBadge status={status as any} />
|
||||
<TicketStatusBadge status={status} />
|
||||
<Select
|
||||
value={status}
|
||||
onValueChange={async (value) => {
|
||||
const prev = status
|
||||
setStatus(value) // otimista
|
||||
setStatus(value as import("@/lib/schemas/ticket").TicketStatus) // otimista
|
||||
if (!userId) return
|
||||
toast.loading("Atualizando status…", { id: "status" })
|
||||
try {
|
||||
await updateStatus({ ticketId: ticket.id as any, status: value as any, actorId: userId as any })
|
||||
await updateStatus({ ticketId: ticket.id as Id<"tickets">, status: value, actorId: userId as Id<"users"> })
|
||||
toast.success(`Status alterado para ${statusPt[value]}.`, { id: "status" })
|
||||
} catch (e) {
|
||||
setStatus(prev)
|
||||
|
|
@ -95,7 +96,7 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
if (!userId) return
|
||||
toast.loading("Atribuindo responsável…", { id: "assignee" })
|
||||
try {
|
||||
await changeAssignee({ ticketId: ticket.id as any, assigneeId: value as any, actorId: userId as any })
|
||||
await changeAssignee({ ticketId: ticket.id as Id<"tickets">, assigneeId: value as Id<"users">, actorId: userId as Id<"users"> })
|
||||
toast.success("Responsável atualizado!", { id: "assignee" })
|
||||
} catch {
|
||||
toast.error("Não foi possível atribuir.", { id: "assignee" })
|
||||
|
|
@ -104,7 +105,7 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
>
|
||||
<SelectTrigger className="h-8 w-[180px]"><SelectValue placeholder="Selecionar" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{agents.map((a: any) => (
|
||||
{agents.map((a) => (
|
||||
<SelectItem key={a._id} value={a._id}>{a.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
|
@ -118,11 +119,11 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
value={ticket.queue ?? ""}
|
||||
onValueChange={async (value) => {
|
||||
if (!userId) return
|
||||
const q = queues.find((qq: any) => qq.name === value)
|
||||
const q = queues.find((qq) => qq.name === value)
|
||||
if (!q) return
|
||||
toast.loading("Atualizando fila…", { id: "queue" })
|
||||
try {
|
||||
await changeQueue({ ticketId: ticket.id as any, queueId: q.id as any, actorId: userId as any })
|
||||
await changeQueue({ ticketId: ticket.id as Id<"tickets">, queueId: q.id as Id<"queues">, actorId: userId as Id<"users"> })
|
||||
toast.success("Fila atualizada!", { id: "queue" })
|
||||
} catch {
|
||||
toast.error("Não foi possível atualizar a fila.", { id: "queue" })
|
||||
|
|
@ -131,7 +132,7 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
>
|
||||
<SelectTrigger className="h-8 w-[180px]"><SelectValue placeholder="Selecionar" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{queues.map((q: any) => (
|
||||
{queues.map((q) => (
|
||||
<SelectItem key={q.id} value={q.name}>{q.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
|
|
|||
|
|
@ -56,12 +56,12 @@ export function TicketTimeline({ ticket }: TicketTimelineProps) {
|
|||
{entry.payload?.actorName ? (
|
||||
<span className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Avatar className="size-5">
|
||||
<AvatarImage src={entry.payload.actorAvatar} alt={entry.payload.actorName} />
|
||||
<AvatarImage src={entry.payload?.actorAvatar as string | undefined} alt={String(entry.payload?.actorName ?? "")} />
|
||||
<AvatarFallback>
|
||||
{entry.payload.actorName.split(' ').slice(0,2).map((p:string)=>p[0]).join('').toUpperCase()}
|
||||
{String(entry.payload?.actorName ?? '').split(' ').slice(0,2).map((p:string)=>p[0]).join('').toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
por {entry.payload.actorName}
|
||||
por {String(entry.payload?.actorName ?? '')}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="text-xs text-muted-foreground">
|
||||
|
|
@ -69,7 +69,7 @@ export function TicketTimeline({ ticket }: TicketTimelineProps) {
|
|||
</span>
|
||||
</div>
|
||||
{(() => {
|
||||
const p: any = entry.payload || {}
|
||||
const p = (entry.payload || {}) as { toLabel?: string; to?: string; assigneeName?: string; assigneeId?: string; queueName?: string; queueId?: string; requesterName?: string; authorName?: string; authorId?: string }
|
||||
let message: string | null = null
|
||||
if (entry.type === "STATUS_CHANGED" && (p.toLabel || p.to)) message = `Status alterado para ${p.toLabel || p.to}`
|
||||
if (entry.type === "ASSIGNEE_CHANGED" && (p.assigneeName || p.assigneeId)) message = `Responsável alterado${p.assigneeName ? ` para ${p.assigneeName}` : ""}`
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { useQuery } from "convex/react"
|
|||
import { api } from "@/convex/_generated/api"
|
||||
import { DEFAULT_TENANT_ID } from "@/lib/constants"
|
||||
import { mapTicketsFromServerList } from "@/lib/mappers/ticket"
|
||||
import type { Ticket, TicketQueueSummary } from "@/lib/schemas/ticket"
|
||||
import { TicketsFilters, TicketFiltersState, defaultTicketFilters } from "@/components/tickets/tickets-filters"
|
||||
import { TicketsTable } from "@/components/tickets/tickets-table"
|
||||
import { Spinner } from "@/components/ui/spinner"
|
||||
|
|
@ -14,7 +15,7 @@ import { Spinner } from "@/components/ui/spinner"
|
|||
export function TicketsView() {
|
||||
const [filters, setFilters] = useState<TicketFiltersState>(defaultTicketFilters)
|
||||
|
||||
const queues = useQuery(api.queues.summary, { tenantId: DEFAULT_TENANT_ID })
|
||||
const queues = useQuery(api.queues.summary, { tenantId: DEFAULT_TENANT_ID }) as TicketQueueSummary[] | undefined
|
||||
const ticketsRaw = useQuery(api.tickets.list, {
|
||||
tenantId: DEFAULT_TENANT_ID,
|
||||
status: filters.status ?? undefined,
|
||||
|
|
@ -24,16 +25,16 @@ export function TicketsView() {
|
|||
search: filters.search || undefined,
|
||||
})
|
||||
|
||||
const tickets = useMemo(() => mapTicketsFromServerList((ticketsRaw ?? []) as any[]), [ticketsRaw])
|
||||
const tickets = useMemo(() => mapTicketsFromServerList((ticketsRaw ?? []) as unknown[]), [ticketsRaw])
|
||||
|
||||
const filteredTickets = useMemo(() => {
|
||||
if (!filters.queue) return tickets
|
||||
return tickets.filter((t: any) => t.queue === filters.queue)
|
||||
return tickets.filter((t: Ticket) => t.queue === filters.queue)
|
||||
}, [tickets, filters.queue])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 px-4 lg:px-6">
|
||||
<TicketsFilters onChange={setFilters} queues={(queues ?? []).map((q: any) => q.name)} />
|
||||
<TicketsFilters onChange={setFilters} queues={(queues ?? []).map((q) => q.name)} />
|
||||
{ticketsRaw === undefined ? (
|
||||
<div className="rounded-xl border bg-card p-4">
|
||||
<div className="grid gap-3">
|
||||
|
|
@ -46,7 +47,7 @@ export function TicketsView() {
|
|||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<TicketsTable tickets={filteredTickets as any} />
|
||||
<TicketsTable tickets={filteredTickets} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,26 +7,26 @@ const FieldSet = ({ className, ...props }: React.ComponentProps<"fieldset">) =>
|
|||
<fieldset role="group" className={cn("grid gap-3", className)} {...props} />
|
||||
)
|
||||
|
||||
const FieldLegend = ({ className, ...props }: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) => {
|
||||
const { variant = "legend", ...rest } = props as any
|
||||
return (
|
||||
const FieldLegend = (
|
||||
{ className, variant = "legend", ...props }: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }
|
||||
) => (
|
||||
<legend
|
||||
className={cn(
|
||||
variant === "label" ? "text-sm font-medium" : "text-sm font-semibold",
|
||||
"text-foreground", className
|
||||
"text-foreground",
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
const FieldGroup = ({ className, ...props }: React.ComponentProps<"div">) => (
|
||||
<div className={cn("@container/field-group grid gap-4", className)} {...props} />
|
||||
)
|
||||
|
||||
const Field = ({ className, ...props }: React.ComponentProps<"div"> & { orientation?: "vertical" | "horizontal" | "responsive" }) => {
|
||||
const { orientation = "vertical", ...rest } = props as any
|
||||
return (
|
||||
const Field = (
|
||||
{ className, orientation = "vertical", ...props }: React.ComponentProps<"div"> & { orientation?: "vertical" | "horizontal" | "responsive" }
|
||||
) => (
|
||||
<div
|
||||
data-orientation={orientation}
|
||||
className={cn(
|
||||
|
|
@ -36,10 +36,9 @@ const Field = ({ className, ...props }: React.ComponentProps<"div"> & { orientat
|
|||
orientation === "responsive" && "@[480px]/field-group:flex-row @[480px]/field-group:items-center flex-col",
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
const FieldContent = ({ className, ...props }: React.ComponentProps<"div">) => (
|
||||
<div className={cn("flex flex-col", className)} {...props} />
|
||||
|
|
@ -78,4 +77,3 @@ const FieldSeparator = ({ className, ...props }: React.ComponentProps<"div">) =>
|
|||
)
|
||||
|
||||
export { FieldSet, FieldLegend, FieldGroup, Field, FieldContent, FieldLabel, FieldTitle, FieldDescription, FieldError, FieldSeparator }
|
||||
|
||||
|
|
|
|||
218
web/src/components/ui/rich-text-editor.tsx
Normal file
218
web/src/components/ui/rich-text-editor.tsx
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
"use client"
|
||||
|
||||
import { useEffect } from "react"
|
||||
import { useEditor, EditorContent } from "@tiptap/react"
|
||||
import StarterKit from "@tiptap/starter-kit"
|
||||
import Link from "@tiptap/extension-link"
|
||||
import Placeholder from "@tiptap/extension-placeholder"
|
||||
import { cn } from "@/lib/utils"
|
||||
import sanitize from "sanitize-html"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import {
|
||||
Bold,
|
||||
Italic,
|
||||
Strikethrough,
|
||||
List,
|
||||
ListOrdered,
|
||||
Quote,
|
||||
Undo,
|
||||
Redo,
|
||||
Link as LinkIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
type RichTextEditorProps = {
|
||||
value?: string
|
||||
onChange?: (html: string) => void
|
||||
className?: string
|
||||
placeholder?: string
|
||||
disabled?: boolean
|
||||
minHeight?: number
|
||||
}
|
||||
|
||||
export function RichTextEditor({
|
||||
value,
|
||||
onChange,
|
||||
className,
|
||||
placeholder = "Escreva aqui...",
|
||||
disabled,
|
||||
minHeight = 120,
|
||||
}: RichTextEditorProps) {
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
bulletList: { keepMarks: true },
|
||||
orderedList: { keepMarks: true },
|
||||
}),
|
||||
Link.configure({
|
||||
openOnClick: true,
|
||||
autolink: true,
|
||||
protocols: ["http", "https", "mailto"],
|
||||
HTMLAttributes: { rel: "noopener noreferrer", target: "_blank" },
|
||||
}),
|
||||
Placeholder.configure({ placeholder }),
|
||||
],
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class:
|
||||
"prose prose-sm max-w-none focus:outline-none text-foreground",
|
||||
},
|
||||
},
|
||||
content: value || "",
|
||||
onUpdate({ editor }) {
|
||||
onChange?.(editor.getHTML())
|
||||
},
|
||||
editable: !disabled,
|
||||
// Avoid SSR hydration mismatches per Tiptap recommendation
|
||||
immediatelyRender: false,
|
||||
})
|
||||
|
||||
// Keep external value in sync when it changes
|
||||
useEffect(() => {
|
||||
if (!editor) return
|
||||
const current = editor.getHTML()
|
||||
if ((value ?? "") !== current) {
|
||||
editor.commands.setContent(value || "", { emitUpdate: false })
|
||||
}
|
||||
}, [value, editor])
|
||||
|
||||
if (!editor) return null
|
||||
|
||||
return (
|
||||
<div className={cn("rounded-md border bg-background", className)}>
|
||||
<div className="flex flex-wrap items-center gap-1 border-b px-2 py-1">
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||
active={editor.isActive("bold")}
|
||||
ariaLabel="Negrito"
|
||||
>
|
||||
<Bold className="size-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||
active={editor.isActive("italic")}
|
||||
ariaLabel="Itálico"
|
||||
>
|
||||
<Italic className="size-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||
active={editor.isActive("strike")}
|
||||
ariaLabel="Tachado"
|
||||
>
|
||||
<Strikethrough className="size-4" />
|
||||
</ToolbarButton>
|
||||
<Separator orientation="vertical" className="mx-1 h-5" />
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
||||
active={editor.isActive("bulletList")}
|
||||
ariaLabel="Lista"
|
||||
>
|
||||
<List className="size-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||
active={editor.isActive("orderedList")}
|
||||
ariaLabel="Lista ordenada"
|
||||
>
|
||||
<ListOrdered className="size-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
||||
active={editor.isActive("blockquote")}
|
||||
ariaLabel="Citação"
|
||||
>
|
||||
<Quote className="size-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => {
|
||||
const prev = editor.getAttributes("link").href as string | undefined
|
||||
const url = window.prompt("URL do link:", prev || "https://")
|
||||
if (url === null) return
|
||||
if (url === "") {
|
||||
editor.chain().focus().extendMarkRange("link").unsetLink().run()
|
||||
return
|
||||
}
|
||||
editor.chain().focus().extendMarkRange("link").setLink({ href: url }).run()
|
||||
}}
|
||||
active={editor.isActive("link")}
|
||||
ariaLabel="Inserir link"
|
||||
>
|
||||
<LinkIcon className="size-4" />
|
||||
</ToolbarButton>
|
||||
<div className="ms-auto flex items-center gap-1">
|
||||
<ToolbarButton onClick={() => editor.chain().focus().undo().run()} ariaLabel="Desfazer">
|
||||
<Undo className="size-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton onClick={() => editor.chain().focus().redo().run()} ariaLabel="Refazer">
|
||||
<Redo className="size-4" />
|
||||
</ToolbarButton>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ minHeight }} className="rich-text p-3">
|
||||
<EditorContent editor={editor} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ToolbarButton({
|
||||
onClick,
|
||||
active,
|
||||
ariaLabel,
|
||||
children,
|
||||
}: {
|
||||
onClick: () => void
|
||||
active?: boolean
|
||||
ariaLabel?: string
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant={active ? "default" : "ghost"}
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={onClick}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
// Utilitário simples para renderização segura do HTML do editor.
|
||||
// Remove tags <script>/<style> e atributos on*.
|
||||
export function sanitizeEditorHtml(html: string): string {
|
||||
try {
|
||||
return sanitize(html || "", {
|
||||
allowedTags: [
|
||||
"p","br","a","strong","em","u","s","blockquote","ul","ol","li","code","pre","span","h1","h2","h3"
|
||||
],
|
||||
allowedAttributes: {
|
||||
a: ["href","name","target","rel"],
|
||||
span: ["style"],
|
||||
code: ["class"],
|
||||
pre: ["class"],
|
||||
},
|
||||
allowedSchemes: ["http","https","mailto"],
|
||||
// prevent target=_self phishing
|
||||
transformTags: {
|
||||
a: sanitize.simpleTransform("a", { rel: "noopener noreferrer", target: "_blank" }, true),
|
||||
},
|
||||
// disallow inline event handlers
|
||||
allowVulnerableTags: false,
|
||||
})
|
||||
} catch {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
export function RichTextContent({ html, className }: { html: string; className?: string }) {
|
||||
return (
|
||||
<div
|
||||
className={cn("rich-text text-sm leading-relaxed", className)}
|
||||
dangerouslySetInnerHTML={{ __html: sanitizeEditorHtml(html) }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import React, { createContext, useContext, useEffect, useMemo, useState } from "react";
|
||||
import type { Doc } from "@/convex/_generated/dataModel";
|
||||
import { useMutation } from "convex/react";
|
||||
|
||||
// Lazy import to avoid build errors before convex is generated
|
||||
|
|
@ -32,10 +33,8 @@ export function AuthProvider({ demoUser, tenantId, children }: { demoUser: DemoU
|
|||
if (!process.env.NEXT_PUBLIC_CONVEX_URL) return; // allow dev without backend
|
||||
if (!localDemoUser) return;
|
||||
try {
|
||||
const user = await ensureUser({ tenantId, name: localDemoUser.name, email: localDemoUser.email, avatarUrl: localDemoUser.avatarUrl });
|
||||
// Convex returns a full document
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
setUserId((user as any)?._id ?? null);
|
||||
const user = (await ensureUser({ tenantId, name: localDemoUser.name, email: localDemoUser.email, avatarUrl: localDemoUser.avatarUrl })) as Doc<"users"> | null;
|
||||
setUserId(user?._id ?? null);
|
||||
} catch (e) {
|
||||
console.error("Failed to ensure user:", e);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ const serverEventSchema = z.object({
|
|||
|
||||
const serverTicketWithDetailsSchema = serverTicketSchema.extend({
|
||||
description: z.string().optional().nullable(),
|
||||
customFields: z.record(z.any()).default({}).optional(),
|
||||
customFields: z.record(z.string(), z.any()).optional(),
|
||||
timeline: z.array(serverEventSchema),
|
||||
comments: z.array(serverCommentSchema),
|
||||
});
|
||||
|
|
@ -76,7 +76,6 @@ export function mapTicketFromServer(input: unknown) {
|
|||
firstResponseAt: s.firstResponseAt ? new Date(s.firstResponseAt) : null,
|
||||
resolvedAt: s.resolvedAt ? new Date(s.resolvedAt) : null,
|
||||
};
|
||||
// Já validamos o formato recebido (serverTicketSchema). Retornamos no shape da UI.
|
||||
return ui as unknown as z.infer<typeof ticketSchema>;
|
||||
}
|
||||
|
||||
|
|
@ -88,6 +87,7 @@ export function mapTicketWithDetailsFromServer(input: unknown) {
|
|||
const s = serverTicketWithDetailsSchema.parse(input);
|
||||
const ui = {
|
||||
...s,
|
||||
customFields: (s.customFields ?? {}) as Record<string, unknown>,
|
||||
lastTimelineEntry: s.lastTimelineEntry ?? undefined,
|
||||
updatedAt: new Date(s.updatedAt),
|
||||
createdAt: new Date(s.createdAt),
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export type TicketComment = z.infer<typeof ticketCommentSchema>
|
|||
export const ticketEventSchema = z.object({
|
||||
id: z.string(),
|
||||
type: z.string(),
|
||||
payload: z.record(z.any()).optional(),
|
||||
payload: z.record(z.string(), z.any()).optional(),
|
||||
createdAt: z.coerce.date(),
|
||||
})
|
||||
export type TicketEvent = z.infer<typeof ticketEventSchema>
|
||||
|
|
@ -102,7 +102,7 @@ export type Ticket = z.infer<typeof ticketSchema>
|
|||
|
||||
export const ticketWithDetailsSchema = ticketSchema.extend({
|
||||
description: z.string().optional(),
|
||||
customFields: z.record(z.any()).default({}),
|
||||
customFields: z.record(z.string(), z.any()).optional(),
|
||||
timeline: z.array(ticketEventSchema),
|
||||
comments: z.array(ticketCommentSchema),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@
|
|||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
"@/*": ["./src/*"],
|
||||
"@/convex/*": ["./convex/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue