@consilioweb/payload-support 4.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.
Files changed (159) hide show
  1. package/README.md +272 -35
  2. package/dist/components/RichTextEditor/index.js +1 -1
  3. package/dist/components/TicketConversation/RewriteDropdown.js +1 -1
  4. package/dist/components/TicketConversation/components/AISummaryPanel.js +3 -0
  5. package/dist/components/TicketConversation/components/ActionPanels.js +13 -8
  6. package/dist/components/TicketConversation/components/ClientHistory.js +1 -0
  7. package/dist/components/TicketConversation/components/CodeBlock.js +1 -0
  8. package/dist/components/TicketConversation/components/CodeBlockInserter.js +1 -1
  9. package/dist/components/TicketConversation/components/QuickActions.js +10 -8
  10. package/dist/components/TicketConversation/components/TicketHeader.js +2 -0
  11. package/dist/components/TicketConversation/components/TimeTrackingPanel.js +15 -9
  12. package/dist/components/TicketConversation/index.d.ts +10 -0
  13. package/dist/components/TicketConversation/index.js +25 -7
  14. package/dist/index.cjs +421 -73
  15. package/dist/index.d.cts +127 -38
  16. package/dist/index.d.ts +127 -38
  17. package/dist/index.js +415 -72
  18. package/dist/styles/BillingView.module.scss +11 -11
  19. package/dist/styles/ChatView.module.scss +7 -7
  20. package/dist/styles/CommandPalette.module.scss +1 -1
  21. package/dist/styles/CrmView.module.scss +9 -9
  22. package/dist/styles/EmailTracking.module.scss +6 -6
  23. package/dist/styles/ImportConversation.module.scss +5 -5
  24. package/dist/styles/Logs.module.scss +5 -5
  25. package/dist/styles/NewTicket.module.scss +2 -2
  26. package/dist/styles/PendingEmails.module.scss +13 -13
  27. package/dist/styles/SupportDashboard.module.scss +24 -8
  28. package/dist/styles/TicketDetail.module.scss +20 -20
  29. package/dist/styles/TicketInbox.module.scss +6 -6
  30. package/dist/styles/TicketingSettings.module.scss +10 -10
  31. package/dist/styles/TimeDashboard.module.scss +6 -6
  32. package/dist/styles/_tokens.scss +3 -1
  33. package/dist/utils/db.d.ts +34 -0
  34. package/dist/utils/readSettings.d.ts +90 -0
  35. package/dist/views/BillingView/client.js +15 -8
  36. package/dist/views/BillingView/index.js +4 -4
  37. package/dist/views/ChatView/client.js +2 -0
  38. package/dist/views/ChatView/index.js +4 -4
  39. package/dist/views/CrmView/client.js +2 -0
  40. package/dist/views/CrmView/index.js +4 -4
  41. package/dist/views/EmailTrackingView/client.js +23 -11
  42. package/dist/views/EmailTrackingView/index.js +4 -4
  43. package/dist/views/ImportConversationView/client.js +1 -0
  44. package/dist/views/ImportConversationView/index.js +4 -4
  45. package/dist/views/LogsView/client.js +1 -1
  46. package/dist/views/LogsView/index.js +4 -2
  47. package/dist/views/NewTicketView/client.js +20 -13
  48. package/dist/views/NewTicketView/index.js +4 -2
  49. package/dist/views/PendingEmailsView/client.js +9 -3
  50. package/dist/views/PendingEmailsView/index.js +4 -4
  51. package/dist/views/SupportDashboardView/client.js +17 -19
  52. package/dist/views/SupportDashboardView/index.js +4 -4
  53. package/dist/views/TicketDetailView/client.js +23 -11
  54. package/dist/views/TicketDetailView/index.js +4 -4
  55. package/dist/views/TicketInboxView/client.js +52 -26
  56. package/dist/views/TicketInboxView/index.js +4 -2
  57. package/dist/views/TicketingSettingsView/TicketingSettings.module.scss +7 -7
  58. package/dist/views/TicketingSettingsView/client.js +44 -26
  59. package/dist/views/TicketingSettingsView/index.js +4 -4
  60. package/dist/views/TimeDashboardView/client.js +10 -7
  61. package/dist/views/TimeDashboardView/index.js +4 -4
  62. package/dist/views/shared/ErrorBoundary.d.ts +36 -1
  63. package/dist/views/shared/ErrorBoundary.js +59 -28
  64. package/dist/views/shared/adminTokens.d.ts +15 -5
  65. package/dist/views/shared/adminTokens.js +23 -7
  66. package/dist/views/shared/icons.d.ts +49 -0
  67. package/dist/views/shared/icons.js +92 -0
  68. package/dist/views/shared/locales/en.json +15 -1
  69. package/dist/views/shared/locales/fr.json +15 -1
  70. package/dist/views/shared/viewAccess.d.ts +29 -0
  71. package/dist/views/shared/viewAccess.js +24 -0
  72. package/package.json +31 -21
  73. package/scripts/uninstall-data.mjs +178 -0
  74. package/scripts/uninstall.mjs +184 -0
  75. package/src/collections/Tickets.ts +7 -0
  76. package/src/collections/TimeEntries.ts +11 -3
  77. package/src/components/RichTextEditor/index.tsx +1 -1
  78. package/src/components/TicketConversation/RewriteDropdown.tsx +1 -1
  79. package/src/components/TicketConversation/components/AISummaryPanel.tsx +3 -0
  80. package/src/components/TicketConversation/components/ActionPanels.tsx +13 -8
  81. package/src/components/TicketConversation/components/ClientHistory.tsx +1 -0
  82. package/src/components/TicketConversation/components/CodeBlock.tsx +1 -0
  83. package/src/components/TicketConversation/components/CodeBlockInserter.tsx +1 -1
  84. package/src/components/TicketConversation/components/QuickActions.tsx +10 -8
  85. package/src/components/TicketConversation/components/TicketHeader.tsx +2 -0
  86. package/src/components/TicketConversation/components/TimeTrackingPanel.tsx +17 -9
  87. package/src/components/TicketConversation/index.tsx +41 -7
  88. package/src/endpoints/auth-2fa.ts +53 -8
  89. package/src/endpoints/capabilities.ts +2 -4
  90. package/src/endpoints/chatbot.ts +2 -2
  91. package/src/endpoints/delete-account.ts +191 -47
  92. package/src/endpoints/export-data.ts +104 -4
  93. package/src/endpoints/import-conversation.ts +2 -2
  94. package/src/endpoints/login.ts +13 -3
  95. package/src/endpoints/purge-logs.ts +6 -15
  96. package/src/endpoints/push.ts +14 -1
  97. package/src/endpoints/statuses.ts +17 -0
  98. package/src/index.ts +2 -0
  99. package/src/plugin.ts +51 -1
  100. package/src/portal/LiveChat.tsx +1 -1
  101. package/src/portal/auth/profile/page.tsx +3 -2
  102. package/src/portal/icons.tsx +91 -0
  103. package/src/portal/login/page.tsx +14 -4
  104. package/src/styles/BillingView.module.scss +11 -11
  105. package/src/styles/ChatView.module.scss +7 -7
  106. package/src/styles/CommandPalette.module.scss +1 -1
  107. package/src/styles/CrmView.module.scss +9 -9
  108. package/src/styles/EmailTracking.module.scss +6 -6
  109. package/src/styles/ImportConversation.module.scss +5 -5
  110. package/src/styles/Logs.module.scss +5 -5
  111. package/src/styles/NewTicket.module.scss +2 -2
  112. package/src/styles/PendingEmails.module.scss +13 -13
  113. package/src/styles/SupportDashboard.module.scss +24 -8
  114. package/src/styles/TicketDetail.module.scss +20 -20
  115. package/src/styles/TicketInbox.module.scss +6 -6
  116. package/src/styles/TicketingSettings.module.scss +10 -10
  117. package/src/styles/TimeDashboard.module.scss +6 -6
  118. package/src/styles/_tokens.scss +3 -1
  119. package/src/types.ts +20 -0
  120. package/src/utils/push.ts +22 -0
  121. package/src/utils/rateLimiter.ts +98 -0
  122. package/src/utils/retention.ts +111 -0
  123. package/src/utils/slugs.ts +15 -0
  124. package/src/utils/twoFactorChallenge.ts +6 -1
  125. package/src/utils/urlSafety.ts +36 -1
  126. package/src/views/BillingView/client.tsx +17 -8
  127. package/src/views/BillingView/index.tsx +4 -4
  128. package/src/views/ChatView/client.tsx +2 -0
  129. package/src/views/ChatView/index.tsx +4 -4
  130. package/src/views/CrmView/client.tsx +2 -0
  131. package/src/views/CrmView/index.tsx +4 -4
  132. package/src/views/EmailTrackingView/client.tsx +18 -7
  133. package/src/views/EmailTrackingView/index.tsx +4 -4
  134. package/src/views/ImportConversationView/client.tsx +1 -0
  135. package/src/views/ImportConversationView/index.tsx +4 -4
  136. package/src/views/LogsView/client.tsx +1 -1
  137. package/src/views/LogsView/index.tsx +4 -2
  138. package/src/views/NewTicketView/client.tsx +22 -13
  139. package/src/views/NewTicketView/index.tsx +4 -2
  140. package/src/views/PendingEmailsView/client.tsx +9 -3
  141. package/src/views/PendingEmailsView/index.tsx +4 -4
  142. package/src/views/SupportDashboardView/client.tsx +11 -5
  143. package/src/views/SupportDashboardView/index.tsx +4 -4
  144. package/src/views/TicketDetailView/client.tsx +25 -11
  145. package/src/views/TicketDetailView/index.tsx +4 -4
  146. package/src/views/TicketInboxView/client.tsx +47 -7
  147. package/src/views/TicketInboxView/index.tsx +4 -2
  148. package/src/views/TicketingSettingsView/TicketingSettings.module.scss +7 -7
  149. package/src/views/TicketingSettingsView/client.tsx +201 -144
  150. package/src/views/TicketingSettingsView/index.tsx +4 -4
  151. package/src/views/TimeDashboardView/client.tsx +10 -7
  152. package/src/views/TimeDashboardView/index.tsx +4 -4
  153. package/src/views/shared/ErrorBoundary.tsx +95 -31
  154. package/src/views/shared/adminTokens.ts +28 -11
  155. package/src/views/shared/icons.tsx +146 -0
  156. package/src/views/shared/locales/en.json +15 -1
  157. package/src/views/shared/locales/fr.json +15 -1
  158. package/src/views/shared/viewAccess.ts +73 -0
  159. package/src/types/lucide-react.d.ts +0 -42
