@byline/admin 2.5.2 → 2.6.1

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 (260) hide show
  1. package/dist/fields/array/array-field.d.ts +14 -0
  2. package/dist/fields/array/array-field.js +177 -0
  3. package/dist/fields/array/array-field.module.js +11 -0
  4. package/dist/fields/array/array-field_module.css +32 -0
  5. package/dist/fields/blocks/blocks-field.d.ts +13 -0
  6. package/dist/fields/blocks/blocks-field.js +245 -0
  7. package/dist/fields/blocks/blocks-field.module.js +26 -0
  8. package/dist/fields/blocks/blocks-field_module.css +107 -0
  9. package/dist/fields/checkbox/checkbox-field.d.ts +16 -0
  10. package/dist/fields/checkbox/checkbox-field.js +28 -0
  11. package/dist/fields/checkbox/checkbox-field.module.js +6 -0
  12. package/dist/fields/checkbox/checkbox-field_module.css +4 -0
  13. package/dist/fields/column-formatter.d.ts +20 -0
  14. package/dist/fields/column-formatter.js +15 -0
  15. package/dist/fields/date-time-formatter.d.ts +16 -0
  16. package/dist/fields/date-time-formatter.js +8 -0
  17. package/dist/fields/datetime/datetime-field.d.ts +16 -0
  18. package/dist/fields/datetime/datetime-field.js +37 -0
  19. package/dist/fields/datetime/datetime-field.module.js +5 -0
  20. package/dist/fields/datetime/datetime-field_module.css +4 -0
  21. package/dist/fields/draggable-context-menu.d.ts +6 -0
  22. package/dist/fields/draggable-context-menu.js +85 -0
  23. package/dist/fields/draggable-context-menu.module.js +15 -0
  24. package/dist/fields/draggable-context-menu_module.css +91 -0
  25. package/dist/fields/field-helpers.d.ts +26 -0
  26. package/dist/fields/field-helpers.js +50 -0
  27. package/dist/fields/field-renderer.d.ts +37 -0
  28. package/dist/fields/field-renderer.js +206 -0
  29. package/dist/fields/field-renderer.module.js +8 -0
  30. package/dist/fields/field-renderer_module.css +11 -0
  31. package/dist/fields/field-services-context.d.ts +16 -0
  32. package/dist/fields/field-services-context.js +13 -0
  33. package/dist/fields/field-services-types.d.ts +63 -0
  34. package/dist/fields/field-services-types.js +1 -0
  35. package/dist/fields/file/file-field.d.ts +19 -0
  36. package/dist/fields/file/file-field.js +225 -0
  37. package/dist/fields/file/file-field.module.js +18 -0
  38. package/dist/fields/file/file-field_module.css +131 -0
  39. package/dist/fields/file/file-upload-field.d.ts +21 -0
  40. package/dist/fields/file/file-upload-field.js +130 -0
  41. package/dist/fields/file/file-upload-field.module.js +15 -0
  42. package/dist/fields/file/file-upload-field_module.css +74 -0
  43. package/dist/fields/group/group-field.d.ts +15 -0
  44. package/dist/fields/group/group-field.js +59 -0
  45. package/dist/fields/group/group-field.module.js +9 -0
  46. package/dist/fields/group/group-field_module.css +27 -0
  47. package/dist/fields/image/image-field.d.ts +19 -0
  48. package/dist/fields/image/image-field.js +241 -0
  49. package/dist/fields/image/image-field.module.js +22 -0
  50. package/dist/fields/image/image-field_module.css +121 -0
  51. package/dist/fields/image/image-upload-field.d.ts +21 -0
  52. package/dist/fields/image/image-upload-field.js +190 -0
  53. package/dist/fields/image/image-upload-field.module.js +19 -0
  54. package/dist/fields/image/image-upload-field_module.css +92 -0
  55. package/dist/fields/local-date-time.d.ts +27 -0
  56. package/dist/fields/local-date-time.js +49 -0
  57. package/dist/fields/locale-badge.d.ts +18 -0
  58. package/dist/fields/locale-badge.js +10 -0
  59. package/dist/fields/locale-badge.module.js +5 -0
  60. package/dist/fields/locale-badge_module.css +27 -0
  61. package/dist/fields/numerical/numerical-field.d.ts +18 -0
  62. package/dist/fields/numerical/numerical-field.js +74 -0
  63. package/dist/fields/relation/relation-display.d.ts +40 -0
  64. package/dist/fields/relation/relation-display.js +61 -0
  65. package/dist/fields/relation/relation-display.module.js +9 -0
  66. package/dist/fields/relation/relation-display_module.css +21 -0
  67. package/dist/fields/relation/relation-field.d.ts +18 -0
  68. package/dist/fields/relation/relation-field.js +138 -0
  69. package/dist/fields/relation/relation-field.module.js +13 -0
  70. package/dist/fields/relation/relation-field_module.css +62 -0
  71. package/dist/fields/relation/relation-picker.d.ts +59 -0
  72. package/dist/fields/relation/relation-picker.js +237 -0
  73. package/dist/fields/relation/relation-picker.module.js +26 -0
  74. package/dist/fields/relation/relation-picker_module.css +124 -0
  75. package/dist/fields/relation/relation-summary.d.ts +31 -0
  76. package/dist/fields/relation/relation-summary.js +50 -0
  77. package/dist/fields/relation/relation-summary.module.js +11 -0
  78. package/dist/fields/relation/relation-summary_module.css +37 -0
  79. package/dist/fields/select/select-field.d.ts +16 -0
  80. package/dist/fields/select/select-field.js +50 -0
  81. package/dist/fields/select/select-field.module.js +5 -0
  82. package/dist/fields/select/select-field_module.css +4 -0
  83. package/dist/fields/sortable-item.d.ts +15 -0
  84. package/dist/fields/sortable-item.js +81 -0
  85. package/dist/fields/sortable-item.module.js +22 -0
  86. package/dist/fields/sortable-item_module.css +124 -0
  87. package/dist/fields/text/text-field.d.ts +20 -0
  88. package/dist/fields/text/text-field.js +104 -0
  89. package/dist/fields/text/text-field.module.js +6 -0
  90. package/dist/fields/text/text-field_module.css +5 -0
  91. package/dist/fields/text-area/text-area-field.d.ts +20 -0
  92. package/dist/fields/text-area/text-area-field.js +105 -0
  93. package/dist/fields/text-area/text-area-field.module.js +6 -0
  94. package/dist/fields/text-area/text-area-field_module.css +5 -0
  95. package/dist/fields/use-field-change-handler.d.ts +23 -0
  96. package/dist/fields/use-field-change-handler.js +52 -0
  97. package/dist/forms/document-actions.d.ts +48 -0
  98. package/dist/forms/document-actions.js +475 -0
  99. package/dist/forms/document-actions.module.js +34 -0
  100. package/dist/forms/document-actions_module.css +118 -0
  101. package/dist/forms/form-context.d.ts +89 -0
  102. package/dist/forms/form-context.js +466 -0
  103. package/dist/forms/form-renderer.d.ts +98 -0
  104. package/dist/forms/form-renderer.js +597 -0
  105. package/dist/forms/form-renderer.module.js +46 -0
  106. package/dist/forms/form-renderer_module.css +245 -0
  107. package/dist/forms/navigation-guard.d.ts +54 -0
  108. package/dist/forms/navigation-guard.js +22 -0
  109. package/dist/forms/path-widget.d.ts +36 -0
  110. package/dist/forms/path-widget.js +116 -0
  111. package/dist/forms/path-widget.module.js +8 -0
  112. package/dist/forms/path-widget_module.css +29 -0
  113. package/dist/forms/upload-executor.d.ts +57 -0
  114. package/dist/forms/upload-executor.js +94 -0
  115. package/dist/lib/translate-validation-error.d.ts +36 -0
  116. package/dist/lib/translate-validation-error.js +11 -0
  117. package/dist/modules/admin-account/commands.d.ts +2 -1
  118. package/dist/modules/admin-account/commands.js +13 -2
  119. package/dist/modules/admin-account/components/change-password.js +45 -36
  120. package/dist/modules/admin-account/components/container.js +185 -134
  121. package/dist/modules/admin-account/components/preferences.d.ts +8 -0
  122. package/dist/modules/admin-account/components/preferences.js +152 -0
  123. package/dist/modules/admin-account/components/preferences.module.js +11 -0
  124. package/dist/modules/admin-account/components/preferences_module.css +41 -0
  125. package/dist/modules/admin-account/components/update.js +50 -31
  126. package/dist/modules/admin-account/index.d.ts +3 -3
  127. package/dist/modules/admin-account/index.js +2 -2
  128. package/dist/modules/admin-account/schemas.d.ts +4 -0
  129. package/dist/modules/admin-account/schemas.js +4 -1
  130. package/dist/modules/admin-account/service.d.ts +1 -0
  131. package/dist/modules/admin-account/service.js +8 -0
  132. package/dist/modules/admin-permissions/components/inspector.js +31 -41
  133. package/dist/modules/admin-roles/components/create.js +43 -26
  134. package/dist/modules/admin-roles/components/permissions.js +26 -35
  135. package/dist/modules/admin-roles/components/update.js +26 -16
  136. package/dist/modules/admin-users/components/create.js +60 -40
  137. package/dist/modules/admin-users/components/roles.js +9 -15
  138. package/dist/modules/admin-users/components/set-password.js +30 -31
  139. package/dist/modules/admin-users/components/update.js +58 -39
  140. package/dist/modules/admin-users/dto.js +1 -0
  141. package/dist/modules/admin-users/repository.d.ts +17 -0
  142. package/dist/modules/admin-users/schemas.d.ts +4 -0
  143. package/dist/modules/admin-users/schemas.js +6 -2
  144. package/dist/modules/auth/components/sign-in-form.js +10 -8
  145. package/dist/presentation/group.d.ts +27 -0
  146. package/dist/presentation/group.js +14 -0
  147. package/dist/presentation/group.module.js +6 -0
  148. package/dist/presentation/group_module.css +19 -0
  149. package/dist/presentation/row.d.ts +25 -0
  150. package/dist/presentation/row.js +8 -0
  151. package/dist/presentation/row.module.js +5 -0
  152. package/dist/presentation/row_module.css +18 -0
  153. package/dist/presentation/tabs.d.ts +25 -0
  154. package/dist/presentation/tabs.js +39 -0
  155. package/dist/presentation/tabs.module.js +10 -0
  156. package/dist/presentation/tabs_module.css +68 -0
  157. package/dist/react.d.ts +66 -0
  158. package/dist/react.js +36 -0
  159. package/dist/services/admin-services-types.d.ts +16 -0
  160. package/dist/widgets/diff-viewer/diff-modal.d.ts +22 -0
  161. package/dist/widgets/diff-viewer/diff-modal.js +149 -0
  162. package/dist/widgets/diff-viewer/diff-modal.module.js +14 -0
  163. package/dist/widgets/diff-viewer/diff-modal_module.css +56 -0
  164. package/dist/widgets/status-badge/status-badge.d.ts +25 -0
  165. package/dist/widgets/status-badge/status-badge.js +37 -0
  166. package/dist/widgets/status-badge/status-badge.module.js +7 -0
  167. package/dist/widgets/status-badge/status-badge_module.css +20 -0
  168. package/package.json +14 -4
  169. package/src/fields/array/array-field.module.css +48 -0
  170. package/src/fields/array/array-field.tsx +267 -0
  171. package/src/fields/blocks/blocks-field.module.css +148 -0
  172. package/src/fields/blocks/blocks-field.tsx +323 -0
  173. package/src/fields/checkbox/checkbox-field.module.css +4 -0
  174. package/src/fields/checkbox/checkbox-field.tsx +54 -0
  175. package/src/fields/column-formatter.tsx +31 -0
  176. package/src/fields/date-time-formatter.tsx +22 -0
  177. package/src/fields/datetime/datetime-field.module.css +13 -0
  178. package/src/fields/datetime/datetime-field.tsx +54 -0
  179. package/src/fields/draggable-context-menu.module.css +127 -0
  180. package/src/fields/draggable-context-menu.tsx +87 -0
  181. package/src/fields/field-helpers.ts +69 -0
  182. package/src/fields/field-renderer.module.css +22 -0
  183. package/src/fields/field-renderer.tsx +288 -0
  184. package/src/fields/field-services-context.tsx +35 -0
  185. package/src/fields/field-services-types.ts +68 -0
  186. package/src/fields/file/file-field.module.css +153 -0
  187. package/src/fields/file/file-field.tsx +286 -0
  188. package/src/fields/file/file-upload-field.module.css +101 -0
  189. package/src/fields/file/file-upload-field.tsx +187 -0
  190. package/src/fields/group/group-field.module.css +43 -0
  191. package/src/fields/group/group-field.tsx +84 -0
  192. package/src/fields/image/image-field.module.css +155 -0
  193. package/src/fields/image/image-field.tsx +306 -0
  194. package/src/fields/image/image-upload-field.module.css +123 -0
  195. package/src/fields/image/image-upload-field.tsx +276 -0
  196. package/src/fields/local-date-time.tsx +88 -0
  197. package/src/fields/locale-badge.module.css +37 -0
  198. package/src/fields/locale-badge.tsx +32 -0
  199. package/src/fields/numerical/numerical-field.tsx +114 -0
  200. package/src/fields/relation/relation-display.module.css +36 -0
  201. package/src/fields/relation/relation-display.tsx +138 -0
  202. package/src/fields/relation/relation-field.module.css +83 -0
  203. package/src/fields/relation/relation-field.tsx +211 -0
  204. package/src/fields/relation/relation-picker.module.css +168 -0
  205. package/src/fields/relation/relation-picker.tsx +343 -0
  206. package/src/fields/relation/relation-summary.module.css +55 -0
  207. package/src/fields/relation/relation-summary.tsx +123 -0
  208. package/src/fields/select/select-field.module.css +13 -0
  209. package/src/fields/select/select-field.tsx +61 -0
  210. package/src/fields/sortable-item.module.css +167 -0
  211. package/src/fields/sortable-item.tsx +106 -0
  212. package/src/fields/text/text-field.module.css +13 -0
  213. package/src/fields/text/text-field.tsx +146 -0
  214. package/src/fields/text-area/text-area-field.module.css +13 -0
  215. package/src/fields/text-area/text-area-field.tsx +147 -0
  216. package/src/fields/use-field-change-handler.ts +112 -0
  217. package/src/forms/document-actions.module.css +160 -0
  218. package/src/forms/document-actions.tsx +482 -0
  219. package/src/forms/form-context.tsx +704 -0
  220. package/src/forms/form-renderer.module.css +321 -0
  221. package/src/forms/form-renderer.tsx +891 -0
  222. package/src/forms/navigation-guard.tsx +98 -0
  223. package/src/forms/path-widget.module.css +41 -0
  224. package/src/forms/path-widget.test.tsx +217 -0
  225. package/src/forms/path-widget.tsx +183 -0
  226. package/src/forms/upload-executor.ts +192 -0
  227. package/src/lib/translate-validation-error.ts +56 -0
  228. package/src/modules/admin-account/commands.ts +13 -0
  229. package/src/modules/admin-account/components/change-password.tsx +46 -31
  230. package/src/modules/admin-account/components/container.tsx +83 -38
  231. package/src/modules/admin-account/components/preferences.module.css +60 -0
  232. package/src/modules/admin-account/components/preferences.tsx +203 -0
  233. package/src/modules/admin-account/components/update.tsx +53 -27
  234. package/src/modules/admin-account/index.ts +3 -0
  235. package/src/modules/admin-account/schemas.ts +13 -0
  236. package/src/modules/admin-account/service.ts +12 -0
  237. package/src/modules/admin-permissions/components/inspector.tsx +22 -14
  238. package/src/modules/admin-roles/components/create.tsx +51 -23
  239. package/src/modules/admin-roles/components/permissions.tsx +25 -21
  240. package/src/modules/admin-roles/components/update.tsx +37 -19
  241. package/src/modules/admin-users/components/create.tsx +63 -34
  242. package/src/modules/admin-users/components/roles.tsx +9 -8
  243. package/src/modules/admin-users/components/set-password.tsx +34 -28
  244. package/src/modules/admin-users/components/update.tsx +58 -36
  245. package/src/modules/admin-users/dto.ts +1 -0
  246. package/src/modules/admin-users/repository.ts +17 -0
  247. package/src/modules/admin-users/schemas.ts +12 -0
  248. package/src/modules/auth/components/sign-in-form.tsx +14 -8
  249. package/src/presentation/group.module.css +41 -0
  250. package/src/presentation/group.tsx +40 -0
  251. package/src/presentation/row.module.css +32 -0
  252. package/src/presentation/row.tsx +33 -0
  253. package/src/presentation/tabs.module.css +107 -0
  254. package/src/presentation/tabs.tsx +84 -0
  255. package/src/react.ts +84 -0
  256. package/src/services/admin-services-types.ts +18 -0
  257. package/src/widgets/diff-viewer/diff-modal.module.css +79 -0
  258. package/src/widgets/diff-viewer/diff-modal.tsx +186 -0
  259. package/src/widgets/status-badge/status-badge.module.css +31 -0
  260. package/src/widgets/status-badge/status-badge.tsx +71 -0
