@consilioweb/payload-support 0.15.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +347 -435
- package/dist/components/TicketConversation/RewriteDropdown.cjs +98 -0
- package/dist/components/TicketConversation/RewriteDropdown.js +93 -0
- package/dist/components/TicketConversation/SkeletonText.cjs +21 -0
- package/dist/components/TicketConversation/SkeletonText.js +20 -0
- package/dist/components/TicketConversation/components/ActionPanels.cjs +72 -0
- package/dist/components/TicketConversation/components/ActionPanels.js +73 -2
- package/dist/components/TicketConversation/components/QuickActions.cjs +22 -1
- package/dist/components/TicketConversation/components/QuickActions.js +22 -1
- package/dist/components/TicketConversation/constants.cjs +27 -0
- package/dist/components/TicketConversation/constants.js +26 -1
- package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.cjs +20 -0
- package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.js +19 -0
- package/dist/components/TicketConversation/hooks/useTicketActions.cjs +59 -0
- package/dist/components/TicketConversation/hooks/useTicketActions.js +59 -0
- package/dist/components/TicketConversation/index.cjs +42 -149
- package/dist/components/TicketConversation/index.js +37 -144
- package/dist/index.cjs +1835 -741
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1833 -741
- package/dist/views/BillingView/client.cjs +40 -54
- package/dist/views/BillingView/client.js +40 -54
- package/dist/views/ChatView/client.cjs +4 -3
- package/dist/views/ChatView/client.js +4 -3
- package/dist/views/CrmView/client.cjs +7 -7
- package/dist/views/CrmView/client.js +7 -7
- package/dist/views/EmailTrackingView/client.cjs +6 -5
- package/dist/views/EmailTrackingView/client.js +6 -5
- package/dist/views/ImportConversationView/client.cjs +4 -4
- package/dist/views/ImportConversationView/client.js +4 -4
- package/dist/views/LogsView/client.cjs +3 -2
- package/dist/views/LogsView/client.js +3 -2
- package/dist/views/NewTicketView/client.cjs +1 -1
- package/dist/views/NewTicketView/client.js +1 -1
- package/dist/views/PendingEmailsView/client.cjs +7 -6
- package/dist/views/PendingEmailsView/client.js +7 -6
- package/dist/views/SupportDashboardView/client.cjs +19 -22
- package/dist/views/SupportDashboardView/client.js +19 -22
- package/dist/views/TicketDetailView/NextActionItem.cjs +40 -0
- package/dist/views/TicketDetailView/NextActionItem.js +34 -0
- package/dist/views/TicketDetailView/RewriteDropdown.cjs +80 -0
- package/dist/views/TicketDetailView/RewriteDropdown.js +78 -0
- package/dist/views/TicketDetailView/client.cjs +59 -190
- package/dist/views/TicketDetailView/client.js +52 -183
- package/dist/views/TicketDetailView/constants.cjs +20 -0
- package/dist/views/TicketDetailView/constants.js +16 -0
- package/dist/views/TicketDetailView/helpers.cjs +23 -0
- package/dist/views/TicketDetailView/helpers.js +20 -0
- package/dist/views/TicketDetailView/types.cjs +2 -0
- package/dist/views/TicketDetailView/types.js +1 -0
- package/dist/views/TicketInboxView/client.cjs +14 -10
- package/dist/views/TicketInboxView/client.js +14 -10
- package/dist/views/TicketingSettingsView/client.cjs +101 -96
- package/dist/views/TicketingSettingsView/client.js +92 -88
- package/dist/views/TimeDashboardView/client.cjs +2 -2
- package/dist/views/TimeDashboardView/client.js +2 -2
- package/dist/views/shared/StatusPill.cjs +23 -12
- package/dist/views/shared/StatusPill.js +23 -12
- package/dist/views/shared/dateLocale.cjs +5 -0
- package/dist/views/shared/dateLocale.js +3 -0
- package/package.json +23 -15
- package/src/__tests__/generateTrackingToken.test.ts +53 -0
- package/src/__tests__/integration/ai-agent.test.ts +46 -0
- package/src/__tests__/integration/auth.test.ts +43 -0
- package/src/__tests__/integration/automation-rules.test.ts +68 -0
- package/src/__tests__/integration/buildTestPayload.ts +52 -0
- package/src/__tests__/integration/channels.test.ts +52 -0
- package/src/__tests__/integration/core-behaviors.test.ts +58 -0
- package/src/__tests__/integration/dashboard-volume.test.ts +31 -0
- package/src/__tests__/integration/digest.test.ts +48 -0
- package/src/__tests__/integration/invoice-pdf.test.ts +30 -0
- package/src/__tests__/integration/isolation.test.ts +81 -0
- package/src/__tests__/integration/mentions-invoice.test.ts +50 -0
- package/src/__tests__/integration/nps.test.ts +37 -0
- package/src/__tests__/integration/sanitization.test.ts +35 -0
- package/src/__tests__/integration/sla-pause.test.ts +39 -0
- package/src/__tests__/integration/smoke.test.ts +26 -0
- package/src/__tests__/integration/snooze.test.ts +68 -0
- package/src/__tests__/integration/teams.test.ts +38 -0
- package/src/__tests__/rateLimiter.test.ts +89 -0
- package/src/__tests__/sanitizeHtml.test.ts +165 -0
- package/src/__tests__/sla.test.ts +258 -0
- package/src/collections/AutomationRules.ts +66 -0
- package/src/collections/NotificationQueue.ts +30 -0
- package/src/collections/SatisfactionSurveys.ts +11 -2
- package/src/collections/SupportClients.ts +58 -1
- package/src/collections/SupportTeam.ts +27 -0
- package/src/collections/TicketActivityLog.ts +9 -4
- package/src/collections/TicketMessages.ts +137 -31
- package/src/collections/Tickets.ts +31 -72
- package/src/collections/index.ts +3 -0
- package/src/components/TicketConversation/RewriteDropdown.tsx +86 -0
- package/src/components/TicketConversation/SkeletonText.tsx +23 -0
- package/src/components/TicketConversation/components/ActionPanels.tsx +82 -1
- package/src/components/TicketConversation/components/QuickActions.tsx +25 -0
- package/src/components/TicketConversation/constants.ts +29 -0
- package/src/components/TicketConversation/hooks/useDocumentIdFromUrl.ts +21 -0
- package/src/components/TicketConversation/hooks/useTicketActions.ts +51 -0
- package/src/components/TicketConversation/index.tsx +29 -157
- package/src/endpoints/admin-chat-stream.ts +3 -4
- package/src/endpoints/admin-chat.ts +11 -20
- package/src/endpoints/admin-stats.ts +70 -32
- package/src/endpoints/ai-agent.ts +36 -0
- package/src/endpoints/apply-macro.ts +8 -14
- package/src/endpoints/auth-2fa.ts +12 -12
- package/src/endpoints/auto-close.ts +38 -18
- package/src/endpoints/billing.ts +17 -4
- package/src/endpoints/bulk-action.ts +7 -12
- package/src/endpoints/channels.ts +89 -0
- package/src/endpoints/chat-stream.ts +3 -4
- package/src/endpoints/chat.ts +7 -12
- package/src/endpoints/chatbot.ts +2 -2
- package/src/endpoints/client-intelligence.ts +4 -4
- package/src/endpoints/delete-account.ts +9 -16
- package/src/endpoints/email-stats.ts +2 -2
- package/src/endpoints/escalate.ts +2 -2
- package/src/endpoints/export-csv.ts +2 -2
- package/src/endpoints/export-data.ts +5 -8
- package/src/endpoints/import-conversation.ts +6 -10
- package/src/endpoints/index.ts +24 -2
- package/src/endpoints/invite-collaborator.ts +30 -15
- package/src/endpoints/invoice.ts +127 -0
- package/src/endpoints/kb-search.ts +2 -2
- package/src/endpoints/login.ts +12 -4
- package/src/endpoints/merge-clients.ts +14 -19
- package/src/endpoints/merge-tickets.ts +11 -18
- package/src/endpoints/oauth-google.ts +86 -39
- package/src/endpoints/pending-emails-process.ts +15 -25
- package/src/endpoints/process-digests.ts +93 -0
- package/src/endpoints/process-scheduled.ts +6 -10
- package/src/endpoints/process-snooze.ts +78 -0
- package/src/endpoints/purge-logs.ts +2 -2
- package/src/endpoints/resend-notification.ts +3 -4
- package/src/endpoints/round-robin-config.ts +3 -4
- package/src/endpoints/satisfaction.ts +21 -14
- package/src/endpoints/search.ts +5 -8
- package/src/endpoints/seed-kb.ts +3 -4
- package/src/endpoints/send-reminder.ts +186 -0
- package/src/endpoints/settings.ts +5 -2
- package/src/endpoints/signature.ts +3 -4
- package/src/endpoints/sla-check.ts +2 -2
- package/src/endpoints/split-ticket.ts +7 -12
- package/src/endpoints/statuses.ts +2 -2
- package/src/endpoints/ticket-feedback.ts +4 -6
- package/src/endpoints/ticket-synthesis.ts +2 -2
- package/src/endpoints/track-open.ts +16 -28
- package/src/endpoints/transfer-ticket.ts +21 -9
- package/src/endpoints/user-prefs.ts +3 -4
- package/src/hooks/automationRules.ts +84 -0
- package/src/hooks/checkSLA.ts +48 -16
- package/src/plugin.ts +6 -0
- package/src/portal/LiveChat.tsx +2 -63
- package/src/portal/auth/tickets/detail/CloseTicketButton.tsx +10 -0
- package/src/portal/auth/tickets/detail/MarkSolutionButton.tsx +14 -2
- package/src/portal/auth/tickets/detail/MessageActions.tsx +37 -18
- package/src/portal/auth/tickets/detail/ReopenTicketButton.tsx +10 -0
- package/src/portal/auth/tickets/detail/TransferAndInviteActions.tsx +3 -2
- package/src/portal/auth/tickets/detail/page.tsx +2 -2
- package/src/portal/helpers.ts +46 -0
- package/src/portal/login/page.tsx +3 -8
- package/src/portal/types.ts +15 -0
- package/src/types/lucide-react.d.ts +34 -0
- package/src/types/payload-config.d.ts +10 -0
- package/src/utils/adminNotification.ts +2 -2
- package/src/utils/aiAgent.ts +147 -0
- package/src/utils/auth.ts +5 -4
- package/src/utils/channels.ts +36 -0
- package/src/utils/db.ts +73 -0
- package/src/utils/emailTemplate.ts +8 -1
- package/src/utils/fireWebhooks.ts +4 -6
- package/src/utils/generateTicketSynthesis.ts +7 -12
- package/src/utils/invoicePdf.ts +88 -0
- package/src/utils/notificationQueue.ts +36 -0
- package/src/utils/readSettings.ts +22 -6
- package/src/utils/sanitizeHtml.ts +57 -0
- package/src/utils/slugs.ts +6 -0
- package/src/utils/teamAccess.ts +27 -0
- package/src/utils/ticketAccess.ts +53 -0
- package/src/utils/webhookDispatcher.ts +4 -6
- package/src/views/BillingView/client.tsx +49 -37
- package/src/views/ChatView/client.tsx +4 -3
- package/src/views/CrmView/client.tsx +7 -7
- package/src/views/EmailTrackingView/client.tsx +7 -6
- package/src/views/ImportConversationView/client.tsx +4 -4
- package/src/views/LogsView/client.tsx +3 -2
- package/src/views/NewTicketView/client.tsx +1 -1
- package/src/views/PendingEmailsView/client.tsx +7 -6
- package/src/views/SupportDashboardView/client.tsx +21 -21
- package/src/views/TicketDetailView/NextActionItem.tsx +30 -0
- package/src/views/TicketDetailView/RewriteDropdown.tsx +63 -0
- package/src/views/TicketDetailView/client.tsx +54 -179
- package/src/views/TicketDetailView/constants.ts +23 -0
- package/src/views/TicketDetailView/helpers.ts +17 -0
- package/src/views/TicketDetailView/types.ts +10 -0
- package/src/views/TicketInboxView/client.tsx +16 -10
- package/src/views/TicketingSettingsView/client.tsx +90 -87
- package/src/views/TimeDashboardView/client.tsx +2 -2
- package/src/views/shared/StatusPill.tsx +24 -12
- package/src/views/shared/dateLocale.ts +5 -0
- package/src/views/shared/locales/en.json +708 -0
- package/src/views/shared/locales/fr.json +708 -0
|
@@ -6,6 +6,10 @@ import { createAdminNotification } from '../utils/adminNotification'
|
|
|
6
6
|
import { dispatchWebhook } from '../utils/webhookDispatcher'
|
|
7
7
|
import { readSupportSettings } from '../utils/readSettings'
|
|
8
8
|
import { createCheckSlaOnReply } from '../hooks/checkSLA'
|
|
9
|
+
import { resolveAccessibleTicketIds } from '../utils/ticketAccess'
|
|
10
|
+
import { sanitizeMessageHtml } from '../utils/sanitizeHtml'
|
|
11
|
+
import { queueClientNotification } from '../utils/notificationQueue'
|
|
12
|
+
import { dbFind, dbCreate, dbFindByID } from '../utils/db'
|
|
9
13
|
|
|
10
14
|
function createAssignAuthor(slugs: CollectionSlugs): CollectionBeforeChangeHook {
|
|
11
15
|
return async ({ data, operation, req }) => {
|
|
@@ -30,8 +34,16 @@ function createAutoUpdateStatus(slugs: CollectionSlugs): CollectionAfterChangeHo
|
|
|
30
34
|
if (!doc.isInternal) {
|
|
31
35
|
if (doc.authorType === 'admin') {
|
|
32
36
|
updateData.status = 'waiting_client'
|
|
37
|
+
// A fresh public reply starts a new waiting window: drop any pending
|
|
38
|
+
// auto-close arming (manual deadline + day-based reminder anchor) so
|
|
39
|
+
// reminders re-arm cleanly from here.
|
|
40
|
+
updateData.autoCloseScheduledAt = null
|
|
41
|
+
updateData.autoCloseRemindedAt = null
|
|
33
42
|
} else if (doc.authorType === 'client' || doc.authorType === 'email') {
|
|
34
43
|
updateData.lastClientMessageAt = new Date().toISOString()
|
|
44
|
+
// The client answered — cancel any pending automatic closure.
|
|
45
|
+
updateData.autoCloseScheduledAt = null
|
|
46
|
+
updateData.autoCloseRemindedAt = null
|
|
35
47
|
if (ticket.status && ['waiting_client', 'resolved'].includes(ticket.status as string)) {
|
|
36
48
|
updateData.status = 'open'
|
|
37
49
|
}
|
|
@@ -82,30 +94,51 @@ function createNotifyClient(slugs: CollectionSlugs): CollectionAfterChangeHook {
|
|
|
82
94
|
|
|
83
95
|
const replyPreview = (doc.body || '').replace(/\s+/g, ' ').trim().slice(0, 90)
|
|
84
96
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
].join(''), {
|
|
98
|
-
kind: 'reply',
|
|
99
|
-
preheader: replyPreview ? `${replyPreview}…` : `Nouvelle reponse a votre ticket ${ticketNumber}`,
|
|
100
|
-
}),
|
|
101
|
-
})
|
|
97
|
+
// Digest: clients on daily/weekly frequency are queued, not emailed now.
|
|
98
|
+
const freq = (client as { notificationFrequency?: string }).notificationFrequency
|
|
99
|
+
if (freq === 'daily' || freq === 'weekly') {
|
|
100
|
+
await queueClientNotification(req.payload, slugs, {
|
|
101
|
+
client: (client as { id: number | string }).id,
|
|
102
|
+
type: 'reply',
|
|
103
|
+
title: `Nouvelle reponse — ${ticketNumber}`,
|
|
104
|
+
message: replyPreview || `Reponse a votre ticket ${ticketNumber}`,
|
|
105
|
+
ticket: ticketId,
|
|
106
|
+
})
|
|
107
|
+
return doc
|
|
108
|
+
}
|
|
102
109
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
110
|
+
// Fire-and-forget: don't block the reply mutation on the SMTP round-trip
|
|
111
|
+
// (200-500ms). The email + its emailSentAt bookkeeping run detached.
|
|
112
|
+
void (async () => {
|
|
113
|
+
try {
|
|
114
|
+
await req.payload.sendEmail({
|
|
115
|
+
to: client.email,
|
|
116
|
+
...(supportEmail ? { replyTo: supportEmail } : {}),
|
|
117
|
+
...(internalBcc ? { bcc: internalBcc } : {}),
|
|
118
|
+
subject: `Re: [${ticketNumber}] ${subject}`,
|
|
119
|
+
html: emailWrapper(`Nouvelle reponse — ${ticketNumber}`, [
|
|
120
|
+
emailParagraph(`Bonjour <strong>${escapeHtml(client.firstName || '')}</strong>,`),
|
|
121
|
+
emailParagraph(`Notre equipe a apporte une reponse a votre ticket <strong>${escapeHtml(ticketNumber)}</strong> — <em>${escapeHtml(subject)}</em>.`),
|
|
122
|
+
rawContent,
|
|
123
|
+
emailButton('Consulter le ticket', portalUrl),
|
|
124
|
+
emailParagraph('<span style="font-size: 13px; color: #6b7280;">Vous pouvez egalement repondre directement a cet email. Votre message sera automatiquement ajoute au ticket.</span>'),
|
|
125
|
+
emailTrackingPixel(ticketId, doc.id),
|
|
126
|
+
].join(''), {
|
|
127
|
+
kind: 'reply',
|
|
128
|
+
preheader: replyPreview ? `${replyPreview}…` : `Nouvelle reponse a votre ticket ${ticketNumber}`,
|
|
129
|
+
}),
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
await req.payload.update({
|
|
133
|
+
collection: slugs.ticketMessages,
|
|
134
|
+
id: doc.id,
|
|
135
|
+
data: { emailSentAt: new Date().toISOString(), emailSentTo: client.email },
|
|
136
|
+
overrideAccess: true,
|
|
137
|
+
})
|
|
138
|
+
} catch (err) {
|
|
139
|
+
console.error('[support] Failed to notify client (async):', err)
|
|
140
|
+
}
|
|
141
|
+
})()
|
|
109
142
|
} catch (err) {
|
|
110
143
|
console.error('[support] Failed to notify client:', err)
|
|
111
144
|
}
|
|
@@ -153,8 +186,7 @@ function createSyncTicketReplyToChat(slugs: CollectionSlugs): CollectionAfterCha
|
|
|
153
186
|
const clientId = typeof ticket.client === 'object' ? (ticket.client as { id: number | string }).id : ticket.client
|
|
154
187
|
|
|
155
188
|
// Create a chat message so the client sees it in the widget
|
|
156
|
-
await payload.
|
|
157
|
-
collection: slugs.chatMessages as any,
|
|
189
|
+
await dbCreate(payload, slugs.chatMessages, {
|
|
158
190
|
data: {
|
|
159
191
|
session: ticket.chatSession,
|
|
160
192
|
client: clientId,
|
|
@@ -294,6 +326,72 @@ function createDispatchWebhookOnReply(slugs: CollectionSlugs): CollectionAfterCh
|
|
|
294
326
|
}
|
|
295
327
|
}
|
|
296
328
|
|
|
329
|
+
/**
|
|
330
|
+
* Sanitize submitted rich HTML before persistence. `bodyHtml` is rendered via
|
|
331
|
+
* dangerouslySetInnerHTML in the admin & portal, so unsanitized HTML is a
|
|
332
|
+
* stored-XSS vector (→ admin session theft → cross-client access). Runs for
|
|
333
|
+
* every writer (client AND admin) — neither needs to store scripts.
|
|
334
|
+
*/
|
|
335
|
+
function createSanitizeMessageHtml(): CollectionBeforeChangeHook {
|
|
336
|
+
return ({ data }) => {
|
|
337
|
+
if (data && typeof data.bodyHtml === 'string' && data.bodyHtml) {
|
|
338
|
+
data.bodyHtml = sanitizeMessageHtml(data.bodyHtml)
|
|
339
|
+
}
|
|
340
|
+
return data
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Matches `@someone@example.com` mentions inside a message body.
|
|
345
|
+
const MENTION_RE = /@([^\s@]+@[^\s@]+\.[^\s@]+)/g
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Resolve `@email` mentions in the message body to agent (users) IDs, stored on
|
|
349
|
+
* the `mentions` field so the UI can highlight them and we can notify them.
|
|
350
|
+
*/
|
|
351
|
+
function createResolveMentions(slugs: CollectionSlugs): CollectionBeforeChangeHook {
|
|
352
|
+
return async ({ data, operation, req }) => {
|
|
353
|
+
if (operation !== 'create') return data
|
|
354
|
+
const body = typeof data?.body === 'string' ? data.body : ''
|
|
355
|
+
const emails = Array.from(new Set([...body.matchAll(MENTION_RE)].map((m) => m[1].toLowerCase())))
|
|
356
|
+
if (emails.length === 0) return data
|
|
357
|
+
try {
|
|
358
|
+
const users = await dbFind(req.payload, slugs.users, { where: { email: { in: emails } }, limit: 50, depth: 0, overrideAccess: true })
|
|
359
|
+
const ids = users.docs.map((u) => (u as { id: number | string }).id)
|
|
360
|
+
if (ids.length > 0) data.mentions = ids
|
|
361
|
+
} catch { /* users lookup is best-effort */ }
|
|
362
|
+
return data
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Email the agents mentioned in a freshly-created message. */
|
|
367
|
+
function createNotifyMentions(slugs: CollectionSlugs): CollectionAfterChangeHook {
|
|
368
|
+
return async ({ doc, operation, req }) => {
|
|
369
|
+
if (operation !== 'create') return doc
|
|
370
|
+
const mentions: Array<number | string | { id: number | string }> = Array.isArray(doc.mentions) ? doc.mentions : []
|
|
371
|
+
if (mentions.length === 0) return doc
|
|
372
|
+
const ticketId = typeof doc.ticket === 'object' ? doc.ticket.id : doc.ticket
|
|
373
|
+
const baseUrl = process.env.NEXT_PUBLIC_SERVER_URL || ''
|
|
374
|
+
for (const m of mentions) {
|
|
375
|
+
const userId = typeof m === 'object' ? m.id : m
|
|
376
|
+
try {
|
|
377
|
+
const user = await dbFindByID(req.payload, slugs.users, { id: userId, depth: 0, overrideAccess: true })
|
|
378
|
+
if (!user?.email) continue
|
|
379
|
+
await req.payload.sendEmail({
|
|
380
|
+
to: user.email,
|
|
381
|
+
subject: `Vous avez ete mentionne — ticket #${ticketId}`,
|
|
382
|
+
html: emailWrapper('Mention', [
|
|
383
|
+
emailParagraph(`Vous avez ete mentionne dans un message du ticket <strong>#${escapeHtml(String(ticketId))}</strong>.`),
|
|
384
|
+
...(baseUrl ? [emailButton('Ouvrir le ticket', `${baseUrl}/admin/collections/${slugs.tickets}/${ticketId}`, 'dark')] : []),
|
|
385
|
+
].join(''), { kind: 'system' }),
|
|
386
|
+
})
|
|
387
|
+
} catch (err) {
|
|
388
|
+
console.error('[support] Failed to notify mention:', err)
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return doc
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
297
395
|
export function createTicketMessagesCollection(slugs: CollectionSlugs, options?: {
|
|
298
396
|
notificationSlug?: string
|
|
299
397
|
}): CollectionConfig {
|
|
@@ -304,14 +402,14 @@ export function createTicketMessagesCollection(slugs: CollectionSlugs, options?:
|
|
|
304
402
|
labels: { singular: 'Message', plural: 'Messages' },
|
|
305
403
|
admin: { hidden: true, group: 'Support', defaultColumns: ['ticket', 'authorType', 'createdAt'] },
|
|
306
404
|
fields: [
|
|
307
|
-
{ name: 'ticket', type: 'relationship', relationTo: slugs.tickets, required: true, label: 'Ticket' },
|
|
405
|
+
{ name: 'ticket', type: 'relationship', relationTo: slugs.tickets, required: true, index: true, label: 'Ticket' },
|
|
308
406
|
{ name: 'body', type: 'textarea', required: true, label: 'Message' },
|
|
309
407
|
{ name: 'bodyHtml', type: 'textarea', label: 'Message HTML', admin: { hidden: true } },
|
|
310
408
|
{
|
|
311
409
|
type: 'row',
|
|
312
410
|
fields: [
|
|
313
411
|
{
|
|
314
|
-
name: 'authorType', type: 'select', label: 'Type d\'auteur', defaultValue: 'admin',
|
|
412
|
+
name: 'authorType', type: 'select', index: true, label: 'Type d\'auteur', defaultValue: 'admin',
|
|
315
413
|
options: [
|
|
316
414
|
{ label: 'Client', value: 'client' },
|
|
317
415
|
{ label: 'Support', value: 'admin' },
|
|
@@ -340,8 +438,9 @@ export function createTicketMessagesCollection(slugs: CollectionSlugs, options?:
|
|
|
340
438
|
},
|
|
341
439
|
{ name: 'isInternal', type: 'checkbox', defaultValue: false, label: 'Note interne', admin: { position: 'sidebar' } },
|
|
342
440
|
{ name: 'isSolution', type: 'checkbox', defaultValue: false, label: 'Reponse solution', admin: { position: 'sidebar' } },
|
|
441
|
+
{ name: 'mentions', type: 'relationship', relationTo: slugs.users, hasMany: true, label: 'Agents mentionnes', admin: { hidden: true } },
|
|
343
442
|
{ name: 'skipNotification', type: 'checkbox', defaultValue: false, label: 'Sans notification', admin: { position: 'sidebar', condition: (data) => data?.skipNotification === true } },
|
|
344
|
-
{ name: 'scheduledAt', type: 'date', label: 'Programme pour', admin: { date: { pickerAppearance: 'dayAndTime' }, position: 'sidebar', condition: (data) => !!data?.scheduledAt } },
|
|
443
|
+
{ name: 'scheduledAt', type: 'date', index: true, label: 'Programme pour', admin: { date: { pickerAppearance: 'dayAndTime' }, position: 'sidebar', condition: (data) => !!data?.scheduledAt } },
|
|
345
444
|
{ name: 'scheduledSent', type: 'checkbox', defaultValue: false, admin: { hidden: true } },
|
|
346
445
|
{ name: 'editedAt', type: 'date', label: 'Modifie le', admin: { hidden: true } },
|
|
347
446
|
{ name: 'deletedAt', type: 'date', label: 'Supprime le', admin: { hidden: true } },
|
|
@@ -350,7 +449,7 @@ export function createTicketMessagesCollection(slugs: CollectionSlugs, options?:
|
|
|
350
449
|
{ name: 'emailOpenedAt', type: 'date', label: 'Email ouvert le', admin: { hidden: true } },
|
|
351
450
|
],
|
|
352
451
|
hooks: {
|
|
353
|
-
beforeChange: [createAssignAuthor(slugs)],
|
|
452
|
+
beforeChange: [createSanitizeMessageHtml(), createResolveMentions(slugs), createAssignAuthor(slugs)],
|
|
354
453
|
afterChange: [
|
|
355
454
|
createAutoUpdateStatus(slugs),
|
|
356
455
|
createNotifyClient(slugs),
|
|
@@ -360,16 +459,23 @@ export function createTicketMessagesCollection(slugs: CollectionSlugs, options?:
|
|
|
360
459
|
createNotifyAdminOnClientMessage(slugs, notificationSlug),
|
|
361
460
|
createFireMessageWebhooks(slugs),
|
|
362
461
|
createDispatchWebhookOnReply(slugs),
|
|
462
|
+
createNotifyMentions(slugs),
|
|
363
463
|
],
|
|
364
464
|
},
|
|
365
465
|
access: {
|
|
366
466
|
create: ({ req }) => req.user?.collection === slugs.users || req.user?.collection === slugs.supportClients,
|
|
367
|
-
read: ({ req }) => {
|
|
467
|
+
read: async ({ req }) => {
|
|
368
468
|
if (req.user?.collection === slugs.users) return true
|
|
369
469
|
if (req.user?.collection === slugs.supportClients) {
|
|
470
|
+
// Constrain to tickets the client owns or collaborates on, then filter
|
|
471
|
+
// messages by `ticket IN (...)`. We do NOT use a nested relationship
|
|
472
|
+
// filter ('ticket.client') as the security boundary — its behaviour
|
|
473
|
+
// through the SQLite adapter is not guaranteed. Empty set → sentinel
|
|
474
|
+
// id so the IN matches nothing (fail closed).
|
|
475
|
+
const ids = await resolveAccessibleTicketIds(req.payload, slugs, req.user.id)
|
|
370
476
|
return {
|
|
371
477
|
and: [
|
|
372
|
-
{
|
|
478
|
+
{ ticket: { in: ids.length > 0 ? ids : [-1] } } as Where,
|
|
373
479
|
{ isInternal: { equals: false } } as Where,
|
|
374
480
|
{ or: [{ scheduledAt: { exists: false } } as Where, { scheduledSent: { equals: true } } as Where] } as Where,
|
|
375
481
|
],
|
|
@@ -13,8 +13,11 @@ import { createAdminNotification } from '../utils/adminNotification'
|
|
|
13
13
|
import { dispatchWebhook } from '../utils/webhookDispatcher'
|
|
14
14
|
import { readSupportSettings } from '../utils/readSettings'
|
|
15
15
|
import { createTicketStatusEmail } from '../hooks/ticketStatusEmail'
|
|
16
|
-
import { createAssignSlaDeadlines, createCheckSlaOnResolve } from '../hooks/checkSLA'
|
|
16
|
+
import { createAssignSlaDeadlines, createCheckSlaOnResolve, createPauseSlaOnHold } from '../hooks/checkSLA'
|
|
17
|
+
import { createApplyAutomationRules } from '../hooks/automationRules'
|
|
18
|
+
import { isTeamScopingEnabled, resolveAgentTeamIds } from '../utils/teamAccess'
|
|
17
19
|
import { generateTicketSynthesis } from '../utils/generateTicketSynthesis'
|
|
20
|
+
import { dbFind, dbCreate, dbDelete } from '../utils/db'
|
|
18
21
|
|
|
19
22
|
// ─── Hooks ───────────────────────────────────────────────
|
|
20
23
|
|
|
@@ -314,64 +317,6 @@ function createNotifyClientOnResolve(slugs: CollectionSlugs): CollectionAfterCha
|
|
|
314
317
|
}
|
|
315
318
|
}
|
|
316
319
|
|
|
317
|
-
function createAutoCalculateSLA(slugs: CollectionSlugs): CollectionAfterChangeHook {
|
|
318
|
-
return async ({ doc, operation, req }) => {
|
|
319
|
-
if (operation !== 'create') return doc
|
|
320
|
-
try {
|
|
321
|
-
const { payload } = req
|
|
322
|
-
const ticketPriority = doc.priority || 'normal'
|
|
323
|
-
|
|
324
|
-
// Try to find a SLA policy matching the ticket's priority first
|
|
325
|
-
let policy: any = null
|
|
326
|
-
const byPriority = await payload.find({
|
|
327
|
-
collection: slugs.slaPolicies as any,
|
|
328
|
-
where: { priority: { equals: ticketPriority } },
|
|
329
|
-
limit: 1,
|
|
330
|
-
depth: 0,
|
|
331
|
-
overrideAccess: true,
|
|
332
|
-
})
|
|
333
|
-
if (byPriority.docs.length > 0) {
|
|
334
|
-
policy = byPriority.docs[0]
|
|
335
|
-
} else {
|
|
336
|
-
// Fall back to the default policy
|
|
337
|
-
const defaults = await payload.find({
|
|
338
|
-
collection: slugs.slaPolicies as any,
|
|
339
|
-
where: { isDefault: { equals: true } },
|
|
340
|
-
limit: 1,
|
|
341
|
-
depth: 0,
|
|
342
|
-
overrideAccess: true,
|
|
343
|
-
})
|
|
344
|
-
if (defaults.docs.length > 0) {
|
|
345
|
-
policy = defaults.docs[0]
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if (!policy) return doc
|
|
350
|
-
|
|
351
|
-
// SLA fields are in minutes (firstResponseTime, resolutionTime)
|
|
352
|
-
const now = new Date()
|
|
353
|
-
const firstResponseMinutes = policy.firstResponseTime || 240 // default 4h
|
|
354
|
-
const resolutionMinutes = policy.resolutionTime || 1440 // default 24h
|
|
355
|
-
const firstResponseDue = new Date(now.getTime() + firstResponseMinutes * 60000)
|
|
356
|
-
const resolutionDue = new Date(now.getTime() + resolutionMinutes * 60000)
|
|
357
|
-
|
|
358
|
-
await payload.update({
|
|
359
|
-
collection: slugs.tickets as any,
|
|
360
|
-
id: doc.id,
|
|
361
|
-
data: {
|
|
362
|
-
slaPolicy: policy.id,
|
|
363
|
-
slaFirstResponseDue: firstResponseDue.toISOString(),
|
|
364
|
-
slaResolutionDue: resolutionDue.toISOString(),
|
|
365
|
-
},
|
|
366
|
-
overrideAccess: true,
|
|
367
|
-
})
|
|
368
|
-
} catch (err) {
|
|
369
|
-
console.error('[support] Failed to auto-calculate SLA:', err)
|
|
370
|
-
}
|
|
371
|
-
return doc
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
320
|
function createFireTicketWebhooks(slugs: CollectionSlugs): CollectionAfterChangeHook {
|
|
376
321
|
return async ({ doc, previousDoc, operation, req }) => {
|
|
377
322
|
const { payload } = req
|
|
@@ -478,8 +423,7 @@ function createCascadeDelete(slugs: CollectionSlugs): CollectionBeforeDeleteHook
|
|
|
478
423
|
return async ({ id, req }) => {
|
|
479
424
|
const collections = [slugs.ticketMessages, slugs.ticketActivityLog, slugs.timeEntries, slugs.satisfactionSurveys]
|
|
480
425
|
for (const slug of collections) {
|
|
481
|
-
await req.payload
|
|
482
|
-
collection: slug as any,
|
|
426
|
+
await dbDelete(req.payload, slug, {
|
|
483
427
|
where: { ticket: { equals: id } },
|
|
484
428
|
overrideAccess: true,
|
|
485
429
|
})
|
|
@@ -619,7 +563,7 @@ export function createTicketsCollection(slugs: CollectionSlugs, options?: {
|
|
|
619
563
|
type: 'row',
|
|
620
564
|
fields: [
|
|
621
565
|
{
|
|
622
|
-
name: 'status', type: 'select', defaultValue: 'open', label: 'Statut',
|
|
566
|
+
name: 'status', type: 'select', index: true, defaultValue: 'open', label: 'Statut',
|
|
623
567
|
options: [
|
|
624
568
|
{ label: 'Ouvert', value: 'open' },
|
|
625
569
|
{ label: 'En attente client', value: 'waiting_client' },
|
|
@@ -630,7 +574,7 @@ export function createTicketsCollection(slugs: CollectionSlugs, options?: {
|
|
|
630
574
|
admin: { width: '50%' },
|
|
631
575
|
},
|
|
632
576
|
{
|
|
633
|
-
name: 'priority', type: 'select', defaultValue: 'normal', label: 'Priorite',
|
|
577
|
+
name: 'priority', type: 'select', index: true, defaultValue: 'normal', label: 'Priorite',
|
|
634
578
|
options: [
|
|
635
579
|
{ label: 'Basse', value: 'low' },
|
|
636
580
|
{ label: 'Normale', value: 'normal' },
|
|
@@ -644,7 +588,7 @@ export function createTicketsCollection(slugs: CollectionSlugs, options?: {
|
|
|
644
588
|
{
|
|
645
589
|
type: 'row',
|
|
646
590
|
fields: [
|
|
647
|
-
{ name: 'client', type: 'relationship', relationTo: slugs.supportClients, required: true, label: 'Client', admin: { width: '50%' } },
|
|
591
|
+
{ name: 'client', type: 'relationship', relationTo: slugs.supportClients, required: true, index: true, label: 'Client', admin: { width: '50%' } },
|
|
648
592
|
],
|
|
649
593
|
},
|
|
650
594
|
// Billing collapsible
|
|
@@ -710,8 +654,8 @@ export function createTicketsCollection(slugs: CollectionSlugs, options?: {
|
|
|
710
654
|
{
|
|
711
655
|
type: 'row',
|
|
712
656
|
fields: [
|
|
713
|
-
{ name: 'slaFirstResponseDue', type: 'date', label: 'Echeance 1ere reponse (SLA)', admin: { readOnly: true, width: '50%', date: { displayFormat: 'dd/MM/yyyy HH:mm' } } },
|
|
714
|
-
{ name: 'slaResolutionDue', type: 'date', label: 'Echeance resolution (SLA)', admin: { readOnly: true, width: '50%', date: { displayFormat: 'dd/MM/yyyy HH:mm' } } },
|
|
657
|
+
{ name: 'slaFirstResponseDue', type: 'date', index: true, label: 'Echeance 1ere reponse (SLA)', admin: { readOnly: true, width: '50%', date: { displayFormat: 'dd/MM/yyyy HH:mm' } } },
|
|
658
|
+
{ name: 'slaResolutionDue', type: 'date', index: true, label: 'Echeance resolution (SLA)', admin: { readOnly: true, width: '50%', date: { displayFormat: 'dd/MM/yyyy HH:mm' } } },
|
|
715
659
|
],
|
|
716
660
|
},
|
|
717
661
|
{
|
|
@@ -719,6 +663,7 @@ export function createTicketsCollection(slugs: CollectionSlugs, options?: {
|
|
|
719
663
|
fields: [
|
|
720
664
|
{ name: 'slaFirstResponseBreached', type: 'checkbox', label: '1ere reponse depassee', admin: { readOnly: true, width: '50%' } },
|
|
721
665
|
{ name: 'slaResolutionBreached', type: 'checkbox', label: 'Resolution depassee', admin: { readOnly: true, width: '50%' } },
|
|
666
|
+
{ name: 'slaPausedAt', type: 'date', label: 'SLA en pause depuis', admin: { hidden: true } },
|
|
722
667
|
],
|
|
723
668
|
},
|
|
724
669
|
],
|
|
@@ -739,13 +684,14 @@ export function createTicketsCollection(slugs: CollectionSlugs, options?: {
|
|
|
739
684
|
},
|
|
740
685
|
{ name: 'mergedInto', type: 'relationship', relationTo: slugs.tickets, label: 'Fusionne dans', admin: { readOnly: true } },
|
|
741
686
|
{ name: 'autoCloseRemindedAt', type: 'date', label: 'Rappel auto-close envoye', admin: { readOnly: true, date: { displayFormat: 'dd/MM/yyyy HH:mm' } } },
|
|
687
|
+
{ name: 'autoCloseScheduledAt', type: 'date', index: true, label: 'Fermeture auto programmee', admin: { readOnly: true, date: { displayFormat: 'dd/MM/yyyy HH:mm' }, description: 'Echeance ferme posee par une relance manuelle. Le ticket se ferme apres cette date sans reponse client.' } },
|
|
742
688
|
],
|
|
743
689
|
},
|
|
744
690
|
// Sidebar
|
|
745
691
|
{ name: 'ticketNumber', type: 'text', unique: true, label: 'N° Ticket', admin: { position: 'sidebar' } },
|
|
746
692
|
{ name: 'slaPolicy', type: 'relationship', relationTo: slugs.slaPolicies, label: 'Politique SLA', admin: { position: 'sidebar' } },
|
|
747
693
|
{
|
|
748
|
-
name: 'category', type: 'select', label: 'Categorie',
|
|
694
|
+
name: 'category', type: 'select', index: true, label: 'Categorie',
|
|
749
695
|
options: [
|
|
750
696
|
{ label: 'Bug / Dysfonctionnement', value: 'bug' },
|
|
751
697
|
{ label: 'Modification de contenu', value: 'content' },
|
|
@@ -755,7 +701,8 @@ export function createTicketsCollection(slugs: CollectionSlugs, options?: {
|
|
|
755
701
|
],
|
|
756
702
|
admin: { position: 'sidebar' },
|
|
757
703
|
},
|
|
758
|
-
{ name: 'assignedTo', type: 'relationship', relationTo: slugs.users, label: 'Assigne a', admin: { position: 'sidebar' } },
|
|
704
|
+
{ name: 'assignedTo', type: 'relationship', relationTo: slugs.users, index: true, label: 'Assigne a', admin: { position: 'sidebar' } },
|
|
705
|
+
{ name: 'team', type: 'relationship', relationTo: slugs.supportTeams, index: true, label: 'Equipe', admin: { position: 'sidebar' } },
|
|
759
706
|
{
|
|
760
707
|
name: 'source', type: 'select', defaultValue: 'portal', label: 'Source',
|
|
761
708
|
options: [
|
|
@@ -763,6 +710,8 @@ export function createTicketsCollection(slugs: CollectionSlugs, options?: {
|
|
|
763
710
|
{ label: 'Chat en direct', value: 'live-chat' },
|
|
764
711
|
{ label: 'Email', value: 'email' },
|
|
765
712
|
{ label: 'Admin', value: 'admin' },
|
|
713
|
+
{ label: 'WhatsApp', value: 'whatsapp' },
|
|
714
|
+
{ label: 'Messenger', value: 'messenger' },
|
|
766
715
|
],
|
|
767
716
|
admin: { position: 'sidebar' },
|
|
768
717
|
},
|
|
@@ -810,8 +759,8 @@ export function createTicketsCollection(slugs: CollectionSlugs, options?: {
|
|
|
810
759
|
afterChange: [
|
|
811
760
|
createTrackSLA(slugs),
|
|
812
761
|
createTrackAiSummaryOnResolve(slugs),
|
|
813
|
-
createAutoCalculateSLA(slugs),
|
|
814
762
|
createAssignSlaDeadlines(slugs, notificationSlug),
|
|
763
|
+
createPauseSlaOnHold(slugs),
|
|
815
764
|
createCheckSlaOnResolve(slugs, notificationSlug),
|
|
816
765
|
createLogTicketActivity(slugs),
|
|
817
766
|
createNotifyOnAssignment(slugs),
|
|
@@ -820,6 +769,7 @@ export function createTicketsCollection(slugs: CollectionSlugs, options?: {
|
|
|
820
769
|
createNotifyAdminOnNewTicket(slugs, notificationSlug),
|
|
821
770
|
createFireTicketWebhooks(slugs),
|
|
822
771
|
createDispatchWebhookOnTicket(slugs),
|
|
772
|
+
createApplyAutomationRules(slugs),
|
|
823
773
|
],
|
|
824
774
|
beforeDelete: [createCascadeDelete(slugs)],
|
|
825
775
|
},
|
|
@@ -830,15 +780,24 @@ export function createTicketsCollection(slugs: CollectionSlugs, options?: {
|
|
|
830
780
|
return false
|
|
831
781
|
},
|
|
832
782
|
read: async ({ req }) => {
|
|
833
|
-
if (req.user?.collection === slugs.users)
|
|
783
|
+
if (req.user?.collection === slugs.users) {
|
|
784
|
+
if (!isTeamScopingEnabled()) return true
|
|
785
|
+
// Team scoping (opt-in): agents see their teams' tickets + team-less ones.
|
|
786
|
+
const teamIds = await resolveAgentTeamIds(req.payload, slugs, req.user.id)
|
|
787
|
+
return {
|
|
788
|
+
or: [
|
|
789
|
+
{ team: { in: teamIds.length > 0 ? teamIds : [-1] } },
|
|
790
|
+
{ team: { exists: false } },
|
|
791
|
+
],
|
|
792
|
+
} as Where
|
|
793
|
+
}
|
|
834
794
|
if (req.user?.collection === slugs.supportClients) {
|
|
835
795
|
// Owner OR collaborator (via ticket-collaborators table).
|
|
836
796
|
// We resolve the collaborator ticket-ids upfront so we keep a single Where
|
|
837
797
|
// expression — Payload's access fns support async returns.
|
|
838
798
|
let collabTicketIds: Array<number | string> = []
|
|
839
799
|
try {
|
|
840
|
-
const rows = await req.payload
|
|
841
|
-
collection: 'ticket-collaborators' as any,
|
|
800
|
+
const rows = await dbFind(req.payload, 'ticket-collaborators', {
|
|
842
801
|
where: { client: { equals: req.user.id } },
|
|
843
802
|
limit: 1000,
|
|
844
803
|
depth: 0,
|
package/src/collections/index.ts
CHANGED
|
@@ -17,3 +17,6 @@ export { createTicketStatusesCollection } from './TicketStatuses'
|
|
|
17
17
|
export { createClientSummariesCollection } from './ClientSummaries'
|
|
18
18
|
export { createTicketFeedbackCollection } from './TicketFeedback'
|
|
19
19
|
export { createTicketCollaboratorsCollection } from './TicketCollaborators'
|
|
20
|
+
export { createNotificationQueueCollection } from './NotificationQueue'
|
|
21
|
+
export { createAutomationRulesCollection } from './AutomationRules'
|
|
22
|
+
export { createSupportTeamCollection } from './SupportTeam'
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react'
|
|
4
|
+
import { REWRITE_STYLES, s } from './constants'
|
|
5
|
+
|
|
6
|
+
export const RewriteDropdown: React.FC<{
|
|
7
|
+
disabled: boolean
|
|
8
|
+
loading: boolean
|
|
9
|
+
onSelect: (style: string) => void
|
|
10
|
+
}> = ({ disabled, loading, onSelect }) => {
|
|
11
|
+
const [open, setOpen] = useState(false)
|
|
12
|
+
const ref = React.useRef<HTMLDivElement>(null)
|
|
13
|
+
|
|
14
|
+
React.useEffect(() => {
|
|
15
|
+
if (!open) return
|
|
16
|
+
const close = (e: MouseEvent) => {
|
|
17
|
+
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
|
|
18
|
+
}
|
|
19
|
+
document.addEventListener('mousedown', close)
|
|
20
|
+
return () => document.removeEventListener('mousedown', close)
|
|
21
|
+
}, [open])
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div ref={ref} style={{ position: 'relative', display: 'inline-block' }}>
|
|
25
|
+
<button
|
|
26
|
+
type="button"
|
|
27
|
+
onClick={() => setOpen(!open)}
|
|
28
|
+
disabled={disabled}
|
|
29
|
+
style={{
|
|
30
|
+
...s.outlineBtn('#0891b2', disabled),
|
|
31
|
+
fontSize: '11px',
|
|
32
|
+
padding: '3px 10px',
|
|
33
|
+
borderRadius: '14px',
|
|
34
|
+
display: 'flex',
|
|
35
|
+
alignItems: 'center',
|
|
36
|
+
gap: '4px',
|
|
37
|
+
}}
|
|
38
|
+
>
|
|
39
|
+
{loading ? 'Reformulation...' : '✏️ Reformuler'}
|
|
40
|
+
{!loading && <span style={{ fontSize: 9, opacity: 0.7 }}>▼</span>}
|
|
41
|
+
</button>
|
|
42
|
+
{open && !disabled && !loading && (
|
|
43
|
+
<div
|
|
44
|
+
style={{
|
|
45
|
+
position: 'absolute',
|
|
46
|
+
bottom: '100%',
|
|
47
|
+
left: 0,
|
|
48
|
+
marginBottom: 4,
|
|
49
|
+
background: 'var(--theme-elevation-0, #fff)',
|
|
50
|
+
border: '1px solid var(--theme-elevation-200, #e5e7eb)',
|
|
51
|
+
borderRadius: 8,
|
|
52
|
+
boxShadow: '0 4px 16px rgba(0,0,0,0.12)',
|
|
53
|
+
zIndex: 100,
|
|
54
|
+
minWidth: 180,
|
|
55
|
+
overflow: 'hidden',
|
|
56
|
+
}}
|
|
57
|
+
>
|
|
58
|
+
{REWRITE_STYLES.map((style) => (
|
|
59
|
+
<button
|
|
60
|
+
key={style.id}
|
|
61
|
+
type="button"
|
|
62
|
+
onClick={() => { setOpen(false); onSelect(style.id) }}
|
|
63
|
+
style={{
|
|
64
|
+
display: 'flex',
|
|
65
|
+
flexDirection: 'column',
|
|
66
|
+
width: '100%',
|
|
67
|
+
padding: '8px 12px',
|
|
68
|
+
border: 'none',
|
|
69
|
+
background: 'transparent',
|
|
70
|
+
cursor: 'pointer',
|
|
71
|
+
textAlign: 'left',
|
|
72
|
+
borderBottom: '1px solid var(--theme-elevation-100, #f3f4f6)',
|
|
73
|
+
transition: 'background 120ms',
|
|
74
|
+
}}
|
|
75
|
+
onMouseEnter={(e) => { (e.target as HTMLElement).style.background = 'var(--theme-elevation-50, #f9fafb)' }}
|
|
76
|
+
onMouseLeave={(e) => { (e.target as HTMLElement).style.background = 'transparent' }}
|
|
77
|
+
>
|
|
78
|
+
<span style={{ fontSize: 12, fontWeight: 600, color: 'var(--theme-text, #111)' }}>{style.label}</span>
|
|
79
|
+
<span style={{ fontSize: 10, color: 'var(--theme-elevation-500, #6b7280)' }}>{style.desc}</span>
|
|
80
|
+
</button>
|
|
81
|
+
))}
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React from 'react'
|
|
4
|
+
|
|
5
|
+
// Inline skeleton replacement (no external dependency)
|
|
6
|
+
export function SkeletonText({ lines = 3 }: { lines?: number }) {
|
|
7
|
+
return (
|
|
8
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', padding: '12px 0' }}>
|
|
9
|
+
{Array.from({ length: lines }).map((_, i) => (
|
|
10
|
+
<div
|
|
11
|
+
key={i}
|
|
12
|
+
style={{
|
|
13
|
+
height: '14px',
|
|
14
|
+
borderRadius: '4px',
|
|
15
|
+
backgroundColor: '#e2e8f0',
|
|
16
|
+
width: i === lines - 1 ? '60%' : '100%',
|
|
17
|
+
animation: 'pulse 1.5s ease-in-out infinite',
|
|
18
|
+
}}
|
|
19
|
+
/>
|
|
20
|
+
))}
|
|
21
|
+
</div>
|
|
22
|
+
)
|
|
23
|
+
}
|