@@ -0,0 +1,24 @@
1
+ import { formatAdminURL } from 'payload/shared';
2
+ import { SUPPORT_STAFF_SLUG_CONFIG_KEY } from '../../utils/readSettings.js';
3
+
4
+ function supportViewRedirectTarget(initPageResult) {
5
+ const req = initPageResult?.req;
6
+ const config = req?.payload?.config;
7
+ const adminRoute = config?.routes?.admin ?? "/admin";
8
+ const routes = config?.admin?.routes;
9
+ const to = (route, fallback) => formatAdminURL({ adminRoute, path: route ?? fallback });
10
+ if (!req?.user) return to(routes?.login, "/login");
11
+ if (initPageResult?.permissions?.canAccessAdmin === false) {
12
+ return to(routes?.unauthorized, "/unauthorized");
13
+ }
14
+ const custom = config?.custom;
15
+ const registered = custom?.[SUPPORT_STAFF_SLUG_CONFIG_KEY];
16
+ const staffSlug = typeof registered === "string" && registered || config?.admin?.user || null;
17
+ const collection = req.user.collection;
18
+ if (!staffSlug || !collection || collection !== staffSlug) {
19
+ return to(routes?.unauthorized, "/unauthorized");
20
+ }
21
+ return null;
22
+ }
23
+
24
+ export { supportViewRedirectTarget };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@consilioweb/payload-support",
3
- "version": "4.0.0",
3
+ "version": "6.0.0",
4
4
  "description": "Payload CMS plugin — professional support & ticketing system with AI, SLA, time tracking, live chat, and more",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -40,12 +40,17 @@