@@ -0,0 +1,192 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ /**
10
+ * Upload Executor
11
+ *
12
+ * Handles batch execution of pending file uploads at form submission time.
13
+ * This enables "deferred uploads" — files are selected/previewed immediately
14
+ * but only uploaded when the user clicks Save.
15
+ */
16
+
17
+ import type { StoredFileValue } from '@byline/core'
18
+
19
+ import type { UploadFieldFn } from '../fields/field-services-types'
20
+ import type { PendingUpload } from './form-context'
21
+
22
+ export interface UploadResult {
23
+ fieldPath: string
24
+ success: boolean
25
+ storedFile?: StoredFileValue
26
+ error?: string
27
+ }
28
+
29
+ export interface ExecuteUploadsResult {
30
+ /** All upload results (both successful and failed) */
31
+ results: UploadResult[]
32
+ /** Map of field path to StoredFileValue for successful uploads */
33
+ successful: Map<string, StoredFileValue>
34
+ /** Map of field path to error message for failed uploads */
35
+ errors: Map<string, string>
36
+ /** Whether all uploads succeeded */
37
+ allSucceeded: boolean
38
+ }
39
+
40
+ /**
41
+ * Execute all pending uploads sequentially.
42
+ * Returns a result object with successful uploads and any errors.
43
+ *
44
+ * @param pendingUploads - Map of field path to PendingUpload
45
+ * @param uploadField - Host-provided upload transport (resolved via
46
+ * `useBylineFieldServices()` in the calling React tree)
47
+ * @returns Promise resolving to ExecuteUploadsResult
48
+ */
49
+ export async function executeUploads(
50
+ pendingUploads: Map<string, PendingUpload>,
51
+ uploadField: UploadFieldFn
52
+ ): Promise<ExecuteUploadsResult> {
53
+ const results: UploadResult[] = []
54
+ const successful = new Map<string, StoredFileValue>()
55
+ const errors = new Map<string, string>()
56
+
57
+ for (const [fieldPath, upload] of pendingUploads.entries()) {
58
+ const formData = new FormData()
59
+ formData.append('file', upload.file)
60
+ // Tell the server which upload-capable field this file belongs to.
61
+ // With per-field upload config a collection can have multiple
62
+ // image/file fields, each with its own constraints; the server's
63
+ // unique-default fallback covers the single-field case but rejects
64
+ // multi-field collections without an explicit selector.
65
+ formData.append('field', uploadFieldName(fieldPath))
66
+
67
+ try {
68
+ // Pass createDocument=false — we're uploading for an embedded field,
69
+ // the form's save action handles document creation/update.
70
+ const result = await uploadField(upload.collectionPath, formData, false)
71
+
72
+ results.push({
73
+ fieldPath,
74
+ success: true,
75
+ storedFile: result.storedFile,
76
+ })
77
+ successful.set(fieldPath, result.storedFile)
78
+ } catch (err: unknown) {
79
+ const message = err instanceof Error ? err.message : 'Upload failed'
80
+ results.push({
81
+ fieldPath,
82
+ success: false,
83
+ error: message,
84
+ })
85
+ errors.set(fieldPath, message)
86
+ }
87
+ }
88
+
89
+ return {
90
+ results,
91
+ successful,
92
+ errors,
93
+ allSucceeded: errors.size === 0,
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Extract the leaf field name from a `fieldPath`. Top-level upload
99
+ * fields (`'image'`, `'avatar'`) pass through unchanged; nested paths
100
+ * (`'profile.avatar'`) reduce to their last segment, since the
101
+ * server-side resolver matches against top-level field names today.
102
+ * Nested upload fields would need a richer transport selector when
103
+ * they land — the host resolver is the natural place to extend.
104
+ */
105
+ function uploadFieldName(fieldPath: string): string {
106
+ const dot = fieldPath.lastIndexOf('.')
107
+ return dot === -1 ? fieldPath : fieldPath.slice(dot + 1)
108
+ }
109
+
110
+ /**
111
+ * Progress callback type for upload execution with progress tracking.
112
+ */
113
+ export type UploadProgressCallback = (info: {
114
+ current: number
115
+ total: number
116
+ fieldPath: string
117
+ status: 'uploading' | 'done' | 'error'
118
+ }) => void
119
+
120
+ /**
121
+ * Execute uploads with progress callbacks.
122
+ * Useful for showing upload progress in the UI.
123
+ */
124
+ export async function executeUploadsWithProgress(
125
+ pendingUploads: Map<string, PendingUpload>,
126
+ uploadField: UploadFieldFn,
127
+ onProgress?: UploadProgressCallback
128
+ ): Promise<ExecuteUploadsResult> {
129
+ const results: UploadResult[] = []
130
+ const successful = new Map<string, StoredFileValue>()
131
+ const errors = new Map<string, string>()
132
+
133
+ const entries = Array.from(pendingUploads.entries())
134
+ const total = entries.length
135
+
136
+ for (let i = 0; i < entries.length; i++) {
137
+ const entry = entries[i]
138
+ if (!entry) continue
139
+ const [fieldPath, upload] = entry
140
+
141
+ onProgress?.({
142
+ current: i + 1,
143
+ total,
144
+ fieldPath,
145
+ status: 'uploading',
146
+ })
147
+
148
+ const formData = new FormData()
149
+ formData.append('file', upload.file)
150
+ formData.append('field', uploadFieldName(fieldPath))
151
+
152
+ try {
153
+ const result = await uploadField(upload.collectionPath, formData, false)
154
+
155
+ results.push({
156
+ fieldPath,
157
+ success: true,
158
+ storedFile: result.storedFile,
159
+ })
160
+ successful.set(fieldPath, result.storedFile)
161
+
162
+ onProgress?.({
163
+ current: i + 1,
164
+ total,
165
+ fieldPath,
166
+ status: 'done',
167
+ })
168
+ } catch (err: unknown) {
169
+ const message = err instanceof Error ? err.message : 'Upload failed'
170
+ results.push({
171
+ fieldPath,
172
+ success: false,
173
+ error: message,
174
+ })
175
+ errors.set(fieldPath, message)
176
+
177
+ onProgress?.({
178
+ current: i + 1,
179
+ total,
180
+ fieldPath,
181
+ status: 'error',
182
+ })
183
+ }
184
+ }
185
+
186
+ return {
187
+ results,
188
+ successful,
189
+ errors,
190
+ allSucceeded: errors.size === 0,
191
+ }
192
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ /**
10
+ * Map a stable validation-error code (emitted by a `@byline/core/validation`
11
+ * schema) onto its translation. Returns the original input unchanged when
12
+ * the code is not in the map, so non-coded error messages (free-form
13
+ * Zod messages, server-supplied strings) pass through.
14
+ *
15
+ * Why a separate translator: `@byline/core` stays i18n-agnostic — schemas
16
+ * emit codes, not text — so it has no dependency on `@byline/i18n` or
17
+ * any locale data. This package owns the translation surface and keeps
18
+ * the mapping in one place, so callers across the admin shell don't
19
+ * each maintain their own code-to-key lookup.
20
+ *
21
+ * Adding a new code: extend the source schema in `@byline/core/validation`
22
+ * and add the matching key here.
23
+ */
24
+
25
+ import type { UseTranslationReturn } from '@byline/i18n/react'
26
+
27
+ type Translate = UseTranslationReturn['t']
28
+
29
+ /**
30
+ * Maps validation error codes from `@byline/core/validation` schemas to
31
+ * their corresponding `byline-admin` translation keys.
32
+ */
33
+ const VALIDATION_CODE_KEYS: Record<string, string> = {
34
+ 'password.tooShort': 'validation.password.tooShort',
35
+ 'password.tooLong': 'validation.password.tooLong',
36
+ 'password.complexity': 'validation.password.complexity',
37
+ }
38
+
39
+ /**
40
+ * Translate a validation error code into the active locale's message.
41
+ *
42
+ * Pass the raw error string straight out of Zod (e.g. via
43
+ * `firstError(field.state.meta.errors)`). When the string matches a
44
+ * known code, the corresponding translation is returned; otherwise the
45
+ * input flows through unchanged.
46
+ *
47
+ * Designed for the `errorText={…}` slot on form inputs.
48
+ */
49
+ export function translateValidationError(
50
+ t: Translate,
51
+ message: string | undefined
52
+ ): string | undefined {
53
+ if (message == null) return message
54
+ const key = VALIDATION_CODE_KEYS[message]
55
+ return key ? t(key) : message
56
+ }
@@ -11,6 +11,7 @@ import { adminUserResponseSchema } from '../admin-users/schemas.js'
11
11
  import {
12
12
  changeAccountPasswordRequestSchema,
13
13
  getAccountRequestSchema,
14
+ setPreferredLocaleRequestSchema,
14
15
  updateAccountRequestSchema,
15
16
  } from './schemas.js'
16
17
  import { AdminAccountService } from './service.js'
@@ -19,6 +20,7 @@ import type {
19
20
  AccountResponse,
20
21
  ChangeAccountPasswordRequest,
21
22
  GetAccountRequest,
23
+ SetPreferredLocaleRequest,
22
24
  UpdateAccountRequest,
23
25
  } from './schemas.js'
24
26
 
@@ -74,3 +76,14 @@ export const changeAccountPasswordCommand: Command<
74
76
  schemas: { input: changeAccountPasswordRequestSchema, output: adminUserResponseSchema },
75
77
  handler: ({ input, deps, actor }) => serviceOf(deps).changePassword(actor.id, input),
76
78
  })
79
+
80
+ export const setPreferredLocaleCommand: Command<
81
+ SetPreferredLocaleRequest,
82
+ AccountResponse,
83
+ AdminAccountCommandDeps
84
+ > = createCommand({
85
+ method: 'setPreferredLocale',
86
+ auth: { authenticated: true },
87
+ schemas: { input: setPreferredLocaleRequestSchema, output: adminUserResponseSchema },
88
+ handler: ({ input, deps, actor }) => serviceOf(deps).setPreferredLocale(actor.id, input.locale),
89
+ })
@@ -25,30 +25,25 @@
25
25
  * will close that gap.
26
26
  */
27
27
 
28
- import { useState } from 'react'
28
+ import { useMemo, useState } from 'react'
29
29
  import { revalidateLogic, useForm } from '@tanstack/react-form-start'
30
30
 
31
31
  import { passwordSchema } from '@byline/core/validation'
32
+ import { useTranslation } from '@byline/i18n/react'
32
33
  import { Alert, Button, InputPassword, LoaderEllipsis } from '@byline/ui/react'
33
34
  import cx from 'classnames'
34
35
  import { z } from 'zod'
35
36
 
37
+ import { translateValidationError } from '../../../lib/translate-validation-error.js'
36
38
  import { useBylineAdminServices } from '../../../services/admin-services-context.js'
37
39
  import styles from './change-password.module.css'
38
40
  import type { AccountResponse } from '../index.js'
39
41
 
40
- const changePasswordFormSchema = z
41
- .object({
42
- currentPassword: z.string().min(1, { message: 'Please enter your current password' }),
43
- newPassword: passwordSchema,
44
- confirm: z.string({ message: 'Please confirm the new password' }),
45
- })
46
- .refine((v) => v.newPassword === v.confirm, {
47
- message: 'New passwords do not match',
48
- path: ['confirm'],
49
- })
50
-
51
- type ChangePasswordValues = z.infer<typeof changePasswordFormSchema>
42
+ type ChangePasswordValues = {
43
+ currentPassword: string
44
+ newPassword: string
45
+ confirm: string
46
+ }
52
47
 
53
48
  interface ChangePasswordProps {
54
49
  account: AccountResponse
@@ -58,9 +53,33 @@ interface ChangePasswordProps {
58
53
 
59
54
  export function ChangeAccountPassword({ account, onClose, onSuccess }: ChangePasswordProps) {
60
55
  const { changeAccountPassword } = useBylineAdminServices()
56
+ const { t } = useTranslation('byline-admin')
61
57
  const [formError, setFormError] = useState<string | null>(null)
62
58
  const [successMessage, setSuccessMessage] = useState<string | null>(null)
63
59
 
60
+ // Schema rebuilt per-render so error messages reflect the active
61
+ // locale. `passwordSchema` from `@byline/core/validation` emits stable
62
+ // error codes — `translateValidationError(t, …)` below maps them onto
63
+ // the active locale at render time.
64
+ const changePasswordFormSchema = useMemo(
65
+ () =>
66
+ z
67
+ .object({
68
+ currentPassword: z
69
+ .string()
70
+ .min(1, { message: t('account.changePassword.errors.currentRequired') }),
71
+ newPassword: passwordSchema,
72
+ confirm: z.string({
73
+ message: t('account.changePassword.errors.confirmRequired'),
74
+ }),
75
+ })
76
+ .refine((v) => v.newPassword === v.confirm, {
77
+ message: t('account.changePassword.errors.mismatch'),
78
+ path: ['confirm'],
79
+ }),
80
+ [t]
81
+ )
82
+
64
83
  const form = useForm({
65
84
  defaultValues: { currentPassword: '', newPassword: '', confirm: '' } as ChangePasswordValues,
66
85
  validationLogic: revalidateLogic({
@@ -81,30 +100,29 @@ export function ChangeAccountPassword({ account, onClose, onSuccess }: ChangePas
81
100
  newPassword: value.newPassword,
82
101
  },
83
102
  })
84
- setSuccessMessage('Password updated.')
103
+ setSuccessMessage(t('account.changePassword.feedback.updated'))
85
104
  form.reset({ currentPassword: '', newPassword: '', confirm: '' })
86
105
  onSuccess?.(updated)
87
106
  } catch (err) {
88
107
  const code = getErrorCode(err)
89
108
  if (code === 'admin.account.invalidCurrentPassword') {
109
+ const message = t('account.changePassword.errors.currentIncorrect')
90
110
  form.setFieldMeta('currentPassword', (meta) => ({
91
111
  ...meta,
92
- errorMap: { ...meta.errorMap, onServer: 'Current password is incorrect.' },
93
- errors: ['Current password is incorrect.'],
112
+ errorMap: { ...meta.errorMap, onServer: message },
113
+ errors: [message],
94
114
  }))
95
115
  return
96
116
  }
97
117
  if (code === 'admin.users.versionConflict') {
98
- setFormError(
99
- 'Your account has been modified elsewhere since you opened this form. Reload to refresh and try again.'
100
- )
118
+ setFormError(t('common.errors.versionConflict'))
101
119
  return
102
120
  }
103
121
  if (code === 'admin.account.notFound') {
104
- setFormError('Your admin account could not be found. Please sign in again.')
122
+ setFormError(t('common.errors.accountNotFound'))
105
123
  return
106
124
  }
107
- setFormError('Could not change the password. Please try again.')
125
+ setFormError(t('account.changePassword.errors.couldNotChange'))
108
126
  }
109
127
  },
110
128
  })
@@ -123,15 +141,12 @@ export function ChangeAccountPassword({ account, onClose, onSuccess }: ChangePas
123
141
  {formError ? <Alert intent="danger">{formError}</Alert> : null}
124
142
  {successMessage ? <Alert intent="success">{successMessage}</Alert> : null}
125
143
 
126
- <p className="muted">
127
- Other active sessions will continue to work until their tokens expire. Sign out elsewhere
128
- if you suspect another device has been compromised.
129
- </p>
144
+ <p className="muted">{t('account.changePassword.intro')}</p>
130
145
 
131
146
  <form.Field name="currentPassword">
132
147
  {(field) => (
133
148
  <InputPassword
134
- label="Current password"
149
+ label={t('account.changePassword.fields.current')}
135
150
  id="currentPassword"
136
151
  name={field.name}
137
152
  value={field.state.value}
@@ -148,14 +163,14 @@ export function ChangeAccountPassword({ account, onClose, onSuccess }: ChangePas
148
163
  <form.Field name="newPassword">
149
164
  {(field) => (
150
165
  <InputPassword
151
- label="New password"
166
+ label={t('account.changePassword.fields.new')}
152
167
  id="newPassword"
153
168
  name={field.name}
154
169
  value={field.state.value}
155
170
  onBlur={field.handleBlur}
156
171
  onChange={(e) => field.handleChange(e.currentTarget.value)}
157
172
  error={field.state.meta.errors.length > 0}
158
- errorText={firstError(field.state.meta.errors)}
173
+ errorText={translateValidationError(t, firstError(field.state.meta.errors))}
159
174
  autoComplete="new-password"
160
175
  required
161
176
  />
@@ -165,7 +180,7 @@ export function ChangeAccountPassword({ account, onClose, onSuccess }: ChangePas
165
180
  <form.Field name="confirm">
166
181
  {(field) => (
167
182
  <InputPassword
168
- label="Confirm new password"
183
+ label={t('account.changePassword.fields.confirm')}
169
184
  id="confirm"
170
185
  name={field.name}
171
186
  value={field.state.value}
@@ -187,7 +202,7 @@ export function ChangeAccountPassword({ account, onClose, onSuccess }: ChangePas
187
202
  onClick={onClose}
188
203
  className={cx('byline-account-change-password-action', styles.action)}
189
204
  >
190
- {successMessage ? 'Close' : 'Cancel'}
205
+ {successMessage ? t('common.actions.close') : t('common.actions.cancel')}
191
206
  </Button>
192
207
  <form.Subscribe
193
208
  selector={(state) => ({
@@ -204,7 +219,7 @@ export function ChangeAccountPassword({ account, onClose, onSuccess }: ChangePas
204
219
  disabled={!canSubmit || isSubmitting}
205
220
  className={cx('byline-account-change-password-action', styles.action)}
206
221
  >
207
- {isSubmitting === true ? <LoaderEllipsis size={42} /> : 'Save'}
222
+ {isSubmitting === true ? <LoaderEllipsis size={42} /> : t('common.actions.save')}
208
223
  </Button>
209
224
  )}
210
225
  </form.Subscribe>