@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.
- package/README.md +272 -35
- 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 +421 -73
- package/dist/index.d.cts +127 -38
- package/dist/index.d.ts +127 -38
- package/dist/index.js +415 -72
- 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/utils/db.d.ts +34 -0
- package/dist/utils/readSettings.d.ts +90 -0
- package/dist/views/BillingView/client.js +15 -8
- package/dist/views/BillingView/index.js +4 -4
- package/dist/views/ChatView/client.js +2 -0
- package/dist/views/ChatView/index.js +4 -4
- package/dist/views/CrmView/client.js +2 -0
- package/dist/views/CrmView/index.js +4 -4
- package/dist/views/EmailTrackingView/client.js +23 -11
- package/dist/views/EmailTrackingView/index.js +4 -4
- package/dist/views/ImportConversationView/client.js +1 -0
- package/dist/views/ImportConversationView/index.js +4 -4
- package/dist/views/LogsView/client.js +1 -1
- package/dist/views/LogsView/index.js +4 -2
- package/dist/views/NewTicketView/client.js +20 -13
- package/dist/views/NewTicketView/index.js +4 -2
- package/dist/views/PendingEmailsView/client.js +9 -3
- package/dist/views/PendingEmailsView/index.js +4 -4
- package/dist/views/SupportDashboardView/client.js +17 -19
- package/dist/views/SupportDashboardView/index.js +4 -4
- package/dist/views/TicketDetailView/client.js +23 -11
- package/dist/views/TicketDetailView/index.js +4 -4
- package/dist/views/TicketInboxView/client.js +52 -26
- package/dist/views/TicketInboxView/index.js +4 -2
- package/dist/views/TicketingSettingsView/TicketingSettings.module.scss +7 -7
- package/dist/views/TicketingSettingsView/client.js +44 -26
- package/dist/views/TicketingSettingsView/index.js +4 -4
- package/dist/views/TimeDashboardView/client.js +10 -7
- package/dist/views/TimeDashboardView/index.js +4 -4
- 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/dist/views/shared/viewAccess.d.ts +29 -0
- package/dist/views/shared/viewAccess.js +24 -0
- package/package.json +31 -21
- 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/auth-2fa.ts +53 -8
- package/src/endpoints/capabilities.ts +2 -4
- package/src/endpoints/chatbot.ts +2 -2
- package/src/endpoints/delete-account.ts +191 -47
- package/src/endpoints/export-data.ts +104 -4
- package/src/endpoints/import-conversation.ts +2 -2
- package/src/endpoints/login.ts +13 -3
- package/src/endpoints/purge-logs.ts +6 -15
- package/src/endpoints/push.ts +14 -1
- package/src/endpoints/statuses.ts +17 -0
- 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/portal/login/page.tsx +14 -4
- 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/push.ts +22 -0
- package/src/utils/rateLimiter.ts +98 -0
- package/src/utils/retention.ts +111 -0
- package/src/utils/slugs.ts +15 -0
- package/src/utils/twoFactorChallenge.ts +6 -1
- package/src/utils/urlSafety.ts +36 -1
- package/src/views/BillingView/client.tsx +17 -8
- package/src/views/BillingView/index.tsx +4 -4
- package/src/views/ChatView/client.tsx +2 -0
- package/src/views/ChatView/index.tsx +4 -4
- package/src/views/CrmView/client.tsx +2 -0
- package/src/views/CrmView/index.tsx +4 -4
- package/src/views/EmailTrackingView/client.tsx +18 -7
- package/src/views/EmailTrackingView/index.tsx +4 -4
- package/src/views/ImportConversationView/client.tsx +1 -0
- package/src/views/ImportConversationView/index.tsx +4 -4
- package/src/views/LogsView/client.tsx +1 -1
- package/src/views/LogsView/index.tsx +4 -2
- package/src/views/NewTicketView/client.tsx +22 -13
- package/src/views/NewTicketView/index.tsx +4 -2
- package/src/views/PendingEmailsView/client.tsx +9 -3
- package/src/views/PendingEmailsView/index.tsx +4 -4
- package/src/views/SupportDashboardView/client.tsx +11 -5
- package/src/views/SupportDashboardView/index.tsx +4 -4
- package/src/views/TicketDetailView/client.tsx +25 -11
- package/src/views/TicketDetailView/index.tsx +4 -4
- package/src/views/TicketInboxView/client.tsx +47 -7
- package/src/views/TicketInboxView/index.tsx +4 -2
- package/src/views/TicketingSettingsView/TicketingSettings.module.scss +7 -7
- package/src/views/TicketingSettingsView/client.tsx +201 -144
- package/src/views/TicketingSettingsView/index.tsx +4 -4
- package/src/views/TimeDashboardView/client.tsx +10 -7
- package/src/views/TimeDashboardView/index.tsx +4 -4
- 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/views/shared/viewAccess.ts +73 -0
- 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": "
|
|
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.
|
|
89
|
-
"lucide-react": ">=0.300.0",
|
|
93
|
+
"@payloadcms/next": "^3.79.1",
|
|
90
94
|
"next": "^15.2.9 || ^16.0.0",
|
|
91
|
-
"payload": "^3.
|
|
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.
|
|
104
|
-
"@payloadcms/graphql": "^3.
|
|
105
|
-
"@payloadcms/next": "^3.
|
|
106
|
-
"@payloadcms/richtext-lexical": "^3.
|
|
107
|
-
"@payloadcms/translations": "^3.
|
|
108
|
-
"@payloadcms/ui": "^3.
|
|
109
|
-
"@playwright/test": "^1.
|
|
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.
|
|
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.
|
|
116
|
-
"payload": "^3.
|
|
117
|
-
"react": "^19.2.
|
|
118
|
-
"react-dom": "^19.2.
|
|
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.
|
|
121
|
-
"typescript": "^5.
|
|
122
|
-
"vitest": "^4.1.
|
|
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.
|
|
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-
|
|
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' }}
|