40
40
  }
41
41
  }
42
42
  },
43
+ "bin": {
44
+ "support-uninstall": "./scripts/uninstall.mjs"
45
+ },
43
46
  "files": [
44
47
  "dist",
45
48
  "src",
46
49
  "!src/__tests__",
47
50
  "!src/**/*.test.ts",
48
51
  "!src/**/*.test.tsx",
52
+ "scripts/uninstall.mjs",
53
+ "scripts/uninstall-data.mjs",
49
54
  "README.md",
50
55
  "LICENSE"
51
56
  ],
@@ -85,12 +90,12 @@
85
90
  "author": "ConsilioWEB <contact@consilioweb.fr> (https://consilioweb.fr)",
86
91
  "license": "MIT",
87
92
  "peerDependencies": {
88
- "@payloadcms/next": "^3.37.0",
89
- "lucide-react": ">=0.300.0",
93
+ "@payloadcms/next": "^3.79.1",
90
94
  "next": "^15.2.9 || ^16.0.0",
91
- "payload": "^3.37.0",
95
+ "payload": "^3.79.1",
92
96
  "react": "^19.0.0",
93
- "react-dom": "^19.0.0"
97
+ "react-dom": "^19.0.0",
98
+ "@payloadcms/richtext-lexical": "^3.79.1"
94
99
  },
95
100
  "engines": {
96
101
  "node": ">=20.9.0"
@@ -100,32 +105,37 @@
100
105
  "provenance": true
101
106
  },
102
107
  "devDependencies": {
103
- "@payloadcms/db-sqlite": "^3.86.0",
104
- "@payloadcms/graphql": "^3.86.0",
105
- "@payloadcms/next": "^3.86.0",
106
- "@payloadcms/richtext-lexical": "^3.86.0",
107
- "@payloadcms/translations": "^3.86.0",
108
- "@payloadcms/ui": "^3.86.0",
109
- "@playwright/test": "^1.49.0",
108
+ "@payloadcms/db-sqlite": "^3.88.0",
109
+ "@payloadcms/graphql": "^3.88.0",
110
+ "@payloadcms/next": "^3.88.0",
111
+ "@payloadcms/richtext-lexical": "^3.88.0",
112
+ "@payloadcms/translations": "^3.88.0",
113
+ "@payloadcms/ui": "^3.88.0",
114
+ "@playwright/test": "^1.63.0",
110
115
  "@types/pdfkit": "^0.17.6",
111
- "@types/react": "^19.0.0",
116
+ "@types/react": "^19.2.18",
112
117
  "@types/sanitize-html": "^2.16.1",
113
118
  "@types/web-push": "^3.6.4",
114
119
  "esbuild-sass-plugin": "^3.7.0",
115
- "next": "^16.2.9",
116
- "payload": "^3.86.0",
117
- "react": "^19.2.7",
118
- "react-dom": "^19.2.7",
120
+ "next": "^16.3.4",
121
+ "payload": "^3.88.0",
122
+ "react": "^19.2.8",
123
+ "react-dom": "^19.2.8",
119
124
  "sharp": "0.34.5",
120
- "tsup": "^8.0.0",
121
- "typescript": "^5.5.0",
122
- "vitest": "^4.1.9"
125
+ "tsup": "^8.5.1",
126
+ "typescript": "^5.9.3",
127
+ "vitest": "^4.1.11"
123
128
  },
124
129
  "dependencies": {
125
130
  "pdfkit": "^0.19.1",
126
- "sanitize-html": "^2.17.5",
131
+ "sanitize-html": "^2.17.7",
127
132
  "web-push": "^3.6.7"
128
133
  },
