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(),
|
reference: v.number(),
|
||||||
subject: v.string(),
|
subject: v.string(),
|
||||||
summary: v.optional(v.string()),
|
summary: v.optional(v.string()),
|
||||||
|
description: v.optional(v.string()),
|
||||||
status: v.string(),
|
status: v.string(),
|
||||||
priority: v.string(),
|
priority: v.string(),
|
||||||
channel: v.string(),
|
channel: v.string(),
|
||||||
|
|
@ -59,7 +60,8 @@ export default defineSchema({
|
||||||
.index("by_tenant_status", ["tenantId", "status"])
|
.index("by_tenant_status", ["tenantId", "status"])
|
||||||
.index("by_tenant_queue", ["tenantId", "queueId"])
|
.index("by_tenant_queue", ["tenantId", "queueId"])
|
||||||
.index("by_tenant_assignee", ["tenantId", "assigneeId"])
|
.index("by_tenant_assignee", ["tenantId", "assigneeId"])
|
||||||
.index("by_tenant_reference", ["tenantId", "reference"]),
|
.index("by_tenant_reference", ["tenantId", "reference"])
|
||||||
|
.index("by_tenant", ["tenantId"]),
|
||||||
|
|
||||||
ticketComments: defineTable({
|
ticketComments: defineTable({
|
||||||
ticketId: v.id("tickets"),
|
ticketId: v.id("tickets"),
|
||||||
|
|
@ -87,4 +89,3 @@ export default defineSchema({
|
||||||
createdAt: v.number(),
|
createdAt: v.number(),
|
||||||
}).index("by_ticket", ["ticketId"]),
|
}).index("by_ticket", ["ticketId"]),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { internalMutation, mutation, query } from "./_generated/server";
|
import { internalMutation, mutation, query } from "./_generated/server";
|
||||||
import { v } from "convex/values";
|
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({
|
export const list = query({
|
||||||
args: {
|
args: {
|
||||||
|
|
@ -15,16 +15,28 @@ export const list = query({
|
||||||
limit: v.optional(v.number()),
|
limit: v.optional(v.number()),
|
||||||
},
|
},
|
||||||
handler: async (ctx, args) => {
|
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")
|
.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.priority) filtered = filtered.filter((t) => t.priority === args.priority);
|
||||||
if (args.channel) filtered = filtered.filter((t) => t.channel === args.channel);
|
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) {
|
if (args.search) {
|
||||||
const term = args.search.toLowerCase();
|
const term = args.search.toLowerCase();
|
||||||
filtered = filtered.filter(
|
filtered = filtered.filter(
|
||||||
|
|
@ -38,9 +50,9 @@ export const list = query({
|
||||||
// hydrate requester and assignee
|
// hydrate requester and assignee
|
||||||
const result = await Promise.all(
|
const result = await Promise.all(
|
||||||
limited.map(async (t) => {
|
limited.map(async (t) => {
|
||||||
const requester = await ctx.db.get(t.requesterId);
|
const requester = (await ctx.db.get(t.requesterId)) as Doc<"users"> | null;
|
||||||
const assignee = t.assigneeId ? await ctx.db.get(t.assigneeId) : 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) : null;
|
const queue = t.queueId ? ((await ctx.db.get(t.queueId)) as Doc<"queues"> | null) : null;
|
||||||
return {
|
return {
|
||||||
id: t._id,
|
id: t._id,
|
||||||
reference: t.reference,
|
reference: t.reference,
|
||||||
|
|
@ -80,7 +92,7 @@ export const list = query({
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
// sort by updatedAt desc
|
// 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 }) => {
|
handler: async (ctx, { tenantId, id }) => {
|
||||||
const t = await ctx.db.get(id);
|
const t = await ctx.db.get(id);
|
||||||
if (!t || t.tenantId !== tenantId) return null;
|
if (!t || t.tenantId !== tenantId) return null;
|
||||||
const requester = await ctx.db.get(t.requesterId);
|
const requester = (await ctx.db.get(t.requesterId)) as Doc<"users"> | null;
|
||||||
const assignee = t.assigneeId ? await ctx.db.get(t.assigneeId) : 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) : null;
|
const queue = t.queueId ? ((await ctx.db.get(t.queueId)) as Doc<"queues"> | null) : null;
|
||||||
const comments = await ctx.db
|
const comments = await ctx.db
|
||||||
.query("ticketComments")
|
.query("ticketComments")
|
||||||
.withIndex("by_ticket", (q) => q.eq("ticketId", id))
|
.withIndex("by_ticket", (q) => q.eq("ticketId", id))
|
||||||
|
|
@ -103,7 +115,7 @@ export const getById = query({
|
||||||
|
|
||||||
const commentsHydrated = await Promise.all(
|
const commentsHydrated = await Promise.all(
|
||||||
comments.map(async (c) => {
|
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(
|
const attachments = await Promise.all(
|
||||||
(c.attachments ?? []).map(async (att) => ({
|
(c.attachments ?? []).map(async (att) => ({
|
||||||
id: att.storageId,
|
id: att.storageId,
|
||||||
|
|
@ -296,7 +308,7 @@ export const changeAssignee = mutation({
|
||||||
handler: async (ctx, { ticketId, assigneeId, actorId }) => {
|
handler: async (ctx, { ticketId, assigneeId, actorId }) => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
await ctx.db.patch(ticketId, { assigneeId, updatedAt: 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", {
|
await ctx.db.insert("ticketEvents", {
|
||||||
ticketId,
|
ticketId,
|
||||||
type: "ASSIGNEE_CHANGED",
|
type: "ASSIGNEE_CHANGED",
|
||||||
|
|
@ -311,7 +323,7 @@ export const changeQueue = mutation({
|
||||||
handler: async (ctx, { ticketId, queueId, actorId }) => {
|
handler: async (ctx, { ticketId, queueId, actorId }) => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
await ctx.db.patch(ticketId, { queueId, updatedAt: 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", {
|
await ctx.db.insert("ticketEvents", {
|
||||||
ticketId,
|
ticketId,
|
||||||
type: "QUEUE_CHANGED",
|
type: "QUEUE_CHANGED",
|
||||||
|
|
@ -329,10 +341,18 @@ export const playNext = mutation({
|
||||||
},
|
},
|
||||||
handler: async (ctx, { tenantId, queueId, agentId }) => {
|
handler: async (ctx, { tenantId, queueId, agentId }) => {
|
||||||
// Find eligible tickets: not resolved/closed and not assigned
|
// 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")
|
.query("tickets")
|
||||||
.withIndex("by_tenant_queue", (q) => q.eq("tenantId", tenantId).eq("queueId", queueId ?? undefined as any))
|
.withIndex("by_tenant_queue", (q) => q.eq("tenantId", tenantId).eq("queueId", queueId))
|
||||||
.collect();
|
.collect()
|
||||||
|
} else {
|
||||||
|
candidates = await ctx.db
|
||||||
|
.query("tickets")
|
||||||
|
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
candidates = candidates.filter(
|
candidates = candidates.filter(
|
||||||
(t) => t.status !== "RESOLVED" && t.status !== "CLOSED" && !t.assigneeId
|
(t) => t.status !== "RESOLVED" && t.status !== "CLOSED" && !t.assigneeId
|
||||||
|
|
@ -341,17 +361,18 @@ export const playNext = mutation({
|
||||||
if (candidates.length === 0) return null;
|
if (candidates.length === 0) return null;
|
||||||
|
|
||||||
// prioritize by priority then createdAt
|
// prioritize by priority then createdAt
|
||||||
|
const rank: Record<string, number> = { URGENT: 0, HIGH: 1, MEDIUM: 2, LOW: 3 }
|
||||||
candidates.sort((a, b) => {
|
candidates.sort((a, b) => {
|
||||||
const pa = STATUS_ORDER.indexOf(a.priority as any);
|
const pa = rank[a.priority] ?? 999
|
||||||
const pb = STATUS_ORDER.indexOf(b.priority as any);
|
const pb = rank[b.priority] ?? 999
|
||||||
if (pa !== pb) return pa - pb;
|
if (pa !== pb) return pa - pb
|
||||||
return a.createdAt - b.createdAt;
|
return a.createdAt - b.createdAt
|
||||||
});
|
})
|
||||||
|
|
||||||
const chosen = candidates[0];
|
const chosen = candidates[0];
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
await ctx.db.patch(chosen._id, { assigneeId: agentId, status: chosen.status === "NEW" ? "OPEN" : chosen.status, updatedAt: 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", {
|
await ctx.db.insert("ticketEvents", {
|
||||||
ticketId: chosen._id,
|
ticketId: chosen._id,
|
||||||
type: "ASSIGNEE_CHANGED",
|
type: "ASSIGNEE_CHANGED",
|
||||||
|
|
@ -359,26 +380,19 @@ export const playNext = mutation({
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
});
|
});
|
||||||
|
|
||||||
return await getPublicById(ctx, chosen._id);
|
// hydrate minimal public ticket like in list
|
||||||
},
|
const requester = (await ctx.db.get(chosen.requesterId)) as Doc<"users"> | null
|
||||||
});
|
const assignee = chosen.assigneeId ? ((await ctx.db.get(chosen.assigneeId)) as Doc<"users"> | null) : null
|
||||||
|
const queue = chosen.queueId ? ((await ctx.db.get(chosen.queueId)) as Doc<"queues"> | null) : null
|
||||||
// internal helper to hydrate a ticket in the same shape as list/getById
|
|
||||||
const getPublicById = async (ctx: any, id: Id<"tickets">) => {
|
|
||||||
const t = await ctx.db.get(id);
|
|
||||||
if (!t) return null;
|
|
||||||
const requester = await ctx.db.get(t.requesterId);
|
|
||||||
const assignee = t.assigneeId ? await ctx.db.get(t.assigneeId) : null;
|
|
||||||
const queue = t.queueId ? await ctx.db.get(t.queueId) : null;
|
|
||||||
return {
|
return {
|
||||||
id: t._id,
|
id: chosen._id,
|
||||||
reference: t.reference,
|
reference: chosen.reference,
|
||||||
tenantId: t.tenantId,
|
tenantId: chosen.tenantId,
|
||||||
subject: t.subject,
|
subject: chosen.subject,
|
||||||
summary: t.summary,
|
summary: chosen.summary,
|
||||||
status: t.status,
|
status: chosen.status,
|
||||||
priority: t.priority,
|
priority: chosen.priority,
|
||||||
channel: t.channel,
|
channel: chosen.channel,
|
||||||
queue: queue?.name ?? null,
|
queue: queue?.name ?? null,
|
||||||
requester: requester && {
|
requester: requester && {
|
||||||
id: requester._id,
|
id: requester._id,
|
||||||
|
|
@ -397,13 +411,14 @@ const getPublicById = async (ctx: any, id: Id<"tickets">) => {
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
slaPolicy: null,
|
slaPolicy: null,
|
||||||
dueAt: t.dueAt ?? null,
|
dueAt: chosen.dueAt ?? null,
|
||||||
firstResponseAt: t.firstResponseAt ?? null,
|
firstResponseAt: chosen.firstResponseAt ?? null,
|
||||||
resolvedAt: t.resolvedAt ?? null,
|
resolvedAt: chosen.resolvedAt ?? null,
|
||||||
updatedAt: t.updatedAt,
|
updatedAt: chosen.updatedAt,
|
||||||
createdAt: t.createdAt,
|
createdAt: chosen.createdAt,
|
||||||
tags: t.tags ?? [],
|
tags: chosen.tags ?? [],
|
||||||
lastTimelineEntry: null,
|
lastTimelineEntry: null,
|
||||||
metrics: null,
|
metrics: null,
|
||||||
};
|
}
|
||||||
};
|
},
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,13 @@ const eslintConfig = [
|
||||||
"next-env.d.ts",
|
"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;
|
export default eslintConfig;
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,10 @@
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@tabler/icons-react": "^3.35.0",
|
"@tabler/icons-react": "^3.35.0",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@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",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"convex": "^1.27.3",
|
"convex": "^1.27.3",
|
||||||
|
|
@ -44,6 +48,7 @@
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-hook-form": "^7.64.0",
|
"react-hook-form": "^7.64.0",
|
||||||
"recharts": "^2.15.4",
|
"recharts": "^2.15.4",
|
||||||
|
"sanitize-html": "^2.17.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
|
|
@ -55,6 +60,7 @@
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
"@types/sanitize-html": "^2.16.0",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.5.3",
|
"eslint-config-next": "15.5.3",
|
||||||
"prisma": "^6.16.2",
|
"prisma": "^6.16.2",
|
||||||
|
|
|
||||||
686
web/pnpm-lock.yaml
generated
686
web/pnpm-lock.yaml
generated
|
|
@ -71,6 +71,18 @@ importers:
|
||||||
'@tanstack/react-table':
|
'@tanstack/react-table':
|
||||||
specifier: ^8.21.3
|
specifier: ^8.21.3
|
||||||
version: 8.21.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
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:
|
class-variance-authority:
|
||||||
specifier: ^0.7.1
|
specifier: ^0.7.1
|
||||||
version: 0.7.1
|
version: 0.7.1
|
||||||
|
|
@ -104,6 +116,9 @@ importers:
|
||||||
recharts:
|
recharts:
|
||||||
specifier: ^2.15.4
|
specifier: ^2.15.4
|
||||||
version: 2.15.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
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:
|
sonner:
|
||||||
specifier: ^2.0.7
|
specifier: ^2.0.7
|
||||||
version: 2.0.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
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':
|
'@types/react-dom':
|
||||||
specifier: ^19
|
specifier: ^19
|
||||||
version: 19.2.0(@types/react@19.2.0)
|
version: 19.2.0(@types/react@19.2.0)
|
||||||
|
'@types/sanitize-html':
|
||||||
|
specifier: ^2.16.0
|
||||||
|
version: 2.16.0
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^9
|
specifier: ^9
|
||||||
version: 9.37.0(jiti@2.6.1)
|
version: 9.37.0(jiti@2.6.1)
|
||||||
|
|
@ -1255,6 +1273,9 @@ packages:
|
||||||
'@radix-ui/rect@1.1.1':
|
'@radix-ui/rect@1.1.1':
|
||||||
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
|
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':
|
'@rollup/rollup-android-arm-eabi@4.52.4':
|
||||||
resolution: {integrity: sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==}
|
resolution: {integrity: sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
|
|
@ -1484,6 +1505,160 @@ packages:
|
||||||
resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==}
|
resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==}
|
||||||
engines: {node: '>=12'}
|
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':
|
'@tybys/wasm-util@0.10.1':
|
||||||
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
|
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
|
||||||
|
|
||||||
|
|
@ -1523,6 +1698,15 @@ packages:
|
||||||
'@types/json5@0.0.29':
|
'@types/json5@0.0.29':
|
||||||
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
|
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':
|
'@types/node@20.19.19':
|
||||||
resolution: {integrity: sha512-pb1Uqj5WJP7wrcbLU7Ru4QtA0+3kAXrkutGiD26wUKzSMgNNaPARTUDQmElUXp64kh3cWdou3Q0C7qwwxqSFmg==}
|
resolution: {integrity: sha512-pb1Uqj5WJP7wrcbLU7Ru4QtA0+3kAXrkutGiD26wUKzSMgNNaPARTUDQmElUXp64kh3cWdou3Q0C7qwwxqSFmg==}
|
||||||
|
|
||||||
|
|
@ -1534,6 +1718,12 @@ packages:
|
||||||
'@types/react@19.2.0':
|
'@types/react@19.2.0':
|
||||||
resolution: {integrity: sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA==}
|
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':
|
'@typescript-eslint/eslint-plugin@8.45.0':
|
||||||
resolution: {integrity: sha512-HC3y9CVuevvWCl/oyZuI47dOeDF9ztdMEfMH8/DW/Mhwa9cCLnK1oD7JoTVGW/u7kFzNZUKUoyJEqkaJh5y3Wg==}
|
resolution: {integrity: sha512-HC3y9CVuevvWCl/oyZuI47dOeDF9ztdMEfMH8/DW/Mhwa9cCLnK1oD7JoTVGW/u7kFzNZUKUoyJEqkaJh5y3Wg==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
@ -1910,6 +2100,9 @@ packages:
|
||||||
react:
|
react:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
crelt@1.0.6:
|
||||||
|
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
@ -2010,6 +2203,10 @@ packages:
|
||||||
resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==}
|
resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==}
|
||||||
engines: {node: '>=16.0.0'}
|
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:
|
define-data-property@1.1.4:
|
||||||
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
|
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
@ -2038,6 +2235,19 @@ packages:
|
||||||
dom-helpers@5.2.1:
|
dom-helpers@5.2.1:
|
||||||
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
|
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:
|
dotenv@16.6.1:
|
||||||
resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
|
resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
@ -2060,6 +2270,10 @@ packages:
|
||||||
resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==}
|
resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
|
|
||||||
|
entities@4.5.0:
|
||||||
|
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||||
|
engines: {node: '>=0.12'}
|
||||||
|
|
||||||
es-abstract@1.24.0:
|
es-abstract@1.24.0:
|
||||||
resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==}
|
resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
@ -2393,6 +2607,9 @@ packages:
|
||||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
htmlparser2@8.0.2:
|
||||||
|
resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
|
||||||
|
|
||||||
ignore@5.3.2:
|
ignore@5.3.2:
|
||||||
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
|
|
@ -2484,6 +2701,10 @@ packages:
|
||||||
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
||||||
engines: {node: '>=0.12.0'}
|
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:
|
is-regex@1.2.1:
|
||||||
resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
|
resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
@ -2640,6 +2861,12 @@ packages:
|
||||||
resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==}
|
resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==}
|
||||||
engines: {node: '>= 12.0.0'}
|
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:
|
locate-path@6.0.0:
|
||||||
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
@ -2665,10 +2892,17 @@ packages:
|
||||||
magic-string@0.30.19:
|
magic-string@0.30.19:
|
||||||
resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==}
|
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:
|
math-intrinsics@1.1.0:
|
||||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
mdurl@2.0.0:
|
||||||
|
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
|
||||||
|
|
||||||
merge2@1.4.1:
|
merge2@1.4.1:
|
||||||
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
@ -2785,6 +3019,9 @@ packages:
|
||||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
|
orderedmap@2.1.1:
|
||||||
|
resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==}
|
||||||
|
|
||||||
own-keys@1.0.1:
|
own-keys@1.0.1:
|
||||||
resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
|
resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
@ -2801,6 +3038,9 @@ packages:
|
||||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
parse-srcset@1.0.2:
|
||||||
|
resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==}
|
||||||
|
|
||||||
path-exists@4.0.0:
|
path-exists@4.0.0:
|
||||||
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
@ -2873,6 +3113,68 @@ packages:
|
||||||
prop-types@15.8.1:
|
prop-types@15.8.1:
|
||||||
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
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:
|
punycode@2.3.1:
|
||||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
@ -2996,6 +3298,9 @@ packages:
|
||||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
rope-sequence@1.3.4:
|
||||||
|
resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==}
|
||||||
|
|
||||||
run-parallel@1.2.0:
|
run-parallel@1.2.0:
|
||||||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
||||||
|
|
||||||
|
|
@ -3011,6 +3316,9 @@ packages:
|
||||||
resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
|
resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
sanitize-html@2.17.0:
|
||||||
|
resolution: {integrity: sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA==}
|
||||||
|
|
||||||
scheduler@0.26.0:
|
scheduler@0.26.0:
|
||||||
resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
|
resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
|
||||||
|
|
||||||
|
|
@ -3227,6 +3535,9 @@ packages:
|
||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
uc.micro@2.1.0:
|
||||||
|
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
|
||||||
|
|
||||||
unbox-primitive@1.1.0:
|
unbox-primitive@1.1.0:
|
||||||
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
|
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
@ -3335,6 +3646,9 @@ packages:
|
||||||
jsdom:
|
jsdom:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
w3c-keyname@2.2.8:
|
||||||
|
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
|
||||||
|
|
||||||
which-boxed-primitive@1.1.1:
|
which-boxed-primitive@1.1.1:
|
||||||
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
|
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
@ -4297,6 +4611,8 @@ snapshots:
|
||||||
|
|
||||||
'@radix-ui/rect@1.1.1': {}
|
'@radix-ui/rect@1.1.1': {}
|
||||||
|
|
||||||
|
'@remirror/core-constants@3.0.0': {}
|
||||||
|
|
||||||
'@rollup/rollup-android-arm-eabi@4.52.4':
|
'@rollup/rollup-android-arm-eabi@4.52.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
|
@ -4460,6 +4776,187 @@ snapshots:
|
||||||
|
|
||||||
'@tanstack/table-core@8.21.3': {}
|
'@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':
|
'@tybys/wasm-util@0.10.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
@ -4495,6 +4992,15 @@ snapshots:
|
||||||
|
|
||||||
'@types/json5@0.0.29': {}
|
'@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':
|
'@types/node@20.19.19':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.21.0
|
undici-types: 6.21.0
|
||||||
|
|
@ -4507,6 +5013,12 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
csstype: 3.1.3
|
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)':
|
'@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:
|
dependencies:
|
||||||
'@eslint-community/regexpp': 4.12.1
|
'@eslint-community/regexpp': 4.12.1
|
||||||
|
|
@ -4911,6 +5423,8 @@ snapshots:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
|
|
||||||
|
crelt@1.0.6: {}
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-key: 3.1.1
|
path-key: 3.1.1
|
||||||
|
|
@ -4995,6 +5509,8 @@ snapshots:
|
||||||
|
|
||||||
deepmerge-ts@7.1.5: {}
|
deepmerge-ts@7.1.5: {}
|
||||||
|
|
||||||
|
deepmerge@4.3.1: {}
|
||||||
|
|
||||||
define-data-property@1.1.4:
|
define-data-property@1.1.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
es-define-property: 1.0.1
|
es-define-property: 1.0.1
|
||||||
|
|
@ -5024,6 +5540,24 @@ snapshots:
|
||||||
'@babel/runtime': 7.28.4
|
'@babel/runtime': 7.28.4
|
||||||
csstype: 3.1.3
|
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: {}
|
dotenv@16.6.1: {}
|
||||||
|
|
||||||
dunder-proto@1.0.1:
|
dunder-proto@1.0.1:
|
||||||
|
|
@ -5046,6 +5580,8 @@ snapshots:
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
tapable: 2.3.0
|
tapable: 2.3.0
|
||||||
|
|
||||||
|
entities@4.5.0: {}
|
||||||
|
|
||||||
es-abstract@1.24.0:
|
es-abstract@1.24.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
array-buffer-byte-length: 1.0.2
|
array-buffer-byte-length: 1.0.2
|
||||||
|
|
@ -5572,6 +6108,13 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
function-bind: 1.1.2
|
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@5.3.2: {}
|
||||||
|
|
||||||
ignore@7.0.5: {}
|
ignore@7.0.5: {}
|
||||||
|
|
@ -5664,6 +6207,8 @@ snapshots:
|
||||||
|
|
||||||
is-number@7.0.0: {}
|
is-number@7.0.0: {}
|
||||||
|
|
||||||
|
is-plain-object@5.0.0: {}
|
||||||
|
|
||||||
is-regex@1.2.1:
|
is-regex@1.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bound: 1.0.4
|
call-bound: 1.0.4
|
||||||
|
|
@ -5803,6 +6348,12 @@ snapshots:
|
||||||
lightningcss-win32-arm64-msvc: 1.30.1
|
lightningcss-win32-arm64-msvc: 1.30.1
|
||||||
lightningcss-win32-x64-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:
|
locate-path@6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
p-locate: 5.0.0
|
p-locate: 5.0.0
|
||||||
|
|
@ -5825,8 +6376,19 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@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: {}
|
math-intrinsics@1.1.0: {}
|
||||||
|
|
||||||
|
mdurl@2.0.0: {}
|
||||||
|
|
||||||
merge2@1.4.1: {}
|
merge2@1.4.1: {}
|
||||||
|
|
||||||
micromatch@4.0.8:
|
micromatch@4.0.8:
|
||||||
|
|
@ -5949,6 +6511,8 @@ snapshots:
|
||||||
type-check: 0.4.0
|
type-check: 0.4.0
|
||||||
word-wrap: 1.2.5
|
word-wrap: 1.2.5
|
||||||
|
|
||||||
|
orderedmap@2.1.1: {}
|
||||||
|
|
||||||
own-keys@1.0.1:
|
own-keys@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
get-intrinsic: 1.3.0
|
get-intrinsic: 1.3.0
|
||||||
|
|
@ -5967,6 +6531,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
callsites: 3.1.0
|
callsites: 3.1.0
|
||||||
|
|
||||||
|
parse-srcset@1.0.2: {}
|
||||||
|
|
||||||
path-exists@4.0.0: {}
|
path-exists@4.0.0: {}
|
||||||
|
|
||||||
path-key@3.1.1: {}
|
path-key@3.1.1: {}
|
||||||
|
|
@ -6026,6 +6592,111 @@ snapshots:
|
||||||
object-assign: 4.1.1
|
object-assign: 4.1.1
|
||||||
react-is: 16.13.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: {}
|
punycode@2.3.1: {}
|
||||||
|
|
||||||
pure-rand@6.1.0: {}
|
pure-rand@6.1.0: {}
|
||||||
|
|
@ -6181,6 +6852,8 @@ snapshots:
|
||||||
'@rollup/rollup-win32-x64-msvc': 4.52.4
|
'@rollup/rollup-win32-x64-msvc': 4.52.4
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|
||||||
|
rope-sequence@1.3.4: {}
|
||||||
|
|
||||||
run-parallel@1.2.0:
|
run-parallel@1.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
queue-microtask: 1.2.3
|
queue-microtask: 1.2.3
|
||||||
|
|
@ -6204,6 +6877,15 @@ snapshots:
|
||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
is-regex: 1.2.1
|
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: {}
|
scheduler@0.26.0: {}
|
||||||
|
|
||||||
semver@6.3.1: {}
|
semver@6.3.1: {}
|
||||||
|
|
@ -6472,6 +7154,8 @@ snapshots:
|
||||||
|
|
||||||
typescript@5.9.3: {}
|
typescript@5.9.3: {}
|
||||||
|
|
||||||
|
uc.micro@2.1.0: {}
|
||||||
|
|
||||||
unbox-primitive@1.1.0:
|
unbox-primitive@1.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bound: 1.0.4
|
call-bound: 1.0.4
|
||||||
|
|
@ -6617,6 +7301,8 @@ snapshots:
|
||||||
- supports-color
|
- supports-color
|
||||||
- terser
|
- terser
|
||||||
|
|
||||||
|
w3c-keyname@2.2.8: {}
|
||||||
|
|
||||||
which-boxed-primitive@1.1.1:
|
which-boxed-primitive@1.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-bigint: 1.1.0
|
is-bigint: 1.1.0
|
||||||
|
|
|
||||||
|
|
@ -120,3 +120,21 @@
|
||||||
@apply bg-background text-foreground font-sans antialiased;
|
@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 { TicketDetailView } from "@/components/tickets/ticket-detail-view"
|
||||||
import { TicketDetailStatic } from "@/components/tickets/ticket-detail-static"
|
import { TicketDetailStatic } from "@/components/tickets/ticket-detail-static"
|
||||||
import { getTicketById } from "@/lib/mocks/tickets"
|
import { getTicketById } from "@/lib/mocks/tickets"
|
||||||
|
import type { TicketWithDetails } from "@/lib/schemas/ticket"
|
||||||
|
|
||||||
type TicketDetailPageProps = {
|
type TicketDetailPageProps = {
|
||||||
params: Promise<{ id: string }>
|
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>
|
</AppShell>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useMemo, useState } from "react";
|
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 { useRouter } from "next/navigation";
|
||||||
import { useMutation, useQuery } from "convex/react";
|
import { useMutation, useQuery } from "convex/react";
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// 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 { api } from "@/convex/_generated/api";
|
||||||
import { DEFAULT_TENANT_ID } from "@/lib/constants";
|
import { DEFAULT_TENANT_ID } from "@/lib/constants";
|
||||||
import { useAuth } from "@/lib/auth-client";
|
import { useAuth } from "@/lib/auth-client";
|
||||||
|
import { RichTextEditor } from "@/components/ui/rich-text-editor";
|
||||||
|
|
||||||
export default function NewTicketPage() {
|
export default function NewTicketPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { userId } = useAuth();
|
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 create = useMutation(api.tickets.create);
|
||||||
|
const addComment = useMutation(api.tickets.addComment);
|
||||||
const ensureDefaults = useMutation(api.bootstrap.ensureDefaults);
|
const ensureDefaults = useMutation(api.bootstrap.ensureDefaults);
|
||||||
|
|
||||||
const [subject, setSubject] = useState("");
|
const [subject, setSubject] = useState("");
|
||||||
|
|
@ -21,25 +25,30 @@ export default function NewTicketPage() {
|
||||||
const [priority, setPriority] = useState("MEDIUM");
|
const [priority, setPriority] = useState("MEDIUM");
|
||||||
const [channel, setChannel] = useState("MANUAL");
|
const [channel, setChannel] = useState("MANUAL");
|
||||||
const [queueName, setQueueName] = useState<string | null>(null);
|
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) {
|
async function submit(e: React.FormEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
if (queues.length === 0) await ensureDefaults({ tenantId: DEFAULT_TENANT_ID });
|
if (queues.length === 0) await ensureDefaults({ tenantId: DEFAULT_TENANT_ID });
|
||||||
// Encontrar a fila pelo nome (simples)
|
// Encontrar a fila pelo nome (simples)
|
||||||
const selQueue = (queues as any[]).find((q: any) => q.name === queueName);
|
const selQueue = queues.find((q) => q.name === queueName);
|
||||||
const queueId = selQueue ? selQueue.id : undefined;
|
const queueId = selQueue ? (selQueue.id as Id<"queues">) : undefined;
|
||||||
const id = await create({
|
const id = await create({
|
||||||
tenantId: DEFAULT_TENANT_ID,
|
tenantId: DEFAULT_TENANT_ID,
|
||||||
subject,
|
subject,
|
||||||
summary,
|
summary,
|
||||||
priority,
|
priority,
|
||||||
channel,
|
channel,
|
||||||
queueId: queueId as any,
|
queueId,
|
||||||
requesterId: userId as any,
|
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}`);
|
router.replace(`/tickets/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -55,6 +64,10 @@ export default function NewTicketPage() {
|
||||||
<label className="text-sm">Resumo</label>
|
<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} />
|
<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>
|
||||||
|
<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="grid gap-4 sm:grid-cols-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm">Prioridade</label>
|
<label className="text-sm">Prioridade</label>
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||||
<SidebarHeader className="gap-3">
|
<SidebarHeader className="gap-3">
|
||||||
<VersionSwitcher
|
<VersionSwitcher
|
||||||
label="Release"
|
label="Release"
|
||||||
versions={navigation.versions}
|
versions={[...navigation.versions]}
|
||||||
defaultVersion={navigation.versions[0]}
|
defaultVersion={navigation.versions[0]}
|
||||||
/>
|
/>
|
||||||
<SearchForm placeholder="Buscar tickets, macros ou artigos" />
|
<SearchForm placeholder="Buscar tickets, macros ou artigos" />
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
|
import type { Id } from "@/convex/_generated/dataModel"
|
||||||
|
import type { TicketQueueSummary } from "@/lib/schemas/ticket"
|
||||||
import { useMutation, useQuery } from "convex/react"
|
import { useMutation, useQuery } from "convex/react"
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { api } from "@/convex/_generated/api"
|
import { api } from "@/convex/_generated/api"
|
||||||
|
|
@ -17,10 +19,12 @@ import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { Spinner } from "@/components/ui/spinner"
|
import { Spinner } from "@/components/ui/spinner"
|
||||||
import { Dropzone } from "@/components/ui/dropzone"
|
import { Dropzone } from "@/components/ui/dropzone"
|
||||||
|
import { RichTextEditor } from "@/components/ui/rich-text-editor"
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
subject: z.string().min(3, "Informe um assunto"),
|
subject: z.string().min(3, "Informe um assunto"),
|
||||||
summary: z.string().optional(),
|
summary: z.string().optional(),
|
||||||
|
description: z.string().optional(),
|
||||||
priority: z.enum(["LOW", "MEDIUM", "HIGH", "URGENT"]).default("MEDIUM"),
|
priority: z.enum(["LOW", "MEDIUM", "HIGH", "URGENT"]).default("MEDIUM"),
|
||||||
channel: z.enum(["EMAIL", "WHATSAPP", "CHAT", "PHONE", "API", "MANUAL"]).default("MANUAL"),
|
channel: z.enum(["EMAIL", "WHATSAPP", "CHAT", "PHONE", "API", "MANUAL"]).default("MANUAL"),
|
||||||
queueName: z.string().nullable().optional(),
|
queueName: z.string().nullable().optional(),
|
||||||
|
|
@ -31,11 +35,11 @@ export function NewTicketDialog() {
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const form = useForm<z.infer<typeof schema>>({
|
const form = useForm<z.infer<typeof schema>>({
|
||||||
resolver: zodResolver(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",
|
mode: "onTouched",
|
||||||
})
|
})
|
||||||
const { userId } = useAuth()
|
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 create = useMutation(api.tickets.create)
|
||||||
const addComment = useMutation(api.tickets.addComment)
|
const addComment = useMutation(api.tickets.addComment)
|
||||||
const [attachments, setAttachments] = useState<Array<{ storageId: string; name: string; size?: number; type?: string }>>([])
|
const [attachments, setAttachments] = useState<Array<{ storageId: string; name: string; size?: number; type?: string }>>([])
|
||||||
|
|
@ -45,18 +49,26 @@ export function NewTicketDialog() {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
toast.loading("Criando ticket…", { id: "new-ticket" })
|
toast.loading("Criando ticket…", { id: "new-ticket" })
|
||||||
try {
|
try {
|
||||||
const sel = queues.find((q: any) => q.name === values.queueName)
|
const sel = queues.find((q) => q.name === values.queueName)
|
||||||
const id = await create({
|
const id = await create({
|
||||||
tenantId: DEFAULT_TENANT_ID,
|
tenantId: DEFAULT_TENANT_ID,
|
||||||
subject: values.subject,
|
subject: values.subject,
|
||||||
summary: values.summary,
|
summary: values.summary,
|
||||||
priority: values.priority,
|
priority: values.priority,
|
||||||
channel: values.channel,
|
channel: values.channel,
|
||||||
queueId: sel?.id,
|
queueId: sel?.id as Id<"queues"> | undefined,
|
||||||
requesterId: userId as any,
|
requesterId: userId as Id<"users">,
|
||||||
})
|
})
|
||||||
if (attachments.length > 0 || (values.summary && values.summary.trim().length > 0)) {
|
const hasDescription = (values.description ?? "").replace(/<[^>]*>/g, "").trim().length > 0
|
||||||
await addComment({ ticketId: id as any, authorId: userId as any, visibility: "PUBLIC", body: values.summary || "", attachments })
|
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" })
|
toast.success("Ticket criado!", { id: "new-ticket" })
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
|
|
@ -93,6 +105,14 @@ export function NewTicketDialog() {
|
||||||
<FieldLabel htmlFor="summary">Resumo</FieldLabel>
|
<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")} />
|
<textarea id="summary" className="w-full rounded-md border bg-background p-2 text-sm" rows={3} {...form.register("summary")} />
|
||||||
</Field>
|
</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>
|
<Field>
|
||||||
<FieldLabel>Anexos</FieldLabel>
|
<FieldLabel>Anexos</FieldLabel>
|
||||||
<Dropzone onUploaded={(files) => setAttachments((prev) => [...prev, ...files])} />
|
<Dropzone onUploaded={(files) => setAttachments((prev) => [...prev, ...files])} />
|
||||||
|
|
@ -101,7 +121,7 @@ export function NewTicketDialog() {
|
||||||
<div className="grid gap-3 sm:grid-cols-3">
|
<div className="grid gap-3 sm:grid-cols-3">
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel>Prioridade</FieldLabel>
|
<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>
|
<SelectTrigger><SelectValue placeholder="Prioridade" /></SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="LOW">Baixa</SelectItem>
|
<SelectItem value="LOW">Baixa</SelectItem>
|
||||||
|
|
@ -113,7 +133,7 @@ export function NewTicketDialog() {
|
||||||
</Field>
|
</Field>
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel>Canal</FieldLabel>
|
<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>
|
<SelectTrigger><SelectValue placeholder="Canal" /></SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="EMAIL">E-mail</SelectItem>
|
<SelectItem value="EMAIL">E-mail</SelectItem>
|
||||||
|
|
@ -135,7 +155,7 @@ export function NewTicketDialog() {
|
||||||
<SelectTrigger><SelectValue placeholder="Sem fila" /></SelectTrigger>
|
<SelectTrigger><SelectValue placeholder="Sem fila" /></SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value={NONE}>Sem fila</SelectItem>
|
<SelectItem value={NONE}>Sem fila</SelectItem>
|
||||||
{queues.map((q: any) => (
|
{queues.map((q) => (
|
||||||
<SelectItem key={q.id} value={q.name}>{q.name}</SelectItem>
|
<SelectItem key={q.id} value={q.name}>{q.name}</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
|
import { useState } from "react"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { IconArrowRight, IconPlayerPlayFilled } from "@tabler/icons-react"
|
import { IconArrowRight, IconPlayerPlayFilled } from "@tabler/icons-react"
|
||||||
import { useMutation, useQuery } from "convex/react"
|
import { useMutation, useQuery } from "convex/react"
|
||||||
|
|
@ -9,7 +10,9 @@ import { useMutation, useQuery } from "convex/react"
|
||||||
import { api } from "@/convex/_generated/api"
|
import { api } from "@/convex/_generated/api"
|
||||||
import { DEFAULT_TENANT_ID } from "@/lib/constants"
|
import { DEFAULT_TENANT_ID } from "@/lib/constants"
|
||||||
import { useAuth } from "@/lib/auth-client"
|
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 { Badge } from "@/components/ui/badge"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
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 { TicketPriorityPill } from "@/components/tickets/priority-pill"
|
||||||
import { TicketStatusBadge } from "@/components/tickets/status-badge"
|
import { TicketStatusBadge } from "@/components/tickets/status-badge"
|
||||||
import { Spinner } from "@/components/ui/spinner"
|
import { Spinner } from "@/components/ui/spinner"
|
||||||
|
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select"
|
||||||
|
|
||||||
interface PlayNextTicketCardProps {
|
interface PlayNextTicketCardProps {
|
||||||
context?: TicketPlayContext
|
context?: TicketPlayContext
|
||||||
|
|
@ -25,19 +29,21 @@ interface PlayNextTicketCardProps {
|
||||||
export function PlayNextTicketCard({ context }: PlayNextTicketCardProps) {
|
export function PlayNextTicketCard({ context }: PlayNextTicketCardProps) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { userId } = useAuth()
|
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 playNext = useMutation(api.tickets.playNext)
|
||||||
|
const [selectedQueueId, setSelectedQueueId] = useState<string | undefined>(undefined)
|
||||||
|
|
||||||
const nextTicketFromServer = useQuery(api.tickets.list, {
|
const nextTicketFromServer = useQuery(api.tickets.list, {
|
||||||
tenantId: DEFAULT_TENANT_ID,
|
tenantId: DEFAULT_TENANT_ID,
|
||||||
status: undefined,
|
status: undefined,
|
||||||
priority: undefined,
|
priority: undefined,
|
||||||
channel: undefined,
|
channel: undefined,
|
||||||
queueId: undefined,
|
queueId: (selectedQueueId as Id<"queues">) || undefined,
|
||||||
limit: 1,
|
limit: 1,
|
||||||
})?.[0]
|
})?.[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) {
|
if (!cardContext || !cardContext.nextTicket) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -63,6 +69,18 @@ export function PlayNextTicketCard({ context }: PlayNextTicketCardProps) {
|
||||||
<TicketPriorityPill priority={ticket.priority} />
|
<TicketPriorityPill priority={ticket.priority} />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<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">
|
<div className="space-y-1">
|
||||||
<h2 className="text-xl font-medium text-foreground">{ticket.subject}</h2>
|
<h2 className="text-xl font-medium text-foreground">{ticket.subject}</h2>
|
||||||
<p className="text-sm text-muted-foreground">{ticket.summary}</p>
|
<p className="text-sm text-muted-foreground">{ticket.summary}</p>
|
||||||
|
|
@ -91,7 +109,7 @@ export function PlayNextTicketCard({ context }: PlayNextTicketCardProps) {
|
||||||
className="gap-2"
|
className="gap-2"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (!userId) return
|
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}`)
|
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 { mapTicketsFromServerList } from "@/lib/mappers/ticket";
|
||||||
import { TicketsTable } from "@/components/tickets/tickets-table";
|
import { TicketsTable } from "@/components/tickets/tickets-table";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
|
import type { Ticket } from "@/lib/schemas/ticket";
|
||||||
|
|
||||||
export function RecentTicketsPanel() {
|
export function RecentTicketsPanel() {
|
||||||
const ticketsRaw = useQuery(api.tickets.list, { tenantId: DEFAULT_TENANT_ID, limit: 10 });
|
const ticketsRaw = useQuery(api.tickets.list, { tenantId: DEFAULT_TENANT_ID, limit: 10 });
|
||||||
|
|
@ -24,10 +25,10 @@ export function RecentTicketsPanel() {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const tickets = mapTicketsFromServerList(ticketsRaw as any[]);
|
const tickets = mapTicketsFromServerList((ticketsRaw ?? []) as unknown[]);
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border bg-card">
|
<div className="rounded-xl border bg-card">
|
||||||
<TicketsTable tickets={tickets as any} />
|
<TicketsTable tickets={tickets} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { ticketStatusSchema } from "@/lib/schemas/ticket"
|
import { ticketStatusSchema, type TicketStatus } from "@/lib/schemas/ticket"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
|
||||||
const statusConfig = {
|
const statusConfig = {
|
||||||
|
|
@ -10,11 +10,9 @@ const statusConfig = {
|
||||||
ON_HOLD: { label: "Em espera", className: "bg-purple-100 text-purple-700 border-transparent" },
|
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" },
|
RESOLVED: { label: "Resolvido", className: "bg-emerald-100 text-emerald-700 border-transparent" },
|
||||||
CLOSED: { label: "Fechado", className: "bg-slate-100 text-slate-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 = {
|
type TicketStatusBadgeProps = { status: TicketStatus }
|
||||||
status: (typeof ticketStatusSchema)["_type"]
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TicketStatusBadge({ status }: TicketStatusBadgeProps) {
|
export function TicketStatusBadge({ status }: TicketStatusBadgeProps) {
|
||||||
const config = statusConfig[status]
|
const config = statusConfig[status]
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,13 @@ import { useMemo, useState } from "react"
|
||||||
import { formatDistanceToNow } from "date-fns"
|
import { formatDistanceToNow } from "date-fns"
|
||||||
import { ptBR } from "date-fns/locale"
|
import { ptBR } from "date-fns/locale"
|
||||||
import { IconLock, IconMessage } from "@tabler/icons-react"
|
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"
|
import { useAction, useMutation } from "convex/react"
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { api } from "@/convex/_generated/api"
|
import { api } from "@/convex/_generated/api"
|
||||||
import { useAuth } from "@/lib/auth-client"
|
import { useAuth } from "@/lib/auth-client"
|
||||||
|
import type { Id } from "@/convex/_generated/dataModel"
|
||||||
import type { TicketWithDetails } from "@/lib/schemas/ticket"
|
import type { TicketWithDetails } from "@/lib/schemas/ticket"
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||||
import { Badge } from "@/components/ui/badge"
|
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 { Button } from "@/components/ui/button"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { Dropzone } from "@/components/ui/dropzone"
|
import { Dropzone } from "@/components/ui/dropzone"
|
||||||
|
import { RichTextEditor, RichTextContent, sanitizeEditorHtml } from "@/components/ui/rich-text-editor"
|
||||||
import { Dialog, DialogContent } from "@/components/ui/dialog"
|
import { Dialog, DialogContent } from "@/components/ui/dialog"
|
||||||
|
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select"
|
||||||
|
|
||||||
interface TicketCommentsProps {
|
interface TicketCommentsProps {
|
||||||
ticket: TicketWithDetails
|
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 [attachmentsToSend, setAttachmentsToSend] = useState<Array<{ storageId: string; name: string; size?: number; type?: string; previewUrl?: string }>>([])
|
||||||
const [preview, setPreview] = useState<string | null>(null)
|
const [preview, setPreview] = useState<string | null>(null)
|
||||||
const [pending, setPending] = useState<Pick<TicketWithDetails["comments"][number], "id"|"author"|"visibility"|"body"|"attachments"|"createdAt"|"updatedAt">[]>([])
|
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(() => {
|
const commentsAll = useMemo(() => {
|
||||||
return [...pending, ...ticket.comments]
|
return [...pending, ...ticket.comments]
|
||||||
|
|
@ -43,19 +47,25 @@ export function TicketComments({ ticket }: TicketCommentsProps) {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const optimistic = {
|
const optimistic = {
|
||||||
id: `temp-${now.getTime()}`,
|
id: `temp-${now.getTime()}`,
|
||||||
author: ticket.requester, // placeholder; poderia buscar o próprio usuário se necessário
|
author: ticket.requester,
|
||||||
visibility: "PUBLIC" as const,
|
visibility,
|
||||||
body,
|
body: sanitizeEditorHtml(body),
|
||||||
attachments: attachments.map((a) => ({ id: a.storageId, name: a.name, url: a.previewUrl } as any)),
|
attachments: attachments.map((a) => ({ id: a.storageId, name: a.name, url: a.previewUrl })),
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
}
|
}
|
||||||
setPending((p) => [optimistic, ...p])
|
setPending((p) => [optimistic, ...p])
|
||||||
setBody("")
|
setBody("")
|
||||||
setAttachmentsToSend([])
|
setAttachmentsToSend([])
|
||||||
toast.loading("Enviando comentário…", { id: "comment" })
|
toast.loading("Enviando comentário.", { id: "comment" })
|
||||||
try {
|
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([])
|
setPending([])
|
||||||
toast.success("Comentário enviado!", { id: "comment" })
|
toast.success("Comentário enviado!", { id: "comment" })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -74,7 +84,7 @@ export function TicketComments({ ticket }: TicketCommentsProps) {
|
||||||
<CardContent className="space-y-6 px-4 pb-6">
|
<CardContent className="space-y-6 px-4 pb-6">
|
||||||
{commentsAll.length === 0 ? (
|
{commentsAll.length === 0 ? (
|
||||||
<p className="text-sm text-muted-foreground">
|
<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>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
commentsAll.map((comment) => {
|
commentsAll.map((comment) => {
|
||||||
|
|
@ -101,20 +111,19 @@ export function TicketComments({ ticket }: TicketCommentsProps) {
|
||||||
{formatDistanceToNow(comment.createdAt, { addSuffix: true, locale: ptBR })}
|
{formatDistanceToNow(comment.createdAt, { addSuffix: true, locale: ptBR })}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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">
|
<div className="rounded-lg border border-dashed bg-card px-3 py-2 text-sm leading-relaxed text-muted-foreground break-words">
|
||||||
{comment.body}
|
<RichTextContent html={comment.body} />
|
||||||
</div>
|
</div>
|
||||||
{comment.attachments?.length ? (
|
{comment.attachments?.length ? (
|
||||||
<div className="grid max-w-xl grid-cols-[repeat(auto-fill,minmax(96px,1fr))] gap-3">
|
<div className="grid max-w-xl grid-cols-[repeat(auto-fill,minmax(96px,1fr))] gap-3">
|
||||||
{comment.attachments.map((a) => {
|
{comment.attachments.map((att) => {
|
||||||
const att = a as any
|
|
||||||
const isImg = (att?.url ?? "").match(/\.(png|jpe?g|gif|webp|svg)$/i)
|
const isImg = (att?.url ?? "").match(/\.(png|jpe?g|gif|webp|svg)$/i)
|
||||||
if (isImg && att.url) {
|
if (isImg && att.url) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={att.id}
|
key={att.id}
|
||||||
type="button"
|
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"
|
className="group overflow-hidden rounded-lg border bg-card p-0.5 hover:shadow"
|
||||||
>
|
>
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
{/* 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">
|
<form onSubmit={handleSubmit} className="mt-4 space-y-3">
|
||||||
<textarea
|
<RichTextEditor value={body} onChange={setBody} placeholder="Escreva um comentário..." />
|
||||||
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)}
|
|
||||||
/>
|
|
||||||
<Dropzone onUploaded={(files) => setAttachmentsToSend((prev) => [...prev, ...files])} />
|
<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>
|
<Button type="submit" size="sm">Enviar</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -6,21 +6,23 @@ import { useQuery } from "convex/react";
|
||||||
import { api } from "@/convex/_generated/api";
|
import { api } from "@/convex/_generated/api";
|
||||||
import { DEFAULT_TENANT_ID } from "@/lib/constants";
|
import { DEFAULT_TENANT_ID } from "@/lib/constants";
|
||||||
import { mapTicketWithDetailsFromServer } from "@/lib/mappers/ticket";
|
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 { getTicketById } from "@/lib/mocks/tickets";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
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 { TicketDetailsPanel } from "@/components/tickets/ticket-details-panel";
|
||||||
import { TicketSummaryHeader } from "@/components/tickets/ticket-summary-header";
|
import { TicketSummaryHeader } from "@/components/tickets/ticket-summary-header";
|
||||||
import { TicketTimeline } from "@/components/tickets/ticket-timeline";
|
import { TicketTimeline } from "@/components/tickets/ticket-timeline";
|
||||||
|
|
||||||
export function TicketDetailView({ id }: { id: string }) {
|
export function TicketDetailView({ id }: { id: string }) {
|
||||||
const isMockId = id.startsWith("ticket-");
|
const isMockId = id.startsWith("ticket-");
|
||||||
const t = useQuery(api.tickets.getById, isMockId ? undefined : ({ tenantId: DEFAULT_TENANT_ID, id: id as any }));
|
const t = useQuery(api.tickets.getById, isMockId ? "skip" : ({ tenantId: DEFAULT_TENANT_ID, id: id as Id<"tickets"> }));
|
||||||
let ticket: any | null = null;
|
let ticket: TicketWithDetails | null = null;
|
||||||
if (t) {
|
if (t) {
|
||||||
ticket = mapTicketWithDetailsFromServer(t as any);
|
ticket = mapTicketWithDetailsFromServer(t as unknown);
|
||||||
} else if (isMockId) {
|
} else if (isMockId) {
|
||||||
ticket = getTicketById(id) ?? null;
|
ticket = getTicketById(id) ?? null;
|
||||||
}
|
}
|
||||||
|
|
@ -54,13 +56,13 @@ export function TicketDetailView({ id }: { id: string }) {
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6 px-4 lg:px-6">
|
<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="grid gap-6 lg:grid-cols-[2fr_1fr]">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<TicketComments ticket={ticket as any} />
|
<TicketComments ticket={ticket} />
|
||||||
<TicketTimeline ticket={ticket as any} />
|
<TicketTimeline ticket={ticket} />
|
||||||
</div>
|
</div>
|
||||||
<TicketDetailsPanel ticket={ticket as any} />
|
<TicketDetailsPanel ticket={ticket} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ interface TicketQueueSummaryProps {
|
||||||
|
|
||||||
export function TicketQueueSummaryCards({ queues }: TicketQueueSummaryProps) {
|
export function TicketQueueSummaryCards({ queues }: TicketQueueSummaryProps) {
|
||||||
const fromServer = useQuery(api.queues.summary, { tenantId: DEFAULT_TENANT_ID })
|
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) {
|
if (!queues && fromServer === undefined) {
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
<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 { api } from "@/convex/_generated/api"
|
||||||
|
|
||||||
import { useAuth } from "@/lib/auth-client"
|
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 { Badge } from "@/components/ui/badge"
|
||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from "@/components/ui/separator"
|
||||||
import { TicketPriorityPill } from "@/components/tickets/priority-pill"
|
import { TicketPriorityPill } from "@/components/tickets/priority-pill"
|
||||||
|
|
@ -27,9 +28,9 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
||||||
const updateStatus = useMutation(api.tickets.updateStatus)
|
const updateStatus = useMutation(api.tickets.updateStatus)
|
||||||
const changeAssignee = useMutation(api.tickets.changeAssignee)
|
const changeAssignee = useMutation(api.tickets.changeAssignee)
|
||||||
const changeQueue = useMutation(api.tickets.changeQueue)
|
const changeQueue = useMutation(api.tickets.changeQueue)
|
||||||
const agents = useQuery(api.users.listAgents, { tenantId: ticket.tenantId }) ?? []
|
const agents = (useQuery(api.users.listAgents, { tenantId: ticket.tenantId }) as Doc<"users">[] | undefined) ?? []
|
||||||
const queues = useQuery(api.queues.summary, { tenantId: ticket.tenantId }) ?? []
|
const queues = (useQuery(api.queues.summary, { tenantId: ticket.tenantId }) as TicketQueueSummary[] | undefined) ?? []
|
||||||
const [status, setStatus] = useState(ticket.status)
|
const [status, setStatus] = useState<TicketStatus>(ticket.status)
|
||||||
const statusPt: Record<string, string> = {
|
const statusPt: Record<string, string> = {
|
||||||
NEW: "Novo",
|
NEW: "Novo",
|
||||||
OPEN: "Aberto",
|
OPEN: "Aberto",
|
||||||
|
|
@ -47,16 +48,16 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
||||||
#{ticket.reference}
|
#{ticket.reference}
|
||||||
</Badge>
|
</Badge>
|
||||||
<TicketPriorityPill priority={ticket.priority} />
|
<TicketPriorityPill priority={ticket.priority} />
|
||||||
<TicketStatusBadge status={status as any} />
|
<TicketStatusBadge status={status} />
|
||||||
<Select
|
<Select
|
||||||
value={status}
|
value={status}
|
||||||
onValueChange={async (value) => {
|
onValueChange={async (value) => {
|
||||||
const prev = status
|
const prev = status
|
||||||
setStatus(value) // otimista
|
setStatus(value as import("@/lib/schemas/ticket").TicketStatus) // otimista
|
||||||
if (!userId) return
|
if (!userId) return
|
||||||
toast.loading("Atualizando status…", { id: "status" })
|
toast.loading("Atualizando status…", { id: "status" })
|
||||||
try {
|
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" })
|
toast.success(`Status alterado para ${statusPt[value]}.`, { id: "status" })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setStatus(prev)
|
setStatus(prev)
|
||||||
|
|
@ -95,7 +96,7 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
||||||
if (!userId) return
|
if (!userId) return
|
||||||
toast.loading("Atribuindo responsável…", { id: "assignee" })
|
toast.loading("Atribuindo responsável…", { id: "assignee" })
|
||||||
try {
|
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" })
|
toast.success("Responsável atualizado!", { id: "assignee" })
|
||||||
} catch {
|
} catch {
|
||||||
toast.error("Não foi possível atribuir.", { id: "assignee" })
|
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>
|
<SelectTrigger className="h-8 w-[180px]"><SelectValue placeholder="Selecionar" /></SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{agents.map((a: any) => (
|
{agents.map((a) => (
|
||||||
<SelectItem key={a._id} value={a._id}>{a.name}</SelectItem>
|
<SelectItem key={a._id} value={a._id}>{a.name}</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|
@ -118,11 +119,11 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
||||||
value={ticket.queue ?? ""}
|
value={ticket.queue ?? ""}
|
||||||
onValueChange={async (value) => {
|
onValueChange={async (value) => {
|
||||||
if (!userId) return
|
if (!userId) return
|
||||||
const q = queues.find((qq: any) => qq.name === value)
|
const q = queues.find((qq) => qq.name === value)
|
||||||
if (!q) return
|
if (!q) return
|
||||||
toast.loading("Atualizando fila…", { id: "queue" })
|
toast.loading("Atualizando fila…", { id: "queue" })
|
||||||
try {
|
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" })
|
toast.success("Fila atualizada!", { id: "queue" })
|
||||||
} catch {
|
} catch {
|
||||||
toast.error("Não foi possível atualizar a fila.", { id: "queue" })
|
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>
|
<SelectTrigger className="h-8 w-[180px]"><SelectValue placeholder="Selecionar" /></SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{queues.map((q: any) => (
|
{queues.map((q) => (
|
||||||
<SelectItem key={q.id} value={q.name}>{q.name}</SelectItem>
|
<SelectItem key={q.id} value={q.name}>{q.name}</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|
|
||||||
|
|
@ -56,12 +56,12 @@ export function TicketTimeline({ ticket }: TicketTimelineProps) {
|
||||||
{entry.payload?.actorName ? (
|
{entry.payload?.actorName ? (
|
||||||
<span className="flex items-center gap-1 text-xs text-muted-foreground">
|
<span className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||||
<Avatar className="size-5">
|
<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>
|
<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>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
por {entry.payload.actorName}
|
por {String(entry.payload?.actorName ?? '')}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
|
|
@ -69,7 +69,7 @@ export function TicketTimeline({ ticket }: TicketTimelineProps) {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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
|
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 === "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}` : ""}`
|
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 { api } from "@/convex/_generated/api"
|
||||||
import { DEFAULT_TENANT_ID } from "@/lib/constants"
|
import { DEFAULT_TENANT_ID } from "@/lib/constants"
|
||||||
import { mapTicketsFromServerList } from "@/lib/mappers/ticket"
|
import { mapTicketsFromServerList } from "@/lib/mappers/ticket"
|
||||||
|
import type { Ticket, TicketQueueSummary } from "@/lib/schemas/ticket"
|
||||||
import { TicketsFilters, TicketFiltersState, defaultTicketFilters } from "@/components/tickets/tickets-filters"
|
import { TicketsFilters, TicketFiltersState, defaultTicketFilters } from "@/components/tickets/tickets-filters"
|
||||||
import { TicketsTable } from "@/components/tickets/tickets-table"
|
import { TicketsTable } from "@/components/tickets/tickets-table"
|
||||||
import { Spinner } from "@/components/ui/spinner"
|
import { Spinner } from "@/components/ui/spinner"
|
||||||
|
|
@ -14,7 +15,7 @@ import { Spinner } from "@/components/ui/spinner"
|
||||||
export function TicketsView() {
|
export function TicketsView() {
|
||||||
const [filters, setFilters] = useState<TicketFiltersState>(defaultTicketFilters)
|
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, {
|
const ticketsRaw = useQuery(api.tickets.list, {
|
||||||
tenantId: DEFAULT_TENANT_ID,
|
tenantId: DEFAULT_TENANT_ID,
|
||||||
status: filters.status ?? undefined,
|
status: filters.status ?? undefined,
|
||||||
|
|
@ -24,16 +25,16 @@ export function TicketsView() {
|
||||||
search: filters.search || undefined,
|
search: filters.search || undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
const tickets = useMemo(() => mapTicketsFromServerList((ticketsRaw ?? []) as any[]), [ticketsRaw])
|
const tickets = useMemo(() => mapTicketsFromServerList((ticketsRaw ?? []) as unknown[]), [ticketsRaw])
|
||||||
|
|
||||||
const filteredTickets = useMemo(() => {
|
const filteredTickets = useMemo(() => {
|
||||||
if (!filters.queue) return tickets
|
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])
|
}, [tickets, filters.queue])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6 px-4 lg:px-6">
|
<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 ? (
|
{ticketsRaw === undefined ? (
|
||||||
<div className="rounded-xl border bg-card p-4">
|
<div className="rounded-xl border bg-card p-4">
|
||||||
<div className="grid gap-3">
|
<div className="grid gap-3">
|
||||||
|
|
@ -46,7 +47,7 @@ export function TicketsView() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<TicketsTable tickets={filteredTickets as any} />
|
<TicketsTable tickets={filteredTickets} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -7,26 +7,26 @@ const FieldSet = ({ className, ...props }: React.ComponentProps<"fieldset">) =>
|
||||||
<fieldset role="group" className={cn("grid gap-3", className)} {...props} />
|
<fieldset role="group" className={cn("grid gap-3", className)} {...props} />
|
||||||
)
|
)
|
||||||
|
|
||||||
const FieldLegend = ({ className, ...props }: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) => {
|
const FieldLegend = (
|
||||||
const { variant = "legend", ...rest } = props as any
|
{ className, variant = "legend", ...props }: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }
|
||||||
return (
|
) => (
|
||||||
<legend
|
<legend
|
||||||
className={cn(
|
className={cn(
|
||||||
variant === "label" ? "text-sm font-medium" : "text-sm font-semibold",
|
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">) => (
|
const FieldGroup = ({ className, ...props }: React.ComponentProps<"div">) => (
|
||||||
<div className={cn("@container/field-group grid gap-4", className)} {...props} />
|
<div className={cn("@container/field-group grid gap-4", className)} {...props} />
|
||||||
)
|
)
|
||||||
|
|
||||||
const Field = ({ className, ...props }: React.ComponentProps<"div"> & { orientation?: "vertical" | "horizontal" | "responsive" }) => {
|
const Field = (
|
||||||
const { orientation = "vertical", ...rest } = props as any
|
{ className, orientation = "vertical", ...props }: React.ComponentProps<"div"> & { orientation?: "vertical" | "horizontal" | "responsive" }
|
||||||
return (
|
) => (
|
||||||
<div
|
<div
|
||||||
data-orientation={orientation}
|
data-orientation={orientation}
|
||||||
className={cn(
|
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",
|
orientation === "responsive" && "@[480px]/field-group:flex-row @[480px]/field-group:items-center flex-col",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...rest}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
const FieldContent = ({ className, ...props }: React.ComponentProps<"div">) => (
|
const FieldContent = ({ className, ...props }: React.ComponentProps<"div">) => (
|
||||||
<div className={cn("flex flex-col", className)} {...props} />
|
<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 }
|
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";
|
"use client";
|
||||||
|
|
||||||
import React, { createContext, useContext, useEffect, useMemo, useState } from "react";
|
import React, { createContext, useContext, useEffect, useMemo, useState } from "react";
|
||||||
|
import type { Doc } from "@/convex/_generated/dataModel";
|
||||||
import { useMutation } from "convex/react";
|
import { useMutation } from "convex/react";
|
||||||
|
|
||||||
// Lazy import to avoid build errors before convex is generated
|
// 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 (!process.env.NEXT_PUBLIC_CONVEX_URL) return; // allow dev without backend
|
||||||
if (!localDemoUser) return;
|
if (!localDemoUser) return;
|
||||||
try {
|
try {
|
||||||
const user = await ensureUser({ tenantId, name: localDemoUser.name, email: localDemoUser.email, avatarUrl: localDemoUser.avatarUrl });
|
const user = (await ensureUser({ tenantId, name: localDemoUser.name, email: localDemoUser.email, avatarUrl: localDemoUser.avatarUrl })) as Doc<"users"> | null;
|
||||||
// Convex returns a full document
|
setUserId(user?._id ?? null);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
setUserId((user as any)?._id ?? null);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to ensure user:", e);
|
console.error("Failed to ensure user:", e);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ const serverEventSchema = z.object({
|
||||||
|
|
||||||
const serverTicketWithDetailsSchema = serverTicketSchema.extend({
|
const serverTicketWithDetailsSchema = serverTicketSchema.extend({
|
||||||
description: z.string().optional().nullable(),
|
description: z.string().optional().nullable(),
|
||||||
customFields: z.record(z.any()).default({}).optional(),
|
customFields: z.record(z.string(), z.any()).optional(),
|
||||||
timeline: z.array(serverEventSchema),
|
timeline: z.array(serverEventSchema),
|
||||||
comments: z.array(serverCommentSchema),
|
comments: z.array(serverCommentSchema),
|
||||||
});
|
});
|
||||||
|
|
@ -76,7 +76,6 @@ export function mapTicketFromServer(input: unknown) {
|
||||||
firstResponseAt: s.firstResponseAt ? new Date(s.firstResponseAt) : null,
|
firstResponseAt: s.firstResponseAt ? new Date(s.firstResponseAt) : null,
|
||||||
resolvedAt: s.resolvedAt ? new Date(s.resolvedAt) : 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>;
|
return ui as unknown as z.infer<typeof ticketSchema>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -88,6 +87,7 @@ export function mapTicketWithDetailsFromServer(input: unknown) {
|
||||||
const s = serverTicketWithDetailsSchema.parse(input);
|
const s = serverTicketWithDetailsSchema.parse(input);
|
||||||
const ui = {
|
const ui = {
|
||||||
...s,
|
...s,
|
||||||
|
customFields: (s.customFields ?? {}) as Record<string, unknown>,
|
||||||
lastTimelineEntry: s.lastTimelineEntry ?? undefined,
|
lastTimelineEntry: s.lastTimelineEntry ?? undefined,
|
||||||
updatedAt: new Date(s.updatedAt),
|
updatedAt: new Date(s.updatedAt),
|
||||||
createdAt: new Date(s.createdAt),
|
createdAt: new Date(s.createdAt),
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ export type TicketComment = z.infer<typeof ticketCommentSchema>
|
||||||
export const ticketEventSchema = z.object({
|
export const ticketEventSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
payload: z.record(z.any()).optional(),
|
payload: z.record(z.string(), z.any()).optional(),
|
||||||
createdAt: z.coerce.date(),
|
createdAt: z.coerce.date(),
|
||||||
})
|
})
|
||||||
export type TicketEvent = z.infer<typeof ticketEventSchema>
|
export type TicketEvent = z.infer<typeof ticketEventSchema>
|
||||||
|
|
@ -102,7 +102,7 @@ export type Ticket = z.infer<typeof ticketSchema>
|
||||||
|
|
||||||
export const ticketWithDetailsSchema = ticketSchema.extend({
|
export const ticketWithDetailsSchema = ticketSchema.extend({
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
customFields: z.record(z.any()).default({}),
|
customFields: z.record(z.string(), z.any()).optional(),
|
||||||
timeline: z.array(ticketEventSchema),
|
timeline: z.array(ticketEventSchema),
|
||||||
comments: z.array(ticketCommentSchema),
|
comments: z.array(ticketCommentSchema),
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"],
|
||||||
|
"@/convex/*": ["./convex/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue