@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
@@ -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
- return this.props.children
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
- textSecondary: 'var(--theme-elevation-500)',
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
- blue: '#2563eb',
12
- amber: '#d97706',
13
- orange: '#ea580c',
14
- green: '#16a34a',
15
- red: '#dc2626',
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: '#d97706',
29
+ yellow: '#b45309',
19
30
  } as const
20
31
 
21
- // Button style factory
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
- }