134
+ "peerDependenciesMeta": {
135
+ "@payloadcms/richtext-lexical": {
136
+ "optional": true
137
+ }
138
+ },
129
139
  "scripts": {
130
140
  "build": "tsup && tsc -p tsconfig.types.json && node scripts/copy-subpath-types.mjs",
131
141
  "typecheck": "tsc --noEmit",
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Data half of `support-uninstall`. It is NOT meant to be run directly: it is
5
+ * spawned by `scripts/uninstall.mjs` through `payload run`, which is what puts
6
+ * the TypeScript loader and the environment in place so the host's
7
+ * `payload.config.ts` can be imported.
8
+ *
9
+ * Everything here goes through the Payload local API rather than a `sqlite3`
10
+ * binary. The plugin accepts custom collection slugs and runs on SQLite,
11
+ * PostgreSQL and MongoDB; raw SQL against hardcoded table names would only
12
+ * work on one of the three, and silently do nothing on the other two.
13
+ */
14
+
15
+ import { getPayload } from 'payload'
16
+ import { findConfig } from 'payload/node'
17
+ import { pathToFileURL } from 'node:url'
18
+
19
+ /**
20
+ * Deletion order matters: a child row whose `ticket` / `client` relationship is
21
+ * `required` blocks the delete of its parent (NOT NULL on the foreign key).
22
+ *
23
+ * These are the DEFAULT slugs. They are duplicated from `src/utils/slugs.ts`
24
+ * on purpose — importing `dist/index.js` here would pull pdfkit, web-push and
25
+ * sanitize-html into a script that only deletes rows. `src/__tests__/
26
+ * uninstallScript.test.ts` fails if the two lists ever drift apart.
27
+ */
28
+ const ORDERED_SLUGS = [
29
+ // 1. Rows pointing at a ticket.
30
+ 'ticket-collaborators',
31
+ 'ticket-feedback',
32
+ 'satisfaction-surveys',
33
+ 'ticket-activity-log',
34
+ 'ticket-messages',
35
+ 'time-entries',
36
+ // 2. The tickets themselves.
37
+ 'tickets',
38
+ // 3. Rows pointing at a client, or at nothing.
39
+ 'chat-messages',
40
+ 'pending-emails',
41
+ 'client-summaries',
42
+ 'notification-queue',
43
+ 'push-subscriptions',
44
+ 'canned-responses',
45
+ 'knowledge-base',
46
+ 'macros',
47
+ 'automation-rules',
48
+ 'ticket-statuses',
49
+ 'webhook-endpoints',
50
+ 'sla-policies',
51
+ 'support-teams',
52
+ 'auth-logs',
53
+ 'email-logs',
54
+ 'support-counters',
55
+ 'support-rate-limits',
56
+ // 4. The portal accounts.
57
+ 'support-clients',
58
+ ]
59
+
60
+ /** `payload-preferences` keys the plugin writes. */
61
+ const PREFERENCE_KEYS = {
62
+ exact: ['support-settings', 'support-round-robin'],
63
+ prefix: ['support-user-prefs'],
64
+ }
65
+
66
+ function parseArgs(argv) {
67
+ const confirm = argv.includes('--confirm')
68
+ const keepData = argv.includes('--keep-data')
69
+ const extra = argv.find((a) => a.startsWith('--extra-slugs='))
70
+ return {
71
+ confirm,
72
+ keepData,
73
+ extraSlugs: extra ? extra.slice('--extra-slugs='.length).split(',').map((s) => s.trim()).filter(Boolean) : [],
74
+ }
75
+ }
76
+
77
+ async function main() {
78
+ const { confirm, keepData, extraSlugs } = parseArgs(process.argv.slice(2))
79
+
80
+ const configPath = findConfig()
81
+ const imported = await import(pathToFileURL(configPath).toString())
82
+ const config = await (imported.default ?? imported)
83
+ const payload = await getPayload({ config })
84
+
85
+ const candidates = [...ORDERED_SLUGS, ...extraSlugs]
86
+ const present = candidates.filter((slug) => Boolean(payload.collections?.[slug]))
87
+ const absent = candidates.filter((slug) => !payload.collections?.[slug])
88
+
89
+ console.log('')
90
+ console.log(` Config: ${configPath}`)
91
+ console.log(` Mode: ${confirm ? 'DELETE' : 'DRY RUN (nothing is written)'}`)
92
+ console.log('')
93
+
94
+ if (!keepData) {
95
+ console.log(' Collections found in this app:')
96
+ for (const slug of present) {
97
+ const { totalDocs } = await payload.count({ collection: slug, overrideAccess: true })
98
+ console.log(` ${String(totalDocs).padStart(7)} ${slug}`)
99
+ }
100
+ if (present.length === 0) {
101
+ console.log(' (none — nothing of this plugin is registered in this config)')
102
+ }
103
+ if (absent.length > 0) {
104
+ console.log('')
105
+ console.log(` Not registered here, skipped: ${absent.join(', ')}`)
106
+ console.log(' If you renamed any of them through `collectionSlugs`, re-run with')
107
+ console.log(' --extra-slugs=your-slug,another-slug — this script cannot guess them.')
108
+ }
109
+ } else {
110
+ console.log(' --keep-data: no collection row will be deleted.')
111
+ }
112
+
113
+ console.log('')
114
+ console.log(' payload-preferences keys:')
115
+ for (const key of PREFERENCE_KEYS.exact) console.log(` ${key}`)
116
+ for (const key of PREFERENCE_KEYS.prefix) console.log(` ${key}* (one row per agent)`)
117
+
118
+ if (!confirm) {
119
+ console.log('')
120
+ console.log(' Nothing was deleted. Re-run with --confirm to apply.')
121
+ console.log('')
122
+ process.exit(0)
123
+ }
124
+
125
+ if (!keepData) {
126
+ console.log('')
127
+ for (const slug of present) {
128
+ const result = await payload.delete({
129
+ collection: slug,
130
+ where: { id: { exists: true } },
131
+ overrideAccess: true,
132
+ })
133
+ const deleted = Array.isArray(result?.docs) ? result.docs.length : 0
134
+ const failed = Array.isArray(result?.errors) ? result.errors.length : 0
135
+ console.log(` deleted ${String(deleted).padStart(7)} ${slug}${failed ? ` (${failed} failed)` : ''}`)
136
+ if (failed) {
137
+ console.log(` ${JSON.stringify(result.errors.slice(0, 3))}`)
138
+ }
139
+ }
140
+ }
141
+
142
+ console.log('')
143
+ for (const key of PREFERENCE_KEYS.exact) {
144
+ const result = await payload.delete({
145
+ collection: 'payload-preferences',
146
+ where: { key: { equals: key } },
147
+ overrideAccess: true,
148
+ })
149
+ console.log(` deleted ${String(result?.docs?.length ?? 0).padStart(7)} payload-preferences/${key}`)
150
+ }
151
+ for (const key of PREFERENCE_KEYS.prefix) {
152
+ const result = await payload.delete({
153
+ collection: 'payload-preferences',
154
+ where: { key: { like: key } },
155
+ overrideAccess: true,
156
+ })
157
+ console.log(` deleted ${String(result?.docs?.length ?? 0).padStart(7)} payload-preferences/${key}*`)
158
+ }
159
+
160
+ console.log('')
161
+ console.log(' Left untouched, on purpose:')
162
+ console.log(' - your `media` collection. Attachments uploaded through the support')
163
+ console.log(' desk are ordinary uploads once their messages are gone, and this')
164
+ console.log(' script has no way to tell them apart from the rest of your media.')
165
+ console.log(' Review them yourself before deleting anything there.')
166
+ console.log(' - the tables. Rows are gone; run `payload migrate:create` and apply')
167
+ console.log(' the migration to drop the now-unused tables.')
168
+ console.log('')
169
+
170
+ process.exit(0)
171
+ }
172
+
173
+ main().catch((err) => {
174
+ console.error('')
175
+ console.error(' Uninstall failed:', err)
176
+ console.error('')
177
+ process.exit(1)
178
+ })
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * `support-uninstall` — removes this plugin from a host application.
5
+ *
6
+ * Contract, deliberately identical to the other ConsilioWEB plugins:
7
+ * - DRY RUN by default. Nothing is written without `--confirm`.
8
+ * - a detection gate: the destructive step refuses to run unless this project
9
+ * really uses the plugin (a source file references it, or it is a declared
10
+ * dependency). `--force-db` overrides it.
11
+ * - `--keep-data` cleans the code and the preferences but keeps every row.
12
+ *
13
+ * Usage:
14
+ * npx support-uninstall list what would be removed
15
+ * npx support-uninstall --confirm do it
16
+ * npx support-uninstall --confirm --keep-data code only, rows kept
17
+ * npx support-uninstall --confirm --force-db delete rows even if undetected
18
+ * npx support-uninstall --extra-slugs=a,b add renamed collections
19
+ */
20
+
21
+ import fs from 'node:fs'
22
+ import path from 'node:path'
23
+ import { execSync, spawnSync } from 'node:child_process'
24
+ import { fileURLToPath } from 'node:url'
25
+
26
+ const HERE = path.dirname(fileURLToPath(import.meta.url))
27
+
28
+ function readOwnPackageName() {
29
+ try {
30
+ const pkg = JSON.parse(fs.readFileSync(path.join(HERE, '..', 'package.json'), 'utf-8'))
31
+ if (typeof pkg.name === 'string' && pkg.name) return pkg.name
32
+ } catch {
33
+ // fall through
34
+ }
35
+ return '@consilioweb/payload-support'
36
+ }
37
+
38
+ const PACKAGE_NAME = readOwnPackageName()
39
+
40
+ function detectPackageManager(dir) {
41
+ if (fs.existsSync(path.join(dir, 'pnpm-lock.yaml'))) return 'pnpm'
42
+ if (fs.existsSync(path.join(dir, 'yarn.lock'))) return 'yarn'
43
+ if (fs.existsSync(path.join(dir, 'bun.lockb')) || fs.existsSync(path.join(dir, 'bun.lock'))) return 'bun'
44
+ return 'npm'
45
+ }
46
+
47
+ function findSourceFiles(dir, acc = []) {
48
+ if (!fs.existsSync(dir)) return acc
49
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
50
+ const full = path.join(dir, entry.name)
51
+ if (entry.isDirectory()) {
52
+ if (entry.name === 'node_modules' || entry.name === '.next' || entry.name.startsWith('.')) continue
53
+ findSourceFiles(full, acc)
54
+ } else if (/\.(?:ts|tsx|js|jsx|mjs|cjs)$/.test(entry.name)) {
55
+ acc.push(full)
56
+ }
57
+ }
58
+ return acc
59
+ }
60
+
61
+ function isDeclaredDependency(projectDir) {
62
+ try {
63
+ const pkg = JSON.parse(fs.readFileSync(path.join(projectDir, 'package.json'), 'utf-8'))
64
+ return Boolean(
65
+ pkg.dependencies?.[PACKAGE_NAME]
66
+ || pkg.devDependencies?.[PACKAGE_NAME]
67
+ || pkg.peerDependencies?.[PACKAGE_NAME]
68
+ || pkg.optionalDependencies?.[PACKAGE_NAME],
69
+ )
70
+ } catch {
71
+ return false
72
+ }
73
+ }
74
+
75
+ function run(cmd, cwd) {
76
+ console.log(` $ ${cmd}`)
77
+ try {
78
+ execSync(cmd, { cwd, stdio: 'inherit' })
79
+ return true
80
+ } catch {
81
+ return false
82
+ }
83
+ }
84
+
85
+ function main() {
86
+ const projectDir = process.env.INIT_CWD || process.cwd()
87
+ const argv = process.argv.slice(2)
88
+ const confirm = argv.includes('--confirm')
89
+ const keepData = argv.includes('--keep-data')
90
+ const forceDb = argv.includes('--force-db')
91
+ const extraSlugs = argv.find((a) => a.startsWith('--extra-slugs='))
92
+ const pm = detectPackageManager(projectDir)
93
+
94
+ console.log('')
95
+ console.log(` ${PACKAGE_NAME} — uninstall`)
96
+ console.log(' ─────────────────────────────────────────────')
97
+ console.log(` Project: ${projectDir}`)
98
+ console.log(` Package manager: ${pm}`)
99
+ console.log(` Mode: ${confirm ? 'APPLY' : 'DRY RUN — nothing will be written'}`)
100
+ if (keepData) console.log(' --keep-data: rows will be kept')
101
+ console.log('')
102
+
103
+ // ── 1. Where is the plugin referenced? ───────────────────
104
+ const sourceFiles = findSourceFiles(path.join(projectDir, 'src'))
105
+ const referencing = sourceFiles.filter((file) => {
106
+ try {
107
+ return fs.readFileSync(file, 'utf-8').includes(PACKAGE_NAME)
108
+ } catch {
109
+ return false
110
+ }
111
+ })
112
+
113
+ console.log(' [1/3] Source references')
114
+ if (referencing.length === 0) {
115
+ console.log(' none found under src/')
116
+ } else {
117
+ for (const file of referencing) console.log(` ${path.relative(projectDir, file)}`)
118
+ console.log('')
119
+ console.log(' Remove the `supportPlugin(...)` call and its import yourself.')
120
+ console.log(' This script never rewrites your source: the plugin call carries')
121
+ console.log(' your configuration, and a regex that guesses where it ends is how')
122
+ console.log(' a config file gets silently corrupted.')
123
+ }
124
+ console.log('')
125
+
126
+ const declared = isDeclaredDependency(projectDir)
127
+ const detected = referencing.length > 0 || declared || forceDb
128
+
129
+ // ── 2. Data ──────────────────────────────────────────────
130
+ console.log(' [2/3] Data')
131
+ if (!detected) {
132
+ console.log(` ${PACKAGE_NAME} was not found in this project`)
133
+ console.log(' (no source reference, not in package.json dependencies).')
134
+ console.log(' Skipping the data step. Force it with --force-db.')
135
+ console.log('')
136
+ } else {
137
+ // The Payload local API needs the host's TypeScript config loaded, which is
138
+ // exactly what `payload run` sets up. Spawning it beats reimplementing a
139
+ // TypeScript loader here.
140
+ const dataScript = path.join(HERE, 'uninstall-data.mjs')
141
+ const args = ['payload', 'run', dataScript]
142
+ if (confirm) args.push('--confirm')
143
+ if (keepData) args.push('--keep-data')
144
+ if (extraSlugs) args.push(extraSlugs)
145
+
146
+ const result = spawnSync('npx', args, { cwd: projectDir, stdio: 'inherit' })
147
+ if (result.status !== 0) {
148
+ console.log('')
149
+ console.log(' Could not run the data step through `npx payload run`.')
150
+ console.log(' Run it by hand from your project root:')
151
+ console.log(` npx payload run ${dataScript} ${confirm ? '--confirm' : ''}`)
152
+ console.log('')
153
+ }
154
+ }
155
+
156
+ // ── 3. The package ───────────────────────────────────────
157
+ console.log(' [3/3] Package')
158
+ if (!confirm) {
159
+ const removeCmd = pm === 'npm' ? 'npm uninstall' : `${pm} remove`
160
+ console.log(` would run: ${removeCmd} ${PACKAGE_NAME}`)
161
+ console.log(` would run: ${pm === 'npm' ? 'npx' : pm} payload generate:importmap`)
162
+ console.log('')
163
+ console.log(' Dry run finished. Nothing was changed. Re-run with --confirm.')
164
+ console.log('')
165
+ return
166
+ }
167
+
168
+ if (referencing.length > 0) {
169
+ console.log(' Skipped: the plugin is still imported by the files listed above.')
170
+ console.log(' Remove those references first, then re-run.')
171
+ console.log('')
172
+ return
173
+ }
174
+
175
+ const removeCmd = pm === 'npm' ? 'npm uninstall' : `${pm} remove`
176
+ run(`${removeCmd} ${PACKAGE_NAME}`, projectDir)
177
+ run(`${pm === 'npm' ? 'npx' : pm} payload generate:importmap`, projectDir)
178
+
179
+ console.log('')
180
+ console.log(' Done.')
181
+ console.log('')
182
+ }
183
+
184
+ main()
@@ -525,9 +525,16 @@ function createCascadeDelete(slugs: CollectionSlugs): CollectionBeforeDeleteHook
525
525
  return async ({ id, req }) => {
526
526
  const collections = [slugs.ticketMessages, slugs.ticketActivityLog, slugs.timeEntries, slugs.satisfactionSurveys]
527
527
  for (const slug of collections) {
528
+ // `req` must be forwarded: it carries the ambient transaction. Without
529
+ // it these cascades open their own connection and, on SQLite, deadlock
530
+ // against the transaction that is deleting the ticket — the delete then
531
+ // fails silently (Payload collects per-document errors instead of
532
+ // throwing) and the children survive the parent.
533
+ if (!req.payload.collections?.[slug as never]) continue
528
534
  await dbDelete(req.payload, slug, {
529
535
  where: { ticket: { equals: id } },
530
536
  overrideAccess: true,
537
+ req,
531
538
  })
532
539
  }
533
540
  }
