@consilioweb/payload-support 5.0.0 → 6.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 +249 -22
- package/dist/components/RichTextEditor/index.js +1 -1
- package/dist/components/TicketConversation/RewriteDropdown.js +1 -1
- package/dist/components/TicketConversation/components/AISummaryPanel.js +3 -0
- package/dist/components/TicketConversation/components/ActionPanels.js +13 -8
- package/dist/components/TicketConversation/components/ClientHistory.js +1 -0
- package/dist/components/TicketConversation/components/CodeBlock.js +1 -0
- package/dist/components/TicketConversation/components/CodeBlockInserter.js +1 -1
- package/dist/components/TicketConversation/components/QuickActions.js +10 -8
- package/dist/components/TicketConversation/components/TicketHeader.js +2 -0
- package/dist/components/TicketConversation/components/TimeTrackingPanel.js +15 -9
- package/dist/components/TicketConversation/index.d.ts +10 -0
- package/dist/components/TicketConversation/index.js +25 -7
- package/dist/index.cjs +306 -60
- package/dist/index.d.cts +107 -38
- package/dist/index.d.ts +107 -38
- package/dist/index.js +300 -59
- package/dist/styles/BillingView.module.scss +11 -11
- package/dist/styles/ChatView.module.scss +7 -7
- package/dist/styles/CommandPalette.module.scss +1 -1
- package/dist/styles/CrmView.module.scss +9 -9
- package/dist/styles/EmailTracking.module.scss +6 -6
- package/dist/styles/ImportConversation.module.scss +5 -5
- package/dist/styles/Logs.module.scss +5 -5
- package/dist/styles/NewTicket.module.scss +2 -2
- package/dist/styles/PendingEmails.module.scss +13 -13
- package/dist/styles/SupportDashboard.module.scss +24 -8
- package/dist/styles/TicketDetail.module.scss +20 -20
- package/dist/styles/TicketInbox.module.scss +6 -6
- package/dist/styles/TicketingSettings.module.scss +10 -10
- package/dist/styles/TimeDashboard.module.scss +6 -6
- package/dist/styles/_tokens.scss +3 -1
- package/dist/views/BillingView/client.js +15 -8
- package/dist/views/ChatView/client.js +2 -0
- package/dist/views/CrmView/client.js +2 -0
- package/dist/views/EmailTrackingView/client.js +23 -11
- package/dist/views/ImportConversationView/client.js +1 -0
- package/dist/views/LogsView/client.js +1 -1
- package/dist/views/NewTicketView/client.js +20 -13
- package/dist/views/PendingEmailsView/client.js +9 -3
- package/dist/views/SupportDashboardView/client.js +17 -19
- package/dist/views/TicketDetailView/client.js +23 -11
- package/dist/views/TicketInboxView/client.js +52 -26
- package/dist/views/TicketingSettingsView/TicketingSettings.module.scss +7 -7
- package/dist/views/TicketingSettingsView/client.js +44 -26
- package/dist/views/TimeDashboardView/client.js +10 -7
- package/dist/views/shared/ErrorBoundary.d.ts +36 -1
- package/dist/views/shared/ErrorBoundary.js +59 -28
- package/dist/views/shared/adminTokens.d.ts +15 -5
- package/dist/views/shared/adminTokens.js +23 -7
- package/dist/views/shared/icons.d.ts +49 -0
- package/dist/views/shared/icons.js +92 -0
- package/dist/views/shared/locales/en.json +15 -1
- package/dist/views/shared/locales/fr.json +15 -1
- package/package.json +6 -2
- package/scripts/uninstall-data.mjs +178 -0
- package/scripts/uninstall.mjs +184 -0
- package/src/collections/Tickets.ts +7 -0
- package/src/collections/TimeEntries.ts +11 -3
- package/src/components/RichTextEditor/index.tsx +1 -1
- package/src/components/TicketConversation/RewriteDropdown.tsx +1 -1
- package/src/components/TicketConversation/components/AISummaryPanel.tsx +3 -0
- package/src/components/TicketConversation/components/ActionPanels.tsx +13 -8
- package/src/components/TicketConversation/components/ClientHistory.tsx +1 -0
- package/src/components/TicketConversation/components/CodeBlock.tsx +1 -0
- package/src/components/TicketConversation/components/CodeBlockInserter.tsx +1 -1
- package/src/components/TicketConversation/components/QuickActions.tsx +10 -8
- package/src/components/TicketConversation/components/TicketHeader.tsx +2 -0
- package/src/components/TicketConversation/components/TimeTrackingPanel.tsx +17 -9
- package/src/components/TicketConversation/index.tsx +41 -7
- package/src/endpoints/delete-account.ts +191 -47
- package/src/endpoints/export-data.ts +104 -4
- package/src/endpoints/purge-logs.ts +6 -15
- package/src/index.ts +2 -0
- package/src/plugin.ts +51 -1
- package/src/portal/LiveChat.tsx +1 -1
- package/src/portal/auth/profile/page.tsx +3 -2
- package/src/portal/icons.tsx +91 -0
- package/src/styles/BillingView.module.scss +11 -11
- package/src/styles/ChatView.module.scss +7 -7
- package/src/styles/CommandPalette.module.scss +1 -1
- package/src/styles/CrmView.module.scss +9 -9
- package/src/styles/EmailTracking.module.scss +6 -6
- package/src/styles/ImportConversation.module.scss +5 -5
- package/src/styles/Logs.module.scss +5 -5
- package/src/styles/NewTicket.module.scss +2 -2
- package/src/styles/PendingEmails.module.scss +13 -13
- package/src/styles/SupportDashboard.module.scss +24 -8
- package/src/styles/TicketDetail.module.scss +20 -20
- package/src/styles/TicketInbox.module.scss +6 -6
- package/src/styles/TicketingSettings.module.scss +10 -10
- package/src/styles/TimeDashboard.module.scss +6 -6
- package/src/styles/_tokens.scss +3 -1
- package/src/types.ts +20 -0
- package/src/utils/retention.ts +111 -0
- package/src/utils/slugs.ts +15 -0
- package/src/views/BillingView/client.tsx +17 -8
- package/src/views/ChatView/client.tsx +2 -0
- package/src/views/CrmView/client.tsx +2 -0
- package/src/views/EmailTrackingView/client.tsx +18 -7
- package/src/views/ImportConversationView/client.tsx +1 -0
- package/src/views/LogsView/client.tsx +1 -1
- package/src/views/NewTicketView/client.tsx +22 -13
- package/src/views/PendingEmailsView/client.tsx +9 -3
- package/src/views/SupportDashboardView/client.tsx +11 -5
- package/src/views/TicketDetailView/client.tsx +25 -11
- package/src/views/TicketInboxView/client.tsx +47 -7
- package/src/views/TicketingSettingsView/TicketingSettings.module.scss +7 -7
- package/src/views/TicketingSettingsView/client.tsx +201 -144
- package/src/views/TimeDashboardView/client.tsx +10 -7
- package/src/views/shared/ErrorBoundary.tsx +95 -31
- package/src/views/shared/adminTokens.ts +28 -11
- package/src/views/shared/icons.tsx +146 -0
- package/src/views/shared/locales/en.json +15 -1
- package/src/views/shared/locales/fr.json +15 -1
- package/src/types/lucide-react.d.ts +0 -42
package/README.md
CHANGED
|
@@ -26,6 +26,7 @@ ships as a template you copy into your own `app/` directory — see
|
|
|
26
26
|
- [Quick Start](#quick-start)
|
|
27
27
|
- [Mounting the client portal](#mounting-the-client-portal)
|
|
28
28
|
- [Configuration](#configuration)
|
|
29
|
+
- [Database and updates](#database-and-updates)
|
|
29
30
|
- [Deployment adapters](#deployment-adapters)
|
|
30
31
|
- [API Endpoints](#api-endpoints)
|
|
31
32
|
- [Webhooks](#webhooks)
|
|
@@ -34,6 +35,7 @@ ships as a template you copy into your own `app/` directory — see
|
|
|
34
35
|
- [Requirements](#requirements)
|
|
35
36
|
- [Security](#security)
|
|
36
37
|
- [Performance](#performance)
|
|
38
|
+
- [Personal data, retention and uninstall](#personal-data-retention-and-uninstall)
|
|
37
39
|
- [Troubleshooting](#troubleshooting)
|
|
38
40
|
- [FAQ](#faq)
|
|
39
41
|
- [Upgrading](#upgrading)
|
|
@@ -78,10 +80,10 @@ pnpm add @consilioweb/payload-support
|
|
|
78
80
|
# or: yarn add @consilioweb/payload-support
|
|
79
81
|
```
|
|
80
82
|
|
|
81
|
-
**Peer dependencies** — all
|
|
83
|
+
**Peer dependencies** — all five are required, none is optional:
|
|
82
84
|
|
|
83
85
|
```bash
|
|
84
|
-
pnpm add payload@^3.79.1 @payloadcms/next@^3.79.1 next@^15.2.9 react@^19 react-dom@^19
|
|
86
|
+
pnpm add payload@^3.79.1 @payloadcms/next@^3.79.1 next@^15.2.9 react@^19 react-dom@^19
|
|
85
87
|
```
|
|
86
88
|
|
|
87
89
|
- `payload@^3.79.1` — the floor is a security floor, and it is stated as such rather than
|
|
@@ -96,9 +98,10 @@ pnpm add payload@^3.79.1 @payloadcms/next@^3.79.1 next@^15.2.9 react@^19 react-d
|
|
|
96
98
|
- `@payloadcms/next` and `next` — the 13 admin views are registered unless you pass
|
|
97
99
|
`skipViews: true`, and each imports `DefaultTemplate` from `@payloadcms/next/templates` plus
|
|
98
100
|
`next/navigation` / `next/link` statically. Both are already present in any Payload 3 admin app.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
101
|
+
`lucide-react` is **no longer a peer dependency**. The 19 icons the admin views and the portal
|
|
102
|
+
widget used are inlined as SVG in `src/views/shared/icons.tsx` and `src/portal/icons.tsx`, under
|
|
103
|
+
Lucide's ISC licence. Nothing to install; if your app already uses `lucide-react` for its own UI,
|
|
104
|
+
it is unaffected.
|
|
102
105
|
|
|
103
106
|
**AI runtime dependency** — the AI features load `@anthropic-ai/sdk` through a runtime
|
|
104
107
|
`require`/dynamic `import` and it is marked `external` at build time. It is intentionally not a
|
|
@@ -149,12 +152,13 @@ provider, the sender addresses, the SLA targets and the runtime feature flags.
|
|
|
149
152
|
## Mounting the client portal
|
|
150
153
|
|
|
151
154
|
The portal ships as a **template you copy**, not as an importable subpath — and that is a
|
|
152
|
-
constraint, not an oversight: `src/portal/auth/layout.tsx` and
|
|
155
|
+
constraint, not an oversight: `src/portal/auth/layout.tsx` and four of its pages import
|
|
153
156
|
`@payload-config`, an alias that only exists inside your application, and `src/portal/layout.tsx`
|
|
154
|
-
renders its own `<html>`/`<body>` as a Next.js **root layout**. Neither can be
|
|
155
|
-
`node_modules`, so there is no `./portal` export and `plugin.ts` registers no Next
|
|
157
|
+
renders its own `<html>`/`<body>` (lines 13 and 24) as a Next.js **root layout**. Neither can be
|
|
158
|
+
resolved from `node_modules`, so there is no `./portal` export and `plugin.ts` registers no Next
|
|
159
|
+
route — adding one would break at import time in every host app.
|
|
156
160
|
|
|
157
|
-
The sources are published in the tarball (`files` includes `src
|
|
161
|
+
The sources are published in the tarball (`files` includes `src`, 36 files), so copy them out of
|
|
158
162
|
`node_modules` into a route group of your own `app/` directory. Two directories have to be
|
|
159
163
|
renamed on the way in — the shipped names are plain folders, not Next.js route syntax — and
|
|
160
164
|
`layout.tsx` has to sit at the **group** level, because it renders its own `<html>`/`<body>`
|
|
@@ -196,14 +200,13 @@ What your app must provide for those pages to work:
|
|
|
196
200
|
| Requirement | Why |
|
|
197
201
|
|---|---|
|
|
198
202
|
| The `@payload-config` alias | The authenticated pages call `getPayload({ config })` directly. Standard in a `create-payload-app` project. |
|
|
199
|
-
| **Tailwind CSS** |
|
|
203
|
+
| **Tailwind CSS** | 29 of the 32 portal components are styled with Tailwind utility classes only. Without Tailwind the portal renders unstyled. |
|
|
200
204
|
| `@payloadcms/richtext-lexical` | `auth/faq/page.tsx` renders knowledge-base entries with `RichText` from `@payloadcms/richtext-lexical/react`. It is **not** a peer dependency of this plugin — add it yourself if you mount the FAQ page. |
|
|
201
205
|
| `NEXT_PUBLIC_SERVER_URL` | Email and portal links are built from it. |
|
|
202
206
|
| `NEXT_PUBLIC_SUPPORT_PHONE` | The ticket detail page prints a support phone number; without this variable it prints the placeholder `01 23 45 67 89` to your clients. |
|
|
203
207
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
for the admin views.
|
|
208
|
+
No icon library is needed: `src/portal/icons.tsx` ships the five SVG icons `LiveChat.tsx` uses, and
|
|
209
|
+
it is copied along with the rest of the portal.
|
|
207
210
|
|
|
208
211
|
The `(auth)` layout mounts two widgets. `ChatbotWidget` is always rendered; `ChatWidget` — the
|
|
209
212
|
floating live-chat FAB — returns `null` unless the host app sets
|
|
@@ -221,6 +224,7 @@ plugin upgrades do not update it. Diff it against
|
|
|
221
224
|
|---|---|---|---|
|
|
222
225
|
| `features` | `SupportFeatures` | see below | Build-time feature flags — decide which collections, endpoints and views exist. |
|
|
223
226
|
| `rateLimitStore` | `RateLimitStore \| 'payload'` | in-memory | Shared storage for the endpoint rate limits. `'payload'` adds the `support-rate-limits` collection. |
|
|
227
|
+
| `retention` | `{ authLogsDays?, emailLogsDays?, cron?, queue? } \| false` | **not set — no task is registered** | Opt-in automatic purge of the two journals, run as a Payload Jobs task. Pass `{}` to accept the defaults (`180` / `365` days, `0 30 3 * * *`, queue `default`). `0` on either value disables that journal's purge. Left unset, or `false`, registers nothing. See [Personal data, retention and uninstall](#personal-data-retention-and-uninstall). |
|
|
224
228
|
| `ticketNumber` | `{ prefix?, padding? }` | `{ prefix: 'TK-', padding: 4 }` | Sequential ticket-number formatting, backed by an atomic counter. |
|
|
225
229
|
| `capabilities` | `SupportCapabilities` | — | Host adapters for SMS, inbound email, digests, AI titles/summaries, detailed billing, volunteering, thread cleanup and project suggestions. See [Deployment adapters](#deployment-adapters). |
|
|
226
230
|
| `basePath` | `string` | `'/support'` | Prefix of the admin view routes (`/admin<basePath>/inbox`, …). The conversation-import view is the exception: it is registered at the fixed `/admin/import-conversation`. |
|
|
@@ -361,6 +365,46 @@ of conditions (field, `equals` / `not_equals` / `contains`, value) and a list of
|
|
|
361
365
|
(`set_status`, `set_priority`, `set_category`, `assign`, `add_tag`). For example: trigger
|
|
362
366
|
`ticket_created`, condition `category equals bug`, action `set_priority = urgent`.
|
|
363
367
|
|
|
368
|
+
## Database and updates
|
|
369
|
+
|
|
370
|
+
- This plugin **adds collections to your Payload config; it does not own your schema**. Your app
|
|
371
|
+
does, and it is your app that generates and runs the migrations.
|
|
372
|
+
- Payload gives a plugin no way to ship them. `payload migrate` reads exactly one directory —
|
|
373
|
+
`payload.db.migrationDir`, resolved from the **host application's** cwd
|
|
374
|
+
(`payload/dist/database/migrations/readMigrationFiles.js` and `findMigrationDir.js`). A migration
|
|
375
|
+
file published inside a package is never discovered; it is dead code.
|
|
376
|
+
- **In development**, `push` synchronises the schema on its own. Nothing else to do.
|
|
377
|
+
- **In production**, run `payload migrate:create` then `payload migrate` — or set `prodMigrations`
|
|
378
|
+
on the adapter. Never `push`: the adapter skips it as soon as `NODE_ENV=production`, and mixing a
|
|
379
|
+
dev-pushed database with migrations makes `@payloadcms/drizzle` warn about **data loss**.
|
|
380
|
+
- Every release of this plugin states in its [Upgrading](#upgrading) section whether it changes the
|
|
381
|
+
schema. Between 2.0.1 and 5.0.0, none did. The automatic log purge introduced after 5.0.0 does —
|
|
382
|
+
see the table below.
|
|
383
|
+
|
|
384
|
+
### Options that change the schema
|
|
385
|
+
|
|
386
|
+
Two families of options decide which tables exist. Flipping one on a deployed application is a
|
|
387
|
+
schema change like any other: generate a migration and run it, in the same deploy.
|
|
388
|
+
|
|
389
|
+
| Option | Collection it adds or removes |
|
|
390
|
+
|---|---|
|
|
391
|
+
| `retention` (when you pass it) | Payload's own `payload-jobs` collection and `payload-jobs-stats` global — **only if your app had no jobs at all**. Declaring a task is what turns Payload's queue on, and that is why the option is opt-in: a support plugin has no business adding two tables to every install. Leave it unset and purge from your own cron against the admin endpoint instead. |
|
|
392
|
+
| `rateLimitStore: 'payload'` | `support-rate-limits` |
|
|
393
|
+
| `features.authLogs` | `auth-logs` |
|
|
394
|
+
| `features.timeTracking` | `time-entries` |
|
|
395
|
+
| `features.emailTracking` | `email-logs` |
|
|
396
|
+
| `features.webhooks` | `webhook-endpoints` |
|
|
397
|
+
| `features.sla` | `sla-policies` |
|
|
398
|
+
| `features.macros` | `macros` |
|
|
399
|
+
| `features.customStatuses` | `ticket-statuses` |
|
|
400
|
+
| `features.chat` | `chat-messages` |
|
|
401
|
+
| `features.pendingEmails` | `pending-emails` |
|
|
402
|
+
| `features.ai` | `client-summaries` |
|
|
403
|
+
|
|
404
|
+
Turning a flag **off** does not drop the table; the collection simply stops being registered, and
|
|
405
|
+
its rows become unreachable through Payload while still occupying the database. Drop it explicitly
|
|
406
|
+
if that is what you want — see [Uninstall](#uninstall).
|
|
407
|
+
|
|
364
408
|
## Deployment adapters
|
|
365
409
|
|
|
366
410
|
The plugin owns the generic support workflow; anything provider-specific stays in your application
|
|
@@ -624,7 +668,6 @@ import type { SupportPluginConfig, SupportFeatures } from '@consilioweb/payload-
|
|
|
624
668
|
| `@payloadcms/next` | `^3.79.1` | `peerDependencies` |
|
|
625
669
|
| Next.js | `^15.2.9 \|\| ^16.0.0` | `peerDependencies` |
|
|
626
670
|
| React / React DOM | `^19.0.0` | `peerDependencies` |
|
|
627
|
-
| `lucide-react` | `>=0.300.0` | `peerDependencies` |
|
|
628
671
|
| `@anthropic-ai/sdk` | any recent version | Runtime-only, install it yourself if you use the AI features |
|
|
629
672
|
| `@payloadcms/richtext-lexical` | any 3.x | Only if you mount the portal FAQ page |
|
|
630
673
|
|
|
@@ -697,6 +740,106 @@ does:
|
|
|
697
740
|
| Client time totals | The CRM sums the per-ticket `totalTimeMinutes` rollup instead of scanning `time-entries`. |
|
|
698
741
|
| AI synthesis | Persisted on the ticket (`aiSummary`, `aiSummaryGeneratedAt`) and reused until forced. |
|
|
699
742
|
|
|
743
|
+
## Personal data, retention and uninstall
|
|
744
|
+
|
|
745
|
+
This plugin is a support desk: most of what it stores is personal data, and you are the controller
|
|
746
|
+
of it. This section is what you need to fill in your record of processing activities.
|
|
747
|
+
|
|
748
|
+
### What the plugin stores
|
|
749
|
+
|
|
750
|
+
It registers **up to 25 collections** (14 always, 11 behind a feature flag — see
|
|
751
|
+
[Options that change the schema](#options-that-change-the-schema)). The ones holding personal data:
|
|
752
|
+
|
|
753
|
+
| Collection | Personal data it holds |
|
|
754
|
+
|---|---|
|
|
755
|
+
| `support-clients` | Identity, email, phone, company, hashed password, Google id, 2FA state, notification preferences |
|
|
756
|
+
| `tickets` | Subject and description written by the client, assignment, billing amounts |
|
|
757
|
+
| `ticket-messages` | Full conversation, internal notes, attachments, sender addresses |
|
|
758
|
+
| `ticket-collaborators` | Email of every invited person, whether or not they have an account |
|
|
759
|
+
| `ticket-activity-log` | Who did what and when, on every ticket |
|
|
760
|
+
| `satisfaction-surveys`, `ticket-feedback` | Rating and free-text comment, attributed |
|
|
761
|
+
| `chat-messages` | Live-chat transcripts |
|
|
762
|
+
| `pending-emails` | Raw inbound emails, sender address, attachments |
|
|
763
|
+
| `auth-logs` | Login attempts: email, normalized IP, user agent |
|
|
764
|
+
| `email-logs` | Sender and recipient addresses, subjects, delivery and open events |
|
|
765
|
+
| `client-summaries` | **Derived** data: an AI profile of the client, computed from their history |
|
|
766
|
+
| `push-subscriptions` | Agent browser endpoints and user agent |
|
|
767
|
+
| `time-entries` | Which agent spent how long on what |
|
|
768
|
+
| `notification-queue` | Pending notifications addressed to a client |
|
|
769
|
+
|
|
770
|
+
Two things live **outside** those collections:
|
|
771
|
+
|
|
772
|
+
- **Attachments are stored in your own `media` collection**, because that is the `relationTo` of the
|
|
773
|
+
attachment fields on `ticket-messages` and `pending-emails`. They are not the plugin's to manage,
|
|
774
|
+
but `POST /support/delete-account` does delete the files attached to a deleted client's tickets.
|
|
775
|
+
- **Three `payload-preferences` keys** are written by the plugin: `support-settings` (instance
|
|
776
|
+
settings), `support-user-prefs-<userId>` (one row per agent) and `support-round-robin` (assignment
|
|
777
|
+
cursor). None of them holds client data, but the per-agent key carries a staff account id.
|
|
778
|
+
|
|
779
|
+
### Retention
|
|
780
|
+
|
|
781
|
+
Only the two journals are purged automatically. Everything else lives as long as the ticket it
|
|
782
|
+
belongs to, and is deleted when the client account is.
|
|
783
|
+
|
|
784
|
+
| Data | Default | How to change it |
|
|
785
|
+
|---|---|---|
|
|
786
|
+
| `auth-logs` | **180 days** | `retention.authLogsDays`. CNIL guidance accepts six months for connection logs; do not shorten it "to be safe" — these are the records you investigate an intrusion with. |
|
|
787
|
+
| `email-logs` | **365 days** | `retention.emailLogsDays` |
|
|
788
|
+
| Everything else | Until the ticket or the account is deleted | `POST /support/delete-account` |
|
|
789
|
+
|
|
790
|
+
**The purge is opt-in: nothing is registered until you pass `retention`.** Pass `retention: {}` to
|
|
791
|
+
accept the defaults, or override any of the four keys. Once enabled it runs as a Payload Jobs task
|
|
792
|
+
(`support-purge-logs`), by default daily at 03:30, so it survives a serverless deploy and does not
|
|
793
|
+
run N times in parallel behind N instances. It refuses a retention of `0` — that value disables the
|
|
794
|
+
journal's purge. The manual, admin-only
|
|
795
|
+
`DELETE /api/support/purge-logs?collection=auth-logs&days=0` still wipes a journal entirely; that
|
|
796
|
+
one is a deliberate one-off action, never a schedule.
|
|
797
|
+
|
|
798
|
+
Why opt-in, when a retention policy nobody enables is exactly how the manual purge endpoint ended
|
|
799
|
+
up never being called: declaring the task **enables Payload's job queue**, which adds the
|
|
800
|
+
`payload-jobs` collection and the `payload-jobs-stats` global — two tables your app may not have
|
|
801
|
+
had. A support plugin should not impose a schema change on every install, and defaulting it on
|
|
802
|
+
would buy nothing anyway, since the task only fires when you also run a job runner. If you enable
|
|
803
|
+
it, generate a migration for those two. If you would rather not, leave `retention` unset and call
|
|
804
|
+
the purge endpoint from your own cron — the retention periods below still describe what you should
|
|
805
|
+
be deleting.
|
|
806
|
+
|
|
807
|
+
Payload's job runner has to be running for the task to fire — `jobs.autoRun` in your config, or a
|
|
808
|
+
cron hitting `/api/payload-jobs/run`. If you do not run it, nothing is purged and the endpoint is
|
|
809
|
+
your only lever.
|
|
810
|
+
|
|
811
|
+
### Cookies and trackers
|
|
812
|
+
|
|
813
|
+
`features.emailTracking` is **on by default** and inserts a 1×1 open-tracking pixel in outgoing
|
|
814
|
+
notification emails (`GET /support/track-open`). Under article 82 of the French *Loi Informatique
|
|
815
|
+
et Libertés* that is a tracker and needs consent unless it falls in an exemption; it is your call,
|
|
816
|
+
not the plugin's. It writes no IP address and requires a valid HMAC, but it does record that a
|
|
817
|
+
given recipient opened a given email, at a given time. Set `features: { emailTracking: false }` to
|
|
818
|
+
turn the pixel — and the `email-logs` collection — off entirely.
|
|
819
|
+
|
|
820
|
+
### Data subject requests
|
|
821
|
+
|
|
822
|
+
| Right | Endpoint |
|
|
823
|
+
|---|---|
|
|
824
|
+
| Access (art. 15) and portability (art. 20) | `GET /support/export-data`. The JSON is split in two: `providedByYou` (art. 20) and `derivedData` (art. 15 — the AI client summary, which portability does not cover). |
|
|
825
|
+
| Erasure (art. 17) | `POST /support/delete-account`. Deletes the account and every row above that names it, plus the attachments on its tickets, in a single transaction. |
|
|
826
|
+
|
|
827
|
+
### Uninstall
|
|
828
|
+
|
|
829
|
+
Removing the plugin from `payload.config.ts` stops the collections from being registered; it does
|
|
830
|
+
**not** drop their tables, and the rows stay in the database.
|
|
831
|
+
|
|
832
|
+
```bash
|
|
833
|
+
npx support-uninstall # dry run: lists what would be deleted
|
|
834
|
+
npx support-uninstall --confirm # deletes the rows through the Payload local API
|
|
835
|
+
npx support-uninstall --confirm --keep-data # only removes the payload-preferences keys
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
The script deletes rows, not tables: run `payload migrate:create` after it and apply the migration
|
|
839
|
+
to drop the now-unused tables. It never touches your `media` collection — attachments uploaded
|
|
840
|
+
through the support desk are indistinguishable from your other uploads once the messages are gone,
|
|
841
|
+
so it lists them as a manual decision instead of guessing.
|
|
842
|
+
|
|
700
843
|
## Troubleshooting
|
|
701
844
|
|
|
702
845
|
**`useServerFunctions must be used within ServerFunctionsProvider`** — align every `@payloadcms/*`
|
|
@@ -708,10 +851,12 @@ inserts) and add `busyTimeout: 10000` to the SQLite adapter.
|
|
|
708
851
|
**Admin views do not load** — regenerate the import map (`pnpm payload generate:importmap`). In a
|
|
709
852
|
headless context, pass `skipViews: true`.
|
|
710
853
|
|
|
711
|
-
**New fields or collections missing in production** —
|
|
712
|
-
|
|
713
|
-
`
|
|
714
|
-
`
|
|
854
|
+
**New fields or collections missing in production** — a production build never synchronises the
|
|
855
|
+
schema: the adapter skips `push` as soon as `NODE_ENV=production`. Generate a migration in
|
|
856
|
+
development (`payload migrate:create`) and run `payload migrate` on the target, or set
|
|
857
|
+
`prodMigrations` on the adapter. Do **not** try to `push` in production, and do not migrate a
|
|
858
|
+
database that was previously dev-pushed without reading the data-loss warning
|
|
859
|
+
`@payloadcms/drizzle` prints. See [Database and updates](#database-and-updates).
|
|
715
860
|
|
|
716
861
|
**`Cannot find module '@anthropic-ai/sdk'`** — install it in your app; the plugin does not bundle or
|
|
717
862
|
declare it.
|
|
@@ -733,11 +878,84 @@ type but not implemented: at runtime everything that is not `ollama` goes to Ant
|
|
|
733
878
|
(`SupportPluginConfig`, `SupportFeatures`, `TicketData`, …). All four export subpaths ship
|
|
734
879
|
declarations.
|
|
735
880
|
|
|
736
|
-
**How do I migrate the schema in production?** —
|
|
737
|
-
|
|
881
|
+
**How do I migrate the schema in production?** — `payload migrate:create` in development, commit
|
|
882
|
+
the generated file, then `payload migrate` on the target (or `prodMigrations` on the adapter).
|
|
883
|
+
`push` is a development mechanism and is skipped in production. This plugin ships no migrations of
|
|
884
|
+
its own and cannot: `payload migrate` only ever reads your application's migration directory. See
|
|
885
|
+
[Database and updates](#database-and-updates).
|
|
738
886
|
|
|
739
887
|
## Upgrading
|
|
740
888
|
|
|
889
|
+
### 5.x → 6.0
|
|
890
|
+
|
|
891
|
+
Two changes need an action; everything else in 6.0 is additive.
|
|
892
|
+
|
|
893
|
+
**`GET /support/export-data` changed shape.** `profile`, `tickets`, `messages` and `surveys` now
|
|
894
|
+
sit under `providedByYou`, and a `derivedData` section carries what the service inferred rather
|
|
895
|
+
than what the person supplied — the distinction GDPR article 20 (portability, provided data) and
|
|
896
|
+
article 15 (access, all data) actually make. If anything of yours parses that response, read the
|
|
897
|
+
new keys. The endpoint is the only one whose shape moved.
|
|
898
|
+
|
|
899
|
+
**`lucide-react` is no longer a peer dependency.** The twenty icons are inlined, so the package no
|
|
900
|
+
longer requires you to install an icon library. Nothing breaks if you keep it — it is simply not
|
|
901
|
+
requested any more. Remove it from your own dependencies only if nothing else of yours uses it.
|
|
902
|
+
|
|
903
|
+
Two smaller notes. The exported `V` colour tokens changed value: `amber`, `orange`, `green` (and
|
|
904
|
+
the `yellow` alias) are darker, and `textSecondary` moved off `--theme-elevation-500`, which failed
|
|
905
|
+
WCAG AA as a foreground. If you render your own UI with `V`, expect a visual shift, not a break.
|
|
906
|
+
And the log retention purge is **opt-in**: it registers nothing unless you pass `retention`. See
|
|
907
|
+
[Personal data, retention and uninstall](#personal-data-retention-and-uninstall) for why, and for
|
|
908
|
+
what to run instead if you would rather purge from your own cron.
|
|
909
|
+
|
|
910
|
+
### 4.x → 5.0
|
|
911
|
+
|
|
912
|
+
**No schema change: this release only touches access rules and hooks. Do not generate a
|
|
913
|
+
migration.** The field set of 5.0.0 is identical to 4.0.0's.
|
|
914
|
+
|
|
915
|
+
Data actions to take:
|
|
916
|
+
|
|
917
|
+
1. **Audit your access logs for `<adminRoute>/support/*` hits from accounts that are neither agents
|
|
918
|
+
nor `support-clients`.** Until 5.0.0 the thirteen admin views only tested `!req.user`, so any
|
|
919
|
+
authenticated account of any auth collection reached the admin chrome and the client config
|
|
920
|
+
Payload builds for an authenticated request. The support data itself never travelled — the views
|
|
921
|
+
fetch through `/api/support/*`, which was guarded — but the shape of your CMS did.
|
|
922
|
+
2. **Re-subscribe any agent whose Web Push endpoint is not `https://` on a public host.**
|
|
923
|
+
`POST /support/push/subscribe` now answers `400` for those, and `sendPushToUser` skips rows
|
|
924
|
+
already stored that fail the same check. Agents who subscribed through a local tunnel or an
|
|
925
|
+
`http://` origin stop receiving notifications until they subscribe again.
|
|
926
|
+
3. **Expect the rate-limit counters to restart once.** Keys are now normalized IP literals, so rows
|
|
927
|
+
already in `support-rate-limits` no longer match. This is a one-off reset, not a data loss.
|
|
928
|
+
4. **A custom portal must forward `challenge`** from every `POST /support/2fa {action:'send'}`
|
|
929
|
+
reply to the following `verify`; the bundled portal already does.
|
|
930
|
+
|
|
931
|
+
### 3.x → 4.0
|
|
932
|
+
|
|
933
|
+
**No schema change: this release only touches access rules and hooks. Do not generate a
|
|
934
|
+
migration.** The field set of 4.0.0 is identical to 3.0.0's.
|
|
935
|
+
|
|
936
|
+
Data actions to take:
|
|
937
|
+
|
|
938
|
+
1. **Audit every `webhook-endpoints` row and re-point the ones that are not `https://` on a public
|
|
939
|
+
host.** 4.0.0 validates the URL at save time only, so a row saved earlier stays editable and
|
|
940
|
+
still shows in the admin — it simply stops firing, and the dispatcher records `lastStatus: 0` on
|
|
941
|
+
it. Nothing tells you which rows are in that state, so list them:
|
|
942
|
+
`payload.find({ collection: 'webhook-endpoints' })` and look for `http://`, a private, loopback
|
|
943
|
+
or link-local host. `SUPPORT_ALLOW_INSECURE_WEBHOOKS=1` re-allows `http://` for local
|
|
944
|
+
development only.
|
|
945
|
+
2. **Review the collaborator rows created before 4.0.0.** A row with no explicit `role` now defaults
|
|
946
|
+
to `viewer`, which is read-only: an invitee who used to post messages stops being able to. Set
|
|
947
|
+
their row to `collaborator` to restore it.
|
|
948
|
+
3. **Expect the rate-limit counters to restart once.** `RateLimiter` now namespaces every key and
|
|
949
|
+
keys authenticated callers as `<collection>:<id>`, so rows already in `support-rate-limits` no
|
|
950
|
+
longer match. One-off reset.
|
|
951
|
+
4. **Drop `cookieState` from a hand-written Google OAuth button.** `{ action: 'login' }` now sets an
|
|
952
|
+
`HttpOnly` `support-oauth-state` cookie; the callback must let the browser carry it
|
|
953
|
+
(`credentials: 'include'` cross-origin).
|
|
954
|
+
5. **A dashboard calling `GET /support/email-stats` with a portal client session now gets `403`** —
|
|
955
|
+
it is staff-only.
|
|
956
|
+
6. **If your app replaces `config.custom` wholesale after the plugin runs**, it strips
|
|
957
|
+
`custom.supportStaffCollection` and the settings reads lose the staff slug they compare against.
|
|
958
|
+
|
|
741
959
|
### 2.x → 3.0
|
|
742
960
|
|
|
743
961
|
1. **Rotate the secret of every webhook endpoint.** The legacy transport sent it in clear in
|
|
@@ -752,7 +970,8 @@ auto-migrated: generate and push it before deploying. See [Troubleshooting](#tro
|
|
|
752
970
|
ticket.
|
|
753
971
|
5. **Import `./views` and `./components/TicketConversation` with ESM** — their CJS builds are gone.
|
|
754
972
|
6. Check your platform: Node 20.9+, React 19, Next 15.2.9+, Payload 3.79.1+, and install
|
|
755
|
-
`lucide-react` explicitly if you were relying on it being optional.
|
|
973
|
+
`lucide-react` explicitly if you were relying on it being optional. (It has since stopped being
|
|
974
|
+
a peer dependency altogether — the icons are inlined.)
|
|
756
975
|
|
|
757
976
|
### 1.x → 2.0
|
|
758
977
|
|
|
@@ -773,7 +992,7 @@ auto-migrated: generate and push it before deploying. See [Troubleshooting](#tro
|
|
|
773
992
|
3. Commit your changes (`git commit -m 'feat: add AmazingFeature'`).
|
|
774
993
|
4. Push the branch and open a Pull Request.
|
|
775
994
|
|
|
776
|
-
Run the checks before submitting — the suite is
|
|
995
|
+
Run the checks before submitting — the suite is 391 vitest tests:
|
|
777
996
|
|
|
778
997
|
```bash
|
|
779
998
|
pnpm typecheck && pnpm test && pnpm build
|
|
@@ -791,6 +1010,14 @@ If this plugin saves you time, consider [buying me a coffee](https://buymeacoffe
|
|
|
791
1010
|
|
|
792
1011
|
MIT — see [LICENSE](LICENSE).
|
|
793
1012
|
|
|
1013
|
+
**Runtime dependencies.** The package has exactly three, declared in `package.json`: `pdfkit`
|
|
1014
|
+
(^0.19.1, MIT), `sanitize-html` (^2.17.7, MIT) and `web-push` (^3.6.7, **MPL-2.0**). MPL-2.0 is the
|
|
1015
|
+
only reciprocal licence in that set, and its copyleft is per **file**, not per work: §3.3 lets a
|
|
1016
|
+
Larger Work be distributed under other terms. `payload-support` neither modifies nor bundles
|
|
1017
|
+
`web-push` — it imports it as an external dependency, which you can check in `dist/index.js:5` and
|
|
1018
|
+
`dist/index.cjs:7` — so the obligation is already met by npm shipping `web-push` with its own
|
|
1019
|
+
licence. The licence of this plugin is unaffected: it stays MIT.
|
|
1020
|
+
|
|
794
1021
|
Built and maintained by [ConsilioWEB](https://consilioweb.fr) ·
|
|
795
1022
|
[Issues](https://github.com/pOwn3d/payload-support/issues) ·
|
|
796
1023
|
[npm](https://www.npmjs.com/package/@consilioweb/payload-support)
|
|
@@ -255,7 +255,6 @@ const RichTextEditor = forwardRef(function RichTextEditor2({
|
|
|
255
255
|
fontSize: "14px",
|
|
256
256
|
lineHeight: 1.6,
|
|
257
257
|
color: "#1f2937",
|
|
258
|
-
outline: "none",
|
|
259
258
|
overflowY: "auto"
|
|
260
259
|
}
|
|
261
260
|
}
|
|
@@ -268,6 +267,7 @@ const RichTextEditor = forwardRef(function RichTextEditor2({
|
|
|
268
267
|
type: "file",
|
|
269
268
|
accept: "image/*",
|
|
270
269
|
multiple: true,
|
|
270
|
+
"aria-label": "Inserer une image",
|
|
271
271
|
onChange: handleFileChange,
|
|
272
272
|
style: { display: "none" }
|
|
273
273
|
}
|
|
@@ -80,7 +80,7 @@ const RewriteDropdown = ({ disabled, loading, onSelect }) => {
|
|
|
80
80
|
},
|
|
81
81
|
children: [
|
|
82
82
|
/* @__PURE__ */ jsx("span", { style: { fontSize: 12, fontWeight: 600, color: "var(--theme-text, #111)" }, children: style.label }),
|
|
83
|
-
/* @__PURE__ */ jsx("span", { style: { fontSize: 10, color: "var(--theme-elevation-
|
|
83
|
+
/* @__PURE__ */ jsx("span", { style: { fontSize: 10, color: "var(--theme-elevation-650, #6b7280)" }, children: style.desc })
|
|
84
84
|
]
|
|
85
85
|
},
|
|
86
86
|
style.id
|
|
@@ -31,6 +31,7 @@ function AISummaryPanel({
|
|
|
31
31
|
/* @__PURE__ */ jsx(
|
|
32
32
|
"button",
|
|
33
33
|
{
|
|
34
|
+
type: "button",
|
|
34
35
|
onClick: () => {
|
|
35
36
|
setShowAiSummary(!showAiSummary);
|
|
36
37
|
if (!showAiSummary && !aiSummary) handleAiGenerate();
|
|
@@ -58,6 +59,7 @@ function AISummaryPanel({
|
|
|
58
59
|
/* @__PURE__ */ jsx(
|
|
59
60
|
"button",
|
|
60
61
|
{
|
|
62
|
+
type: "button",
|
|
61
63
|
onClick: handleAiGenerate,
|
|
62
64
|
disabled: aiGenerating,
|
|
63
65
|
style: { ...s.outlineBtn("#17807c", aiGenerating), fontSize: "11px", padding: "4px 10px" },
|
|
@@ -67,6 +69,7 @@ function AISummaryPanel({
|
|
|
67
69
|
aiSummary && !aiGenerating && /* @__PURE__ */ jsx(
|
|
68
70
|
"button",
|
|
69
71
|
{
|
|
72
|
+
type: "button",
|
|
70
73
|
onClick: handleAiSave,
|
|
71
74
|
disabled: aiSaving || aiSaved,
|
|
72
75
|
style: { ...s.btn(aiSaved ? "#16a34a" : "#2563eb", aiSaving), fontSize: "11px", padding: "4px 10px" },
|
|
@@ -21,6 +21,7 @@ function MergePanel({
|
|
|
21
21
|
"input",
|
|
22
22
|
{
|
|
23
23
|
type: "text",
|
|
24
|
+
"aria-label": "Numero du ticket cible",
|
|
24
25
|
value: mergeTarget,
|
|
25
26
|
onChange: (e) => {
|
|
26
27
|
setMergeTarget(e.target.value);
|
|
@@ -31,7 +32,7 @@ function MergePanel({
|
|
|
31
32
|
style: { ...s.input, width: "130px" }
|
|
32
33
|
}
|
|
33
34
|
),
|
|
34
|
-
/* @__PURE__ */ jsx("button", { onClick: handleMergeLookup, style: { ...s.outlineBtn("#ec4899"), fontSize: "12px", padding: "6px 14px" }, children: "Rechercher" }),
|
|
35
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: handleMergeLookup, style: { ...s.outlineBtn("#ec4899"), fontSize: "12px", padding: "6px 14px" }, children: "Rechercher" }),
|
|
35
36
|
mergeError && /* @__PURE__ */ jsx("span", { style: { fontSize: "12px", color: "#be185d", fontWeight: 600 }, children: mergeError })
|
|
36
37
|
] }),
|
|
37
38
|
mergeTargetInfo && /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "8px", alignItems: "center" }, children: [
|
|
@@ -40,7 +41,7 @@ function MergePanel({
|
|
|
40
41
|
" \u2014 ",
|
|
41
42
|
mergeTargetInfo.subject
|
|
42
43
|
] }),
|
|
43
|
-
/* @__PURE__ */ jsx("button", { onClick: handleMerge, disabled: merging, style: { ...s.btn("#ec4899", merging), color: "#fff", fontSize: "12px", padding: "6px 14px" }, children: merging ? "Fusion..." : "Confirmer la fusion" })
|
|
44
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: handleMerge, disabled: merging, style: { ...s.btn("#ec4899", merging), color: "#fff", fontSize: "12px", padding: "6px 14px" }, children: merging ? "Fusion..." : "Confirmer la fusion" })
|
|
44
45
|
] })
|
|
45
46
|
] });
|
|
46
47
|
}
|
|
@@ -63,6 +64,7 @@ function ExtMessagePanel({
|
|
|
63
64
|
/* @__PURE__ */ jsx(
|
|
64
65
|
"textarea",
|
|
65
66
|
{
|
|
67
|
+
"aria-label": "Contenu du message recu",
|
|
66
68
|
value: extMsgBody,
|
|
67
69
|
onChange: (e) => setExtMsgBody(e.target.value),
|
|
68
70
|
placeholder: "Coller le contenu du message re\xE7u...",
|
|
@@ -71,7 +73,7 @@ function ExtMessagePanel({
|
|
|
71
73
|
}
|
|
72
74
|
),
|
|
73
75
|
/* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "8px", alignItems: "center", flexWrap: "wrap", marginBottom: "10px" }, children: [
|
|
74
|
-
/* @__PURE__ */ jsxs("select", { value: extMsgAuthor, onChange: (e) => setExtMsgAuthor(e.target.value), style: { ...s.input, fontSize: "12px" }, children: [
|
|
76
|
+
/* @__PURE__ */ jsxs("select", { "aria-label": "Auteur du message recu", value: extMsgAuthor, onChange: (e) => setExtMsgAuthor(e.target.value), style: { ...s.input, fontSize: "12px" }, children: [
|
|
75
77
|
/* @__PURE__ */ jsx("option", { value: "client", children: "Envoy\xE9 par le client" }),
|
|
76
78
|
/* @__PURE__ */ jsx("option", { value: "admin", children: "Envoy\xE9 par le support" })
|
|
77
79
|
] }),
|
|
@@ -79,19 +81,20 @@ function ExtMessagePanel({
|
|
|
79
81
|
"input",
|
|
80
82
|
{
|
|
81
83
|
type: "datetime-local",
|
|
84
|
+
"aria-label": "Date du message recu",
|
|
82
85
|
value: extMsgDate,
|
|
83
86
|
onChange: (e) => setExtMsgDate(e.target.value),
|
|
84
87
|
style: { ...s.input, fontSize: "12px" }
|
|
85
88
|
}
|
|
86
89
|
),
|
|
87
|
-
/* @__PURE__ */ jsx("input", { ref: extFileInputRef, type: "file", multiple: true, onChange: handleExtFileChange, style: { display: "none" }, accept: "image/*,.pdf,.doc,.docx,.txt,.zip" }),
|
|
90
|
+
/* @__PURE__ */ jsx("input", { ref: extFileInputRef, type: "file", multiple: true, "aria-label": "Joindre des fichiers au message recu", onChange: handleExtFileChange, style: { display: "none" }, accept: "image/*,.pdf,.doc,.docx,.txt,.zip" }),
|
|
88
91
|
/* @__PURE__ */ jsx("button", { type: "button", onClick: () => extFileInputRef.current?.click(), style: { ...s.ghostBtn("#6b7280"), fontSize: "12px", padding: "6px 12px" }, children: "+ PJ" })
|
|
89
92
|
] }),
|
|
90
93
|
extMsgFiles.length > 0 && /* @__PURE__ */ jsx("div", { style: { marginBottom: "10px", display: "flex", gap: "6px", flexWrap: "wrap" }, children: extMsgFiles.map((file, i) => /* @__PURE__ */ jsxs("span", { style: { ...s.badge("#f1f5f9", "#374151"), display: "inline-flex", alignItems: "center", gap: "4px" }, children: [
|
|
91
94
|
file.name,
|
|
92
95
|
/* @__PURE__ */ jsx("button", { type: "button", onClick: () => setExtMsgFiles((prev) => prev.filter((_, idx) => idx !== i)), style: { border: "none", background: "none", color: "#ef4444", fontWeight: 700, cursor: "pointer", fontSize: "14px", lineHeight: 1 }, children: "\xD7" })
|
|
93
96
|
] }, i)) }),
|
|
94
|
-
/* @__PURE__ */ jsx("button", { onClick: handleSendExtMsg, disabled: sendingExtMsg || !extMsgBody.trim(), style: s.btn("#1d2b4d", sendingExtMsg || !extMsgBody.trim()), children: sendingExtMsg ? "Ajout..." : "Ajouter (sans notification)" })
|
|
97
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: handleSendExtMsg, disabled: sendingExtMsg || !extMsgBody.trim(), style: s.btn("#1d2b4d", sendingExtMsg || !extMsgBody.trim()), children: sendingExtMsg ? "Ajout..." : "Ajouter (sans notification)" })
|
|
95
98
|
] });
|
|
96
99
|
}
|
|
97
100
|
const REMINDER_DELAYS = [
|
|
@@ -157,6 +160,7 @@ function ReminderPanel({
|
|
|
157
160
|
/* @__PURE__ */ jsx(
|
|
158
161
|
"button",
|
|
159
162
|
{
|
|
163
|
+
type: "button",
|
|
160
164
|
onClick: handleSendReminder,
|
|
161
165
|
disabled: reminderSending || !clientEmail,
|
|
162
166
|
style: { ...s.btn(C.orange, reminderSending || !clientEmail), color: "#fff", fontSize: "13px", padding: "8px 18px" },
|
|
@@ -169,13 +173,14 @@ function SnoozePanel({ snoozeSaving, handleSnooze }) {
|
|
|
169
173
|
return /* @__PURE__ */ jsxs("div", { style: { padding: "14px 18px", borderRadius: "8px", backgroundColor: "#eef8f7", border: "1px solid #b8dcda", marginBottom: "14px" }, children: [
|
|
170
174
|
/* @__PURE__ */ jsx("h4", { style: { fontSize: "13px", fontWeight: 600, marginBottom: "10px", color: "#5b21b6" }, children: "Snooze \u2014 masquer temporairement" }),
|
|
171
175
|
/* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "8px", flexWrap: "wrap", alignItems: "center" }, children: [
|
|
172
|
-
/* @__PURE__ */ jsx("button", { onClick: () => handleSnooze(1), disabled: snoozeSaving, style: { ...s.outlineBtn("#17807c", snoozeSaving), fontSize: "12px" }, children: "1 jour" }),
|
|
173
|
-
/* @__PURE__ */ jsx("button", { onClick: () => handleSnooze(3), disabled: snoozeSaving, style: { ...s.outlineBtn("#17807c", snoozeSaving), fontSize: "12px" }, children: "3 jours" }),
|
|
174
|
-
/* @__PURE__ */ jsx("button", { onClick: () => handleSnooze(7), disabled: snoozeSaving, style: { ...s.outlineBtn("#17807c", snoozeSaving), fontSize: "12px" }, children: "1 semaine" }),
|
|
176
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: () => handleSnooze(1), disabled: snoozeSaving, style: { ...s.outlineBtn("#17807c", snoozeSaving), fontSize: "12px" }, children: "1 jour" }),
|
|
177
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: () => handleSnooze(3), disabled: snoozeSaving, style: { ...s.outlineBtn("#17807c", snoozeSaving), fontSize: "12px" }, children: "3 jours" }),
|
|
178
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: () => handleSnooze(7), disabled: snoozeSaving, style: { ...s.outlineBtn("#17807c", snoozeSaving), fontSize: "12px" }, children: "1 semaine" }),
|
|
175
179
|
/* @__PURE__ */ jsx(
|
|
176
180
|
"input",
|
|
177
181
|
{
|
|
178
182
|
type: "datetime-local",
|
|
183
|
+
"aria-label": "Snoozer jusqu'a une date precise",
|
|
179
184
|
onChange: (e) => {
|
|
180
185
|
if (e.target.value) handleSnooze(null, e.target.value);
|
|
181
186
|
},
|
|
@@ -95,6 +95,7 @@ function CodeBlockInserter({ onInsert, className, style }) {
|
|
|
95
95
|
{
|
|
96
96
|
ref: inputRef,
|
|
97
97
|
type: "text",
|
|
98
|
+
"aria-label": "Rechercher un langage",
|
|
98
99
|
value: filter,
|
|
99
100
|
onChange: (e) => setFilter(e.target.value),
|
|
100
101
|
onKeyDown: (e) => {
|
|
@@ -113,7 +114,6 @@ function CodeBlockInserter({ onInsert, className, style }) {
|
|
|
113
114
|
border: "1px solid #334155",
|
|
114
115
|
borderRadius: 6,
|
|
115
116
|
color: "#e2e8f0",
|
|
116
|
-
outline: "none",
|
|
117
117
|
boxSizing: "border-box"
|
|
118
118
|
}
|
|
119
119
|
}
|
|
@@ -33,6 +33,7 @@ function QuickActions({
|
|
|
33
33
|
statusTransitions.map((a) => /* @__PURE__ */ jsx(
|
|
34
34
|
"button",
|
|
35
35
|
{
|
|
36
|
+
type: "button",
|
|
36
37
|
onClick: () => onStatusChange(a.status),
|
|
37
38
|
disabled: statusUpdating,
|
|
38
39
|
style: s.outlineBtn(a.color, statusUpdating),
|
|
@@ -40,21 +41,21 @@ function QuickActions({
|
|
|
40
41
|
},
|
|
41
42
|
a.status
|
|
42
43
|
)),
|
|
43
|
-
snoozeUntil && new Date(snoozeUntil) > /* @__PURE__ */ new Date() && /* @__PURE__ */ jsx("button", { onClick: onCancelSnooze, disabled: snoozeSaving, style: { ...s.ghostBtn("#17807c", snoozeSaving), fontSize: "12px", padding: "5px 10px" }, children: "Annuler snooze" }),
|
|
44
|
+
snoozeUntil && new Date(snoozeUntil) > /* @__PURE__ */ new Date() && /* @__PURE__ */ jsx("button", { type: "button", onClick: onCancelSnooze, disabled: snoozeSaving, style: { ...s.ghostBtn("#17807c", snoozeSaving), fontSize: "12px", padding: "5px 10px" }, children: "Annuler snooze" }),
|
|
44
45
|
/* @__PURE__ */ jsx("span", { style: { borderLeft: `1px solid ${C.border}`, height: "20px", margin: "0 4px" } }),
|
|
45
|
-
/* @__PURE__ */ jsx("button", { onClick: onToggleMerge, style: s.ghostBtn("#be185d"), children: "Fusionner" }),
|
|
46
|
-
/* @__PURE__ */ jsx("button", { onClick: onToggleExtMsg, style: s.ghostBtn("#4f46e5"), children: "+ Message re\xE7u" }),
|
|
47
|
-
/* @__PURE__ */ jsx("button", { onClick: onToggleSnooze, style: s.ghostBtn("#17807c"), children: "Snooze" }),
|
|
48
|
-
showReminderButton && /* @__PURE__ */ jsxs("button", { onClick: onToggleReminder, style: s.ghostBtn("#ea580c"), children: [
|
|
46
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: onToggleMerge, style: s.ghostBtn("#be185d"), children: "Fusionner" }),
|
|
47
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: onToggleExtMsg, style: s.ghostBtn("#4f46e5"), children: "+ Message re\xE7u" }),
|
|
48
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: onToggleSnooze, style: s.ghostBtn("#17807c"), children: "Snooze" }),
|
|
49
|
+
showReminderButton && /* @__PURE__ */ jsxs("button", { type: "button", onClick: onToggleReminder, style: s.ghostBtn("#ea580c"), children: [
|
|
49
50
|
"\u23F0",
|
|
50
51
|
" Relancer"
|
|
51
52
|
] }),
|
|
52
|
-
/* @__PURE__ */ jsx("button", { onClick: onNextTicket, style: s.ghostBtn("#16a34a"), children: "Ticket suivant" }),
|
|
53
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: onNextTicket, style: s.ghostBtn("#16a34a"), children: "Ticket suivant" }),
|
|
53
54
|
scheduledClose && /* @__PURE__ */ jsxs("span", { style: { display: "inline-flex", alignItems: "center", gap: "8px", padding: "5px 10px", borderRadius: "6px", backgroundColor: "#fff7ed", border: "1px solid #fed7aa", fontSize: "12px", color: "#9a3412", fontWeight: 600 }, children: [
|
|
54
55
|
"\u23F0",
|
|
55
56
|
" Fermeture auto le ",
|
|
56
57
|
scheduledClose,
|
|
57
|
-
/* @__PURE__ */ jsx("button", { onClick: onCancelScheduledClose, disabled: cancelingClose, style: { border: "none", background: "none", color: "#b91c1c", cursor: cancelingClose ? "not-allowed" : "pointer", fontWeight: 700, fontSize: "12px", textDecoration: "underline", padding: 0, opacity: cancelingClose ? 0.5 : 1 }, children: cancelingClose ? "..." : "Annuler" })
|
|
58
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: onCancelScheduledClose, disabled: cancelingClose, style: { border: "none", background: "none", color: "#b91c1c", cursor: cancelingClose ? "not-allowed" : "pointer", fontWeight: 700, fontSize: "12px", textDecoration: "underline", padding: 0, opacity: cancelingClose ? 0.5 : 1 }, children: cancelingClose ? "..." : "Annuler" })
|
|
58
59
|
] }),
|
|
59
60
|
reminderSuccess && /* @__PURE__ */ jsxs("span", { style: { ...s.badge("#f0fdf4", "#16a34a"), fontSize: "12px", padding: "5px 10px" }, children: [
|
|
60
61
|
"Relance envoy\xE9e ",
|
|
@@ -70,6 +71,7 @@ function QuickActions({
|
|
|
70
71
|
/* @__PURE__ */ jsx(
|
|
71
72
|
"button",
|
|
72
73
|
{
|
|
74
|
+
type: "button",
|
|
73
75
|
onClick: () => {
|
|
74
76
|
window.location.href = `/admin/support/ticket?id=${nextTicketId}`;
|
|
75
77
|
},
|
|
@@ -78,7 +80,7 @@ function QuickActions({
|
|
|
78
80
|
}
|
|
79
81
|
)
|
|
80
82
|
] }) : /* @__PURE__ */ jsx("span", { style: { fontSize: "13px", color: "#166534", fontWeight: 700 }, children: nextTicketInfo }),
|
|
81
|
-
/* @__PURE__ */ jsx("button", { onClick: onCloseNextTicket, style: { border: "none", background: "none", color: C.textMuted, cursor: "pointer", fontSize: "16px", fontWeight: 700, marginLeft: "8px" }, children: "\xD7" })
|
|
83
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: onCloseNextTicket, style: { border: "none", background: "none", color: C.textMuted, cursor: "pointer", fontSize: "16px", fontWeight: 700, marginLeft: "8px" }, children: "\xD7" })
|
|
82
84
|
] })
|
|
83
85
|
] });
|
|
84
86
|
}
|
|
@@ -69,6 +69,7 @@ function TicketHeader({
|
|
|
69
69
|
/* @__PURE__ */ jsx(
|
|
70
70
|
"button",
|
|
71
71
|
{
|
|
72
|
+
type: "button",
|
|
72
73
|
onClick: () => onCopyLink("admin"),
|
|
73
74
|
title: "Copier le lien admin",
|
|
74
75
|
style: { background: "none", border: "none", cursor: "pointer", fontSize: "13px", opacity: copiedLink === "admin" ? 1 : 0.5, padding: "2px 4px" },
|
|
@@ -78,6 +79,7 @@ function TicketHeader({
|
|
|
78
79
|
/* @__PURE__ */ jsx(
|
|
79
80
|
"button",
|
|
80
81
|
{
|
|
82
|
+
type: "button",
|
|
81
83
|
onClick: () => onCopyLink("client"),
|
|
82
84
|
title: "Copier le lien client",
|
|
83
85
|
style: { background: "none", border: "none", cursor: "pointer", fontSize: "13px", opacity: copiedLink === "client" ? 1 : 0.5, padding: "2px 4px" },
|