@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
|
@@ -1,25 +1,100 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import React from 'react'
|
|
4
|
+
import { useTranslation } from '../../components/TicketConversation/hooks/useTranslation'
|
|
4
5
|
|
|
5
6
|
interface ErrorBoundaryProps {
|
|
6
7
|
children: React.ReactNode
|
|
7
8
|
fallback?: React.ReactNode
|
|
8
9
|
viewName?: string
|
|
10
|
+
/**
|
|
11
|
+
* Values that identify the input the subtree renders from (a ticket id, a
|
|
12
|
+
* filter, a page number...). When any of them changes the boundary clears
|
|
13
|
+
* the error on its own, because the new input may well render fine.
|
|
14
|
+
*/
|
|
15
|
+
resetKeys?: unknown[]
|
|
9
16
|
}
|
|
10
17
|
|
|
11
18
|
interface ErrorBoundaryState {
|
|
12
19
|
hasError: boolean
|
|
13
20
|
error: Error | null
|
|
21
|
+
/**
|
|
22
|
+
* Bumped on every reset. It is used as the `key` of the wrapper around the
|
|
23
|
+
* children so that a retry REMOUNTS the subtree. Clearing `hasError` alone
|
|
24
|
+
* keeps the broken component instance and its state, so anything that threw
|
|
25
|
+
* from a bad piece of state throws again on the very next render.
|
|
26
|
+
*/
|
|
27
|
+
resetCount: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Shallow comparison of two reset-key arrays. A missing array is treated as
|
|
32
|
+
* empty, so a boundary declared without `resetKeys` never auto-resets.
|
|
33
|
+
*/
|
|
34
|
+
export function haveResetKeysChanged(previous?: unknown[], next?: unknown[]): boolean {
|
|
35
|
+
const a = previous ?? []
|
|
36
|
+
const b = next ?? []
|
|
37
|
+
|
|
38
|
+
if (a.length !== b.length) return true
|
|
39
|
+
return a.some((value, index) => !Object.is(value, b[index]))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Default fallback UI.
|
|
44
|
+
*
|
|
45
|
+
* Exported so it can be rendered (and asserted on) in isolation. It is the
|
|
46
|
+
* only screen a user sees when a view is broken, so it must stay readable in
|
|
47
|
+
* both themes: every colour comes from a Payload theme token, never a hex
|
|
48
|
+
* literal that only works on a light background.
|
|
49
|
+
*
|
|
50
|
+
* The thrown message is deliberately NOT displayed — it can carry a stack
|
|
51
|
+
* fragment, an internal path or a raw API payload. It is logged to the console
|
|
52
|
+
* by `componentDidCatch` instead.
|
|
53
|
+
*/
|
|
54
|
+
export function AdminErrorFallback({ onRetry }: { onRetry: () => void }): React.ReactElement {
|
|
55
|
+
const { t } = useTranslation()
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div
|
|
59
|
+
role="alert"
|
|
60
|
+
style={{
|
|
61
|
+
padding: '40px',
|
|
62
|
+
textAlign: 'center',
|
|
63
|
+
color: 'var(--theme-error-550)',
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
<h2 style={{ marginBottom: '16px', fontSize: '18px' }}>
|
|
67
|
+
{t('errorBoundary.title')}
|
|
68
|
+
</h2>
|
|
69
|
+
<p style={{ marginBottom: '24px', color: 'var(--theme-elevation-650)', fontSize: '14px' }}>
|
|
70
|
+
{t('errorBoundary.description')}
|
|
71
|
+
</p>
|
|
72
|
+
<button
|
|
73
|
+
type="button"
|
|
74
|
+
onClick={onRetry}
|
|
75
|
+
style={{
|
|
76
|
+
padding: '8px 20px',
|
|
77
|
+
backgroundColor: 'var(--theme-success-550)',
|
|
78
|
+
color: 'var(--theme-elevation-0)',
|
|
79
|
+
border: 'none',
|
|
80
|
+
borderRadius: '6px',
|
|
81
|
+
cursor: 'pointer',
|
|
82
|
+
fontSize: '14px',
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
{t('errorBoundary.retry')}
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
)
|
|
14
89
|
}
|
|
15
90
|
|
|
16
91
|
export class AdminErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
17
92
|
constructor(props: ErrorBoundaryProps) {
|
|
18
93
|
super(props)
|
|
19
|
-
this.state = { hasError: false, error: null }
|
|
94
|
+
this.state = { hasError: false, error: null, resetCount: 0 }
|
|
20
95
|
}
|
|
21
96
|
|
|
22
|
-
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
|
97
|
+
static getDerivedStateFromError(error: Error): Pick<ErrorBoundaryState, 'hasError' | 'error'> {
|
|
23
98
|
return { hasError: true, error }
|
|
24
99
|
}
|
|
25
100
|
|
|
@@ -27,42 +102,31 @@ export class AdminErrorBoundary extends React.Component<ErrorBoundaryProps, Erro
|
|
|
27
102
|
console.error(`[${this.props.viewName || 'AdminView'}] Error:`, error, errorInfo)
|
|
28
103
|
}
|
|
29
104
|
|
|
105
|
+
componentDidUpdate(prevProps: ErrorBoundaryProps): void {
|
|
106
|
+
if (!this.state.hasError) return
|
|
107
|
+
if (!haveResetKeysChanged(prevProps.resetKeys, this.props.resetKeys)) return
|
|
108
|
+
this.reset()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
reset = (): void => {
|
|
112
|
+
this.setState((prev) => ({
|
|
113
|
+
hasError: false,
|
|
114
|
+
error: null,
|
|
115
|
+
resetCount: prev.resetCount + 1,
|
|
116
|
+
}))
|
|
117
|
+
}
|
|
118
|
+
|
|
30
119
|
render(): React.ReactNode {
|
|
31
120
|
if (this.state.hasError) {
|
|
32
121
|
if (this.props.fallback) {
|
|
33
122
|
return this.props.fallback
|
|
34
123
|
}
|
|
35
124
|
|
|
36
|
-
return
|
|
37
|
-
<div style={{
|
|
38
|
-
padding: '40px',
|
|
39
|
-
textAlign: 'center',
|
|
40
|
-
color: '#dc2626',
|
|
41
|
-
}}>
|
|
42
|
-
<h2 style={{ marginBottom: '16px', fontSize: '18px' }}>
|
|
43
|
-
Une erreur est survenue
|
|
44
|
-
</h2>
|
|
45
|
-
<p style={{ marginBottom: '24px', color: '#6b7280', fontSize: '14px' }}>
|
|
46
|
-
{this.state.error?.message || 'Erreur inattendue'}
|
|
47
|
-
</p>
|
|
48
|
-
<button
|
|
49
|
-
onClick={() => this.setState({ hasError: false, error: null })}
|
|
50
|
-
style={{
|
|
51
|
-
padding: '8px 20px',
|
|
52
|
-
backgroundColor: '#2563eb',
|
|
53
|
-
color: '#fff',
|
|
54
|
-
border: 'none',
|
|
55
|
-
borderRadius: '6px',
|
|
56
|
-
cursor: 'pointer',
|
|
57
|
-
fontSize: '14px',
|
|
58
|
-
}}
|
|
59
|
-
>
|
|
60
|
-
Reessayer
|
|
61
|
-
</button>
|
|
62
|
-
</div>
|
|
63
|
-
)
|
|
125
|
+
return <AdminErrorFallback onRetry={this.reset} />
|
|
64
126
|
}
|
|
65
127
|
|
|
66
|
-
|
|
128
|
+
// The key forces a full remount after a reset instead of resuming the
|
|
129
|
+
// component instance that just threw.
|
|
130
|
+
return <React.Fragment key={this.state.resetCount}>{this.props.children}</React.Fragment>
|
|
67
131
|
}
|
|
68
132
|
}
|
|
@@ -3,31 +3,48 @@ import type React from 'react'
|
|
|
3
3
|
// Shared design tokens for all admin views (professional style)
|
|
4
4
|
export const V = {
|
|
5
5
|
text: 'var(--theme-text)',
|
|
6
|
-
|
|
6
|
+
// elevation-650, not elevation-500: Payload's dark theme does not redefine
|
|
7
|
+
// the 500 step (colors.scss jumps from 450 to 550), so elevation-500 stays
|
|
8
|
+
// rgb(128,128,128) in both themes — 3.62:1 on a light background and 4.03:1
|
|
9
|
+
// on a dark one, below WCAG AA in both. elevation-650 gives 6.63:1 / 9.03:1.
|
|
10
|
+
textSecondary: 'var(--theme-elevation-650)',
|
|
7
11
|
bg: 'var(--theme-elevation-50)',
|
|
8
12
|
bgCard: 'var(--theme-elevation-100)',
|
|
9
13
|
border: 'var(--theme-elevation-300)',
|
|
10
|
-
// Professional palette
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
// Professional palette. These are fixed hex values (they do not follow the
|
|
15
|
+
// admin theme), so they are only ever used as a background behind white
|
|
16
|
+
// text; each one is dark enough for white to reach at least 4.5:1.
|
|
17
|
+
blue: '#2563eb', // white on it: 5.17:1
|
|
18
|
+
amber: '#b45309', // white on it: 5.02:1
|
|
19
|
+
orange: '#c2410c', // white on it: 5.18:1
|
|
20
|
+
green: '#15803d', // white on it: 5.02:1
|
|
21
|
+
red: '#dc2626', // white on it: 4.83:1
|
|
22
|
+
// Neutral / secondary button. Both halves follow the admin theme, so they
|
|
23
|
+
// must be used together — white on a theme token is unreadable in one of
|
|
24
|
+
// the two themes whichever token is picked.
|
|
25
|
+
neutralBg: 'var(--theme-elevation-150)',
|
|
26
|
+
neutralFg: 'var(--theme-text)',
|
|
16
27
|
// Legacy aliases (backward compat for views not yet updated)
|
|
17
28
|
cyan: '#2563eb',
|
|
18
|
-
yellow: '#
|
|
29
|
+
yellow: '#b45309',
|
|
19
30
|
} as const
|
|
20
31
|
|
|
21
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Button style factory.
|
|
34
|
+
*
|
|
35
|
+
* `bg` is expected to be one of the fixed hex values of `V`; the default
|
|
36
|
+
* foreground is white, which reaches AA on all of them. Pass `fg` whenever
|
|
37
|
+
* `bg` is a theme variable, since white only works on one of the two themes.
|
|
38
|
+
*/
|
|
22
39
|
export const btnStyle = (
|
|
23
40
|
bg: string,
|
|
24
|
-
opts?: { disabled?: boolean; small?: boolean },
|
|
41
|
+
opts?: { disabled?: boolean; small?: boolean; fg?: string },
|
|
25
42
|
): React.CSSProperties => ({
|
|
26
43
|
padding: opts?.small ? '6px 12px' : '8px 14px',
|
|
27
44
|
borderRadius: 6,
|
|
28
45
|
border: `1px solid var(--theme-elevation-300)`,
|
|
29
46
|
backgroundColor: bg,
|
|
30
|
-
color: '#fff',
|
|
47
|
+
color: opts?.fg ?? '#fff',
|
|
31
48
|
fontWeight: 600,
|
|
32
49
|
fontSize: opts?.small ? 12 : 13,
|
|
33
50
|
cursor: opts?.disabled ? 'not-allowed' : 'pointer',
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inlined Lucide icons.
|
|
3
|
+
*
|
|
4
|
+
* The plugin used to require `lucide-react` as a peer dependency for exactly
|
|
5
|
+
* 15 icons across two admin views. That made every integrator install an icon set,
|
|
6
|
+
* and a missing install a build-time failure, for 15 paths totalling a few
|
|
7
|
+
* hundred bytes. They are inlined here instead, and `lucide-react` is no longer
|
|
8
|
+
* a peer dependency.
|
|
9
|
+
*
|
|
10
|
+
* The geometry below is copied from lucide-react v1.41.0 and, like the rest of
|
|
11
|
+
* Lucide, is under the ISC licence:
|
|
12
|
+
*
|
|
13
|
+
* ISC License
|
|
14
|
+
* Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as
|
|
15
|
+
* part of Feather (MIT). All other copyright (c) for Lucide are held by
|
|
16
|
+
* Lucide Contributors 2022.
|
|
17
|
+
*
|
|
18
|
+
* Permission to use, copy, modify, and/or distribute this software for any
|
|
19
|
+
* purpose with or without fee is hereby granted, provided that the above
|
|
20
|
+
* copyright notice and this permission notice appear in all copies.
|
|
21
|
+
*
|
|
22
|
+
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
23
|
+
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
24
|
+
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
25
|
+
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
26
|
+
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
27
|
+
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
|
28
|
+
* IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import React from 'react'
|
|
32
|
+
|
|
33
|
+
export interface IconProps extends Omit<React.SVGProps<SVGSVGElement>, 'width' | 'height'> {
|
|
34
|
+
/** Rendered width and height. Lucide's own default is 24. */
|
|
35
|
+
size?: number | string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type IconNode = ReadonlyArray<readonly [string, Record<string, string | number>]>
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Icons are decorative by default (`aria-hidden`): every control that has
|
|
42
|
+
* nothing but an icon inside carries its own `aria-label`. Pass
|
|
43
|
+
* `aria-hidden={false}` plus a label to make one meaningful on its own.
|
|
44
|
+
*/
|
|
45
|
+
function createIcon(displayName: string, nodes: IconNode): React.FC<IconProps> {
|
|
46
|
+
const Icon: React.FC<IconProps> = ({ size = 24, strokeWidth = 2, ...rest }) => (
|
|
47
|
+
<svg
|
|
48
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
49
|
+
width={size}
|
|
50
|
+
height={size}
|
|
51
|
+
viewBox="0 0 24 24"
|
|
52
|
+
fill="none"
|
|
53
|
+
stroke="currentColor"
|
|
54
|
+
strokeWidth={strokeWidth}
|
|
55
|
+
strokeLinecap="round"
|
|
56
|
+
strokeLinejoin="round"
|
|
57
|
+
aria-hidden="true"
|
|
58
|
+
{...rest}
|
|
59
|
+
>
|
|
60
|
+
{nodes.map(([tag, attrs], index) => React.createElement(tag, { key: index, ...attrs }))}
|
|
61
|
+
</svg>
|
|
62
|
+
)
|
|
63
|
+
Icon.displayName = displayName
|
|
64
|
+
return Icon
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const Bot = createIcon('Bot', [
|
|
68
|
+
['path', { d: "M12 8V4H8" }],
|
|
69
|
+
['rect', { width: "16", height: "12", x: "4", y: "8", rx: "2" }],
|
|
70
|
+
['path', { d: "M2 14h2" }],
|
|
71
|
+
['path', { d: "M20 14h2" }],
|
|
72
|
+
['path', { d: "M15 13v2" }],
|
|
73
|
+
['path', { d: "M9 13v2" }],
|
|
74
|
+
] as const)
|
|
75
|
+
|
|
76
|
+
export const ChevronDown = createIcon('ChevronDown', [
|
|
77
|
+
['path', { d: "m6 9 6 6 6-6" }],
|
|
78
|
+
] as const)
|
|
79
|
+
|
|
80
|
+
export const ChevronUp = createIcon('ChevronUp', [
|
|
81
|
+
['path', { d: "m18 15-6-6-6 6" }],
|
|
82
|
+
] as const)
|
|
83
|
+
|
|
84
|
+
export const Clock = createIcon('Clock', [
|
|
85
|
+
['circle', { cx: "12", cy: "12", r: "10" }],
|
|
86
|
+
['path', { d: "M12 6v6l4 2" }],
|
|
87
|
+
] as const)
|
|
88
|
+
|
|
89
|
+
export const FileSignature = createIcon('FileSignature', [
|
|
90
|
+
['path', { d: "M14.364 13.634a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506l4.013-4.009a1 1 0 0 0-3.004-3.004z" }],
|
|
91
|
+
['path', { d: "M14.487 7.858A1 1 0 0 1 14 7V2" }],
|
|
92
|
+
['path', { d: "M20 19.645V20a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l2.516 2.516" }],
|
|
93
|
+
['path', { d: "M8 18h1" }],
|
|
94
|
+
] as const)
|
|
95
|
+
|
|
96
|
+
export const Globe = createIcon('Globe', [
|
|
97
|
+
['circle', { cx: "12", cy: "12", r: "10" }],
|
|
98
|
+
['path', { d: "M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" }],
|
|
99
|
+
['path', { d: "M2 12h20" }],
|
|
100
|
+
] as const)
|
|
101
|
+
|
|
102
|
+
export const Inbox = createIcon('Inbox', [
|
|
103
|
+
['polyline', { points: "22 12 16 12 14 15 10 15 8 12 2 12" }],
|
|
104
|
+
['path', { d: "M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" }],
|
|
105
|
+
] as const)
|
|
106
|
+
|
|
107
|
+
export const Link2 = createIcon('Link2', [
|
|
108
|
+
['path', { d: "M9 17H7A5 5 0 0 1 7 7h2" }],
|
|
109
|
+
['path', { d: "M15 7h2a5 5 0 1 1 0 10h-2" }],
|
|
110
|
+
['line', { x1: "8", x2: "16", y1: "12", y2: "12" }],
|
|
111
|
+
] as const)
|
|
112
|
+
|
|
113
|
+
export const Mail = createIcon('Mail', [
|
|
114
|
+
['path', { d: "m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7" }],
|
|
115
|
+
['rect', { x: "2", y: "4", width: "20", height: "16", rx: "2" }],
|
|
116
|
+
] as const)
|
|
117
|
+
|
|
118
|
+
export const Paperclip = createIcon('Paperclip', [
|
|
119
|
+
['path', { d: "m16 6-8.414 8.586a2 2 0 0 0 2.829 2.829l8.414-8.586a4 4 0 1 0-5.657-5.657l-8.379 8.551a6 6 0 1 0 8.485 8.485l8.379-8.551" }],
|
|
120
|
+
] as const)
|
|
121
|
+
|
|
122
|
+
export const Plus = createIcon('Plus', [
|
|
123
|
+
['path', { d: "M5 12h14" }],
|
|
124
|
+
['path', { d: "M12 5v14" }],
|
|
125
|
+
] as const)
|
|
126
|
+
|
|
127
|
+
export const Search = createIcon('Search', [
|
|
128
|
+
['path', { d: "m21 21-4.34-4.34" }],
|
|
129
|
+
['circle', { cx: "11", cy: "11", r: "8" }],
|
|
130
|
+
] as const)
|
|
131
|
+
|
|
132
|
+
export const Settings = createIcon('Settings', [
|
|
133
|
+
['path', { d: "M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" }],
|
|
134
|
+
['circle', { cx: "12", cy: "12", r: "3" }],
|
|
135
|
+
] as const)
|
|
136
|
+
|
|
137
|
+
export const Timer = createIcon('Timer', [
|
|
138
|
+
['line', { x1: "10", x2: "14", y1: "2", y2: "2" }],
|
|
139
|
+
['line', { x1: "12", x2: "15", y1: "14", y2: "11" }],
|
|
140
|
+
['circle', { cx: "12", cy: "14", r: "8" }],
|
|
141
|
+
] as const)
|
|
142
|
+
|
|
143
|
+
export const X = createIcon('X', [
|
|
144
|
+
['path', { d: "M18 6 6 18" }],
|
|
145
|
+
['path', { d: "m6 6 12 12" }],
|
|
146
|
+
] as const)
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
+
"errorBoundary": {
|
|
3
|
+
"title": "Something went wrong",
|
|
4
|
+
"description": "This section could not be displayed. The technical details were logged to the browser console.",
|
|
5
|
+
"retry": "Retry"
|
|
6
|
+
},
|
|
2
7
|
"common": {
|
|
3
8
|
"loading": "Loading...",
|
|
4
9
|
"cancel": "Cancel",
|
|
@@ -144,6 +149,7 @@
|
|
|
144
149
|
"title": "CRM Clients",
|
|
145
150
|
"subtitle": "Client management and history",
|
|
146
151
|
"searchPlaceholder": "Search a client...",
|
|
152
|
+
"searchLabel": "Search a client",
|
|
147
153
|
"noClientFound": "No client found",
|
|
148
154
|
"selectClient": "Select a client to see their details",
|
|
149
155
|
"editButton": "Edit",
|
|
@@ -316,6 +322,8 @@
|
|
|
316
322
|
"resolved": "Resolved",
|
|
317
323
|
"slaBreach": "SLA breached"
|
|
318
324
|
},
|
|
325
|
+
"searchLabel": "Search a ticket",
|
|
326
|
+
"sortLabel": "Sort the tickets",
|
|
319
327
|
"sort": {
|
|
320
328
|
"newest": "Most recent",
|
|
321
329
|
"oldest": "Oldest first",
|
|
@@ -324,6 +332,7 @@
|
|
|
324
332
|
},
|
|
325
333
|
"selected": "{{count}} selected",
|
|
326
334
|
"selectedPlural": "{{count}} selected",
|
|
335
|
+
"tabsLabel": "Inbox filters",
|
|
327
336
|
"closeAction": "Close",
|
|
328
337
|
"reopenAction": "Reopen",
|
|
329
338
|
"moreActions": "More actions...",
|
|
@@ -344,6 +353,7 @@
|
|
|
344
353
|
"emailTracking": {
|
|
345
354
|
"title": "Email tracking",
|
|
346
355
|
"searchPlaceholder": "Search by recipient or subject...",
|
|
356
|
+
"searchLabel": "Search an email",
|
|
347
357
|
"noLogs": "No logs found for this period",
|
|
348
358
|
"tabs": {
|
|
349
359
|
"all": "All",
|
|
@@ -366,7 +376,8 @@
|
|
|
366
376
|
"action": "Action",
|
|
367
377
|
"time": "Time"
|
|
368
378
|
},
|
|
369
|
-
"errorLabel": "Error"
|
|
379
|
+
"errorLabel": "Error",
|
|
380
|
+
"toggleError": "Show the error detail"
|
|
370
381
|
},
|
|
371
382
|
"logs": {
|
|
372
383
|
"title": "Email Logs",
|
|
@@ -530,8 +541,11 @@
|
|
|
530
541
|
"messageDeletedToast": "Message deleted",
|
|
531
542
|
"split": {
|
|
532
543
|
"subjectPlaceholder": "New ticket subject...",
|
|
544
|
+
"subjectLabel": "Subject of the new ticket",
|
|
533
545
|
"createBtn": "Create ticket"
|
|
534
546
|
},
|
|
547
|
+
"sendAs": "Send as",
|
|
548
|
+
"manualMinutes": "Minutes to add",
|
|
535
549
|
"editMessage": "Edit",
|
|
536
550
|
"editMessageLabel": "Edit message",
|
|
537
551
|
"uploadItem": "Attachment {{name}}",
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
+
"errorBoundary": {
|
|
3
|
+
"title": "Une erreur est survenue",
|
|
4
|
+
"description": "Cette section n'a pas pu s'afficher. Le detail technique est consigne dans la console du navigateur.",
|
|
5
|
+
"retry": "Reessayer"
|
|
6
|
+
},
|
|
2
7
|
"common": {
|
|
3
8
|
"loading": "Chargement...",
|
|
4
9
|
"cancel": "Annuler",
|
|
@@ -144,6 +149,7 @@
|
|
|
144
149
|
"title": "CRM Clients",
|
|
145
150
|
"subtitle": "Gestion des clients et historique",
|
|
146
151
|
"searchPlaceholder": "Rechercher un client...",
|
|
152
|
+
"searchLabel": "Rechercher un client",
|
|
147
153
|
"noClientFound": "Aucun client trouvé",
|
|
148
154
|
"selectClient": "Sélectionnez un client pour voir ses détails",
|
|
149
155
|
"editButton": "Modifier",
|
|
@@ -316,6 +322,8 @@
|
|
|
316
322
|
"resolved": "Résolus",
|
|
317
323
|
"slaBreach": "SLA dépassé"
|
|
318
324
|
},
|
|
325
|
+
"searchLabel": "Rechercher un ticket",
|
|
326
|
+
"sortLabel": "Trier les tickets",
|
|
319
327
|
"sort": {
|
|
320
328
|
"newest": "Plus récent",
|
|
321
329
|
"oldest": "Plus ancien",
|
|
@@ -324,6 +332,7 @@
|
|
|
324
332
|
},
|
|
325
333
|
"selected": "{{count}} sélectionné",
|
|
326
334
|
"selectedPlural": "{{count}} sélectionnés",
|
|
335
|
+
"tabsLabel": "Filtres de la boite de reception",
|
|
327
336
|
"closeAction": "Fermer",
|
|
328
337
|
"reopenAction": "Rouvrir",
|
|
329
338
|
"moreActions": "Plus d'actions...",
|
|
@@ -344,6 +353,7 @@
|
|
|
344
353
|
"emailTracking": {
|
|
345
354
|
"title": "Suivi des emails",
|
|
346
355
|
"searchPlaceholder": "Rechercher par destinataire ou sujet...",
|
|
356
|
+
"searchLabel": "Rechercher un email",
|
|
347
357
|
"noLogs": "Aucun log trouvé pour cette période",
|
|
348
358
|
"tabs": {
|
|
349
359
|
"all": "Tous",
|
|
@@ -366,7 +376,8 @@
|
|
|
366
376
|
"action": "Action",
|
|
367
377
|
"time": "Temps"
|
|
368
378
|
},
|
|
369
|
-
"errorLabel": "Erreur"
|
|
379
|
+
"errorLabel": "Erreur",
|
|
380
|
+
"toggleError": "Afficher le detail de l'erreur"
|
|
370
381
|
},
|
|
371
382
|
"logs": {
|
|
372
383
|
"title": "Logs Email",
|
|
@@ -530,8 +541,11 @@
|
|
|
530
541
|
"messageDeletedToast": "Message supprimé",
|
|
531
542
|
"split": {
|
|
532
543
|
"subjectPlaceholder": "Sujet du nouveau ticket...",
|
|
544
|
+
"subjectLabel": "Sujet du nouveau ticket",
|
|
533
545
|
"createBtn": "Créer le ticket"
|
|
534
546
|
},
|
|
547
|
+
"sendAs": "Envoyer en tant que",
|
|
548
|
+
"manualMinutes": "Minutes a ajouter",
|
|
535
549
|
"editMessage": "Éditer",
|
|
536
550
|
"editMessageLabel": "Éditer le message",
|
|
537
551
|
"uploadItem": "PJ {{name}}",
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { AdminViewServerProps } from 'payload'
|
|
2
|
+
import { formatAdminURL } from 'payload/shared'
|
|
3
|
+
import { SUPPORT_STAFF_SLUG_CONFIG_KEY } from '../../utils/readSettings.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Where a custom admin view must send a caller it refuses, or `null` to render.
|
|
7
|
+
*
|
|
8
|
+
* Payload does NOT gate custom admin views. `RootPage` skips its own
|
|
9
|
+
* `canAccessAdmin` redirect as soon as `isCustomAdminView()` matches, and that
|
|
10
|
+
* helper only compares the request path against the registered `view.path` —
|
|
11
|
+
* it reads no visibility flag, despite what its docblock claims. Authorising a
|
|
12
|
+
* custom view is therefore the view's own job, and every view registered by
|
|
13
|
+
* this plugin sits at a custom path (`/support/inbox`, `/support/ticket`, …).
|
|
14
|
+
*
|
|
15
|
+
* `!req.user` alone is not that check. A single `payload-token` cookie serves
|
|
16
|
+
* every auth collection of the host app, so an ordinary front-office account —
|
|
17
|
+
* a `customers`, `members` or `subscribers` signup — carries one on `/admin`
|
|
18
|
+
* routes too. Such a caller reached `DefaultTemplate` and received the admin
|
|
19
|
+
* chrome together with the client config Payload builds for any authenticated
|
|
20
|
+
* request: the field schema of every collection and global, `admin.hidden`
|
|
21
|
+
* ones included, plus an unfiltered `visibleEntities`.
|
|
22
|
+
*
|
|
23
|
+
* The ticket data itself never travelled — the client components fetch through
|
|
24
|
+
* `/api/support/*`, which `requireAdmin` has guarded all along — but the shape
|
|
25
|
+
* of the whole CMS did.
|
|
26
|
+
*
|
|
27
|
+
* The gate mirrors `requireAdmin` (utils/auth.ts): membership of the staff
|
|
28
|
+
* collection. It additionally honours `canAccessAdmin`, so an account the host
|
|
29
|
+
* disabled through its own `access.admin` is refused here too, and it fails
|
|
30
|
+
* closed on anything it cannot resolve.
|
|
31
|
+
*/
|
|
32
|
+
export function supportViewRedirectTarget(
|
|
33
|
+
initPageResult: AdminViewServerProps['initPageResult'],
|
|
34
|
+
): string | null {
|
|
35
|
+
const req = initPageResult?.req
|
|
36
|
+
const config = req?.payload?.config
|
|
37
|
+
const adminRoute = config?.routes?.admin ?? '/admin'
|
|
38
|
+
const routes = config?.admin?.routes
|
|
39
|
+
|
|
40
|
+
const to = (route: string | undefined, fallback: string) =>
|
|
41
|
+
formatAdminURL({ adminRoute, path: (route ?? fallback) as `/${string}` })
|
|
42
|
+
|
|
43
|
+
if (!req?.user) return to(routes?.login, '/login')
|
|
44
|
+
|
|
45
|
+
// `sanitizePermissions` deletes the key when it is false, so an explicit
|
|
46
|
+
// `false` is a denial while `undefined` simply means "not computed here".
|
|
47
|
+
if (initPageResult?.permissions?.canAccessAdmin === false) {
|
|
48
|
+
return to(routes?.unauthorized, '/unauthorized')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Same source of truth as the write path: the slug `plugin.ts` publishes on
|
|
52
|
+
// `config.custom`, NOT `config.admin.user` — Payload defaults the latter to
|
|
53
|
+
// the app's first auth collection, which on a host declaring a front office
|
|
54
|
+
// first is precisely the collection we are trying to keep out.
|
|
55
|
+
//
|
|
56
|
+
// `resolveStaffPrefSlug` is deliberately NOT used here. Its last-resort
|
|
57
|
+
// fallback is the literal `'users'`, which is the right call on a read path
|
|
58
|
+
// (a wrong scope returns no settings) and the wrong one on an authorisation
|
|
59
|
+
// path: it would admit whoever happens to sit in a collection named `users`
|
|
60
|
+
// on a host where neither source resolved. A gate with nothing to compare
|
|
61
|
+
// against refuses.
|
|
62
|
+
const custom = config?.custom as Record<string, unknown> | undefined
|
|
63
|
+
const registered = custom?.[SUPPORT_STAFF_SLUG_CONFIG_KEY]
|
|
64
|
+
const staffSlug =
|
|
65
|
+
(typeof registered === 'string' && registered) || config?.admin?.user || null
|
|
66
|
+
|
|
67
|
+
const collection = (req.user as { collection?: string }).collection
|
|
68
|
+
if (!staffSlug || !collection || collection !== staffSlug) {
|
|
69
|
+
return to(routes?.unauthorized, '/unauthorized')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ambient stub for 'lucide-react'.
|
|
3
|
-
*
|
|
4
|
-
* lucide-react is a REQUIRED peer dependency — three unconditionally registered
|
|
5
|
-
* components import it statically, so a host without it fails at build time.
|
|
6
|
-
* It is deliberately not a devDependency here; this stub is what lets the plugin
|
|
7
|
-
* type-check in isolation without pulling the icon set into the repo.
|
|
8
|
-
*
|
|
9
|
-
* Keep the icon list below in sync with the actual imports:
|
|
10
|
-
* src/portal/LiveChat.tsx
|
|
11
|
-
* src/views/PendingEmailsView/client.tsx
|
|
12
|
-
* src/views/TicketingSettingsView/client.tsx
|
|
13
|
-
*/
|
|
14
|
-
declare module 'lucide-react' {
|
|
15
|
-
import type { FC, SVGProps } from 'react'
|
|
16
|
-
type LucideIcon = FC<SVGProps<SVGSVGElement> & { size?: number | string; strokeWidth?: number | string }>
|
|
17
|
-
// Export every named icon as LucideIcon so any import resolves cleanly
|
|
18
|
-
const MessageCircle: LucideIcon
|
|
19
|
-
const Send: LucideIcon
|
|
20
|
-
const X: LucideIcon
|
|
21
|
-
const Minimize2: LucideIcon
|
|
22
|
-
const ArrowLeft: LucideIcon
|
|
23
|
-
const Inbox: LucideIcon
|
|
24
|
-
const Plus: LucideIcon
|
|
25
|
-
const Link2: LucideIcon
|
|
26
|
-
const Search: LucideIcon
|
|
27
|
-
const ChevronDown: LucideIcon
|
|
28
|
-
const ChevronUp: LucideIcon
|
|
29
|
-
const Paperclip: LucideIcon
|
|
30
|
-
const Settings: LucideIcon
|
|
31
|
-
const Mail: LucideIcon
|
|
32
|
-
const Bot: LucideIcon
|
|
33
|
-
const Clock: LucideIcon
|
|
34
|
-
const Timer: LucideIcon
|
|
35
|
-
const Globe: LucideIcon
|
|
36
|
-
const FileSignature: LucideIcon
|
|
37
|
-
export {
|
|
38
|
-
MessageCircle, Send, X, Minimize2, ArrowLeft,
|
|
39
|
-
Inbox, Plus, Link2, Search, ChevronDown, ChevronUp, Paperclip,
|
|
40
|
-
Settings, Mail, Bot, Clock, Timer, Globe, FileSignature,
|
|
41
|
-
}
|
|
42
|
-
}
|