@@ -1,13 +1,19 @@
1
- import type { CollectionConfig, CollectionAfterChangeHook, CollectionAfterDeleteHook } from 'payload'
1
+ import type { CollectionConfig, CollectionAfterChangeHook, CollectionAfterDeleteHook, PayloadRequest } from 'payload'
2
2
  import type { Payload } from 'payload'
3
3
  import type { CollectionSlugs } from '../utils/slugs'
4
4
 
5
5
  // ─── Hooks ───────────────────────────────────────────────
6
6
 
7
+ /**
8
+ * `req` is forwarded to every operation so the rollup joins the caller's
9
+ * transaction. Reading or writing outside it would see a stale total and, on
10
+ * SQLite, contend with the very transaction that triggered the hook.
11
+ */
7
12
  async function recalculateTicketTime(
8
13
  payload: Payload,
9
14
  slugs: CollectionSlugs,
10
15
  ticketId: number | string,
16
+ req?: PayloadRequest,
11
17
  ): Promise<void> {
12
18
  // Sum all time entries for this ticket, paginating to avoid loading all at once
13
19
  let totalMinutes = 0
@@ -23,6 +29,7 @@ async function recalculateTicketTime(
23
29
  depth: 0,
24
30
  overrideAccess: true,
25
31
  select: { duration: true },
32
+ req,
26
33
  })
27
34
 
28
35
  for (const entry of entries.docs) {
@@ -38,6 +45,7 @@ async function recalculateTicketTime(
38
45
  id: ticketId,
39
46
  data: { totalTimeMinutes: totalMinutes },
40
47
  overrideAccess: true,
48
+ req,
41
49
  })
42
50
  }
43
51
 
@@ -45,7 +53,7 @@ function createRecalculateTicketTime(slugs: CollectionSlugs): CollectionAfterCha
45
53
  return async ({ doc, req }) => {
46
54
  if (!doc.ticket) return
47
55
  const ticketId = typeof doc.ticket === 'object' ? doc.ticket.id : doc.ticket
48
- await recalculateTicketTime(req.payload, slugs, ticketId)
56
+ await recalculateTicketTime(req.payload, slugs, ticketId, req)
49
57
  }
50
58
  }
51
59
 
@@ -59,7 +67,7 @@ function createRecalculateTicketTimeOnDelete(slugs: CollectionSlugs): Collection
59
67
  if (!doc?.ticket) return
60
68
  const ticketId = typeof doc.ticket === 'object' ? doc.ticket.id : doc.ticket
61
69
  try {
62
- await recalculateTicketTime(req.payload, slugs, ticketId)
70
+ await recalculateTicketTime(req.payload, slugs, ticketId, req)
63
71
  } catch (err) {
64
72
  // The entry is already gone; never fail the delete on the rollup refresh.
65
73
  console.error('[support] Failed to recalculate ticket time after delete:', err)
@@ -287,7 +287,6 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, Props>(function R
287
287
  fontSize: '14px',
288
288
  lineHeight: 1.6,
289
289
  color: '#1f2937',
290
- outline: 'none',
291
290
  overflowY: 'auto',
292
291
  }}
293
292
  />
@@ -300,6 +299,7 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, Props>(function R
300
299
  type="file"
301
300
  accept="image/*"
302
301
  multiple
302
+ aria-label="Inserer une image"
303
303
  onChange={handleFileChange}
304
304
  style={{ display: 'none' }}
305
305
  />
@@ -76,7 +76,7 @@ export const RewriteDropdown: React.FC<{
76
76
  onMouseLeave={(e) => { (e.target as HTMLElement).style.background = 'transparent' }}
77
77
  >
78
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>
79
+ <span style={{ fontSize: 10, color: 'var(--theme-elevation-650, #6b7280)' }}>{style.desc}</span>
80
80
  </button>
81
81
  ))}
82
82
  </div>
@@ -39,6 +39,7 @@ export function AISummaryPanel({
39
39
  return (
40
40
  <div style={{ marginBottom: '14px' }}>
41
41
  <button
42
+ type="button"
42
43
  onClick={() => { setShowAiSummary(!showAiSummary); if (!showAiSummary && !aiSummary) handleAiGenerate() }}
43
44
  style={{
44
45
  ...s.ghostBtn('#17807c', false),
@@ -61,6 +62,7 @@ export function AISummaryPanel({
61
62
  </h4>
62
63
  <div style={{ display: 'flex', gap: '6px' }}>
63
64
  <button
65
+ type="button"
64
66
  onClick={handleAiGenerate}
65
67
  disabled={aiGenerating}
66
68
  style={{ ...s.outlineBtn('#17807c', aiGenerating), fontSize: '11px', padding: '4px 10px' }}
@@ -69,6 +71,7 @@ export function AISummaryPanel({
69
71
  </button>
70
72
  {aiSummary && !aiGenerating && (
71
73
  <button
74
+ type="button"
72
75
  onClick={handleAiSave}
73
76
  disabled={aiSaving || aiSaved}
74
77
  style={{ ...s.btn(aiSaved ? '#16a34a' : '#2563eb', aiSaving), fontSize: '11px', padding: '4px 10px' }}