@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.
- package/dist/fields/array/array-field.d.ts +14 -0
- package/dist/fields/array/array-field.js +177 -0
- package/dist/fields/array/array-field.module.js +11 -0
- package/dist/fields/array/array-field_module.css +32 -0
- package/dist/fields/blocks/blocks-field.d.ts +13 -0
- package/dist/fields/blocks/blocks-field.js +245 -0
- package/dist/fields/blocks/blocks-field.module.js +26 -0
- package/dist/fields/blocks/blocks-field_module.css +107 -0
- package/dist/fields/checkbox/checkbox-field.d.ts +16 -0
- package/dist/fields/checkbox/checkbox-field.js +28 -0
- package/dist/fields/checkbox/checkbox-field.module.js +6 -0
- package/dist/fields/checkbox/checkbox-field_module.css +4 -0
- package/dist/fields/column-formatter.d.ts +20 -0
- package/dist/fields/column-formatter.js +15 -0
- package/dist/fields/date-time-formatter.d.ts +16 -0
- package/dist/fields/date-time-formatter.js +8 -0
- package/dist/fields/datetime/datetime-field.d.ts +16 -0
- package/dist/fields/datetime/datetime-field.js +37 -0
- package/dist/fields/datetime/datetime-field.module.js +5 -0
- package/dist/fields/datetime/datetime-field_module.css +4 -0
- package/dist/fields/draggable-context-menu.d.ts +6 -0
- package/dist/fields/draggable-context-menu.js +85 -0
- package/dist/fields/draggable-context-menu.module.js +15 -0
- package/dist/fields/draggable-context-menu_module.css +91 -0
- package/dist/fields/field-helpers.d.ts +26 -0
- package/dist/fields/field-helpers.js +50 -0
- package/dist/fields/field-renderer.d.ts +37 -0
- package/dist/fields/field-renderer.js +206 -0
- package/dist/fields/field-renderer.module.js +8 -0
- package/dist/fields/field-renderer_module.css +11 -0
- package/dist/fields/field-services-context.d.ts +16 -0
- package/dist/fields/field-services-context.js +13 -0
- package/dist/fields/field-services-types.d.ts +63 -0
- package/dist/fields/field-services-types.js +1 -0
- package/dist/fields/file/file-field.d.ts +19 -0
- package/dist/fields/file/file-field.js +225 -0
- package/dist/fields/file/file-field.module.js +18 -0
- package/dist/fields/file/file-field_module.css +131 -0
- package/dist/fields/file/file-upload-field.d.ts +21 -0
- package/dist/fields/file/file-upload-field.js +130 -0
- package/dist/fields/file/file-upload-field.module.js +15 -0
- package/dist/fields/file/file-upload-field_module.css +74 -0
- package/dist/fields/group/group-field.d.ts +15 -0
- package/dist/fields/group/group-field.js +59 -0
- package/dist/fields/group/group-field.module.js +9 -0
- package/dist/fields/group/group-field_module.css +27 -0
- package/dist/fields/image/image-field.d.ts +19 -0
- package/dist/fields/image/image-field.js +241 -0
- package/dist/fields/image/image-field.module.js +22 -0
- package/dist/fields/image/image-field_module.css +121 -0
- package/dist/fields/image/image-upload-field.d.ts +21 -0
- package/dist/fields/image/image-upload-field.js +190 -0
- package/dist/fields/image/image-upload-field.module.js +19 -0
- package/dist/fields/image/image-upload-field_module.css +92 -0
- package/dist/fields/local-date-time.d.ts +27 -0
- package/dist/fields/local-date-time.js +49 -0
- package/dist/fields/locale-badge.d.ts +18 -0
- package/dist/fields/locale-badge.js +10 -0
- package/dist/fields/locale-badge.module.js +5 -0
- package/dist/fields/locale-badge_module.css +27 -0
- package/dist/fields/numerical/numerical-field.d.ts +18 -0
- package/dist/fields/numerical/numerical-field.js +74 -0
- package/dist/fields/relation/relation-display.d.ts +40 -0
- package/dist/fields/relation/relation-display.js +61 -0
- package/dist/fields/relation/relation-display.module.js +9 -0
- package/dist/fields/relation/relation-display_module.css +21 -0
- package/dist/fields/relation/relation-field.d.ts +18 -0
- package/dist/fields/relation/relation-field.js +138 -0
- package/dist/fields/relation/relation-field.module.js +13 -0
- package/dist/fields/relation/relation-field_module.css +62 -0
- package/dist/fields/relation/relation-picker.d.ts +59 -0
- package/dist/fields/relation/relation-picker.js +237 -0
- package/dist/fields/relation/relation-picker.module.js +26 -0
- package/dist/fields/relation/relation-picker_module.css +124 -0
- package/dist/fields/relation/relation-summary.d.ts +31 -0
- package/dist/fields/relation/relation-summary.js +50 -0
- package/dist/fields/relation/relation-summary.module.js +11 -0
- package/dist/fields/relation/relation-summary_module.css +37 -0
- package/dist/fields/select/select-field.d.ts +16 -0
- package/dist/fields/select/select-field.js +50 -0
- package/dist/fields/select/select-field.module.js +5 -0
- package/dist/fields/select/select-field_module.css +4 -0
- package/dist/fields/sortable-item.d.ts +15 -0
- package/dist/fields/sortable-item.js +81 -0
- package/dist/fields/sortable-item.module.js +22 -0
- package/dist/fields/sortable-item_module.css +124 -0
- package/dist/fields/text/text-field.d.ts +20 -0
- package/dist/fields/text/text-field.js +104 -0
- package/dist/fields/text/text-field.module.js +6 -0
- package/dist/fields/text/text-field_module.css +5 -0
- package/dist/fields/text-area/text-area-field.d.ts +20 -0
- package/dist/fields/text-area/text-area-field.js +105 -0
- package/dist/fields/text-area/text-area-field.module.js +6 -0
- package/dist/fields/text-area/text-area-field_module.css +5 -0
- package/dist/fields/use-field-change-handler.d.ts +23 -0
- package/dist/fields/use-field-change-handler.js +52 -0
- package/dist/forms/document-actions.d.ts +48 -0
- package/dist/forms/document-actions.js +475 -0
- package/dist/forms/document-actions.module.js +34 -0
- package/dist/forms/document-actions_module.css +118 -0
- package/dist/forms/form-context.d.ts +89 -0
- package/dist/forms/form-context.js +466 -0
- package/dist/forms/form-renderer.d.ts +98 -0
- package/dist/forms/form-renderer.js +597 -0
- package/dist/forms/form-renderer.module.js +46 -0
- package/dist/forms/form-renderer_module.css +245 -0
- package/dist/forms/navigation-guard.d.ts +54 -0
- package/dist/forms/navigation-guard.js +22 -0
- package/dist/forms/path-widget.d.ts +36 -0
- package/dist/forms/path-widget.js +116 -0
- package/dist/forms/path-widget.module.js +8 -0
- package/dist/forms/path-widget_module.css +29 -0
- package/dist/forms/upload-executor.d.ts +57 -0
- package/dist/forms/upload-executor.js +94 -0
- package/dist/lib/translate-validation-error.d.ts +36 -0
- package/dist/lib/translate-validation-error.js +11 -0
- package/dist/modules/admin-account/commands.d.ts +2 -1
- package/dist/modules/admin-account/commands.js +13 -2
- package/dist/modules/admin-account/components/change-password.js +45 -36
- package/dist/modules/admin-account/components/container.js +185 -134
- package/dist/modules/admin-account/components/preferences.d.ts +8 -0
- package/dist/modules/admin-account/components/preferences.js +152 -0
- package/dist/modules/admin-account/components/preferences.module.js +11 -0
- package/dist/modules/admin-account/components/preferences_module.css +41 -0
- package/dist/modules/admin-account/components/update.js +50 -31
- package/dist/modules/admin-account/index.d.ts +3 -3
- package/dist/modules/admin-account/index.js +2 -2
- package/dist/modules/admin-account/schemas.d.ts +4 -0
- package/dist/modules/admin-account/schemas.js +4 -1
- package/dist/modules/admin-account/service.d.ts +1 -0
- package/dist/modules/admin-account/service.js +8 -0
- package/dist/modules/admin-permissions/components/inspector.js +31 -41
- package/dist/modules/admin-roles/components/create.js +43 -26
- package/dist/modules/admin-roles/components/permissions.js +26 -35
- package/dist/modules/admin-roles/components/update.js +26 -16
- package/dist/modules/admin-users/components/create.js +60 -40
- package/dist/modules/admin-users/components/roles.js +9 -15
- package/dist/modules/admin-users/components/set-password.js +30 -31
- package/dist/modules/admin-users/components/update.js +58 -39
- package/dist/modules/admin-users/dto.js +1 -0
- package/dist/modules/admin-users/repository.d.ts +17 -0
- package/dist/modules/admin-users/schemas.d.ts +4 -0
- package/dist/modules/admin-users/schemas.js +6 -2
- package/dist/modules/auth/components/sign-in-form.js +10 -8
- package/dist/presentation/group.d.ts +27 -0
- package/dist/presentation/group.js +14 -0
- package/dist/presentation/group.module.js +6 -0
- package/dist/presentation/group_module.css +19 -0
- package/dist/presentation/row.d.ts +25 -0
- package/dist/presentation/row.js +8 -0
- package/dist/presentation/row.module.js +5 -0
- package/dist/presentation/row_module.css +18 -0
- package/dist/presentation/tabs.d.ts +25 -0
- package/dist/presentation/tabs.js +39 -0
- package/dist/presentation/tabs.module.js +10 -0
- package/dist/presentation/tabs_module.css +68 -0
- package/dist/react.d.ts +66 -0
- package/dist/react.js +36 -0
- package/dist/services/admin-services-types.d.ts +16 -0
- package/dist/widgets/diff-viewer/diff-modal.d.ts +22 -0
- package/dist/widgets/diff-viewer/diff-modal.js +149 -0
- package/dist/widgets/diff-viewer/diff-modal.module.js +14 -0
- package/dist/widgets/diff-viewer/diff-modal_module.css +56 -0
- package/dist/widgets/status-badge/status-badge.d.ts +25 -0
- package/dist/widgets/status-badge/status-badge.js +37 -0
- package/dist/widgets/status-badge/status-badge.module.js +7 -0
- package/dist/widgets/status-badge/status-badge_module.css +20 -0
- package/package.json +14 -4
- package/src/fields/array/array-field.module.css +48 -0
- package/src/fields/array/array-field.tsx +267 -0
- package/src/fields/blocks/blocks-field.module.css +148 -0
- package/src/fields/blocks/blocks-field.tsx +323 -0
- package/src/fields/checkbox/checkbox-field.module.css +4 -0
- package/src/fields/checkbox/checkbox-field.tsx +54 -0
- package/src/fields/column-formatter.tsx +31 -0
- package/src/fields/date-time-formatter.tsx +22 -0
- package/src/fields/datetime/datetime-field.module.css +13 -0
- package/src/fields/datetime/datetime-field.tsx +54 -0
- package/src/fields/draggable-context-menu.module.css +127 -0
- package/src/fields/draggable-context-menu.tsx +87 -0
- package/src/fields/field-helpers.ts +69 -0
- package/src/fields/field-renderer.module.css +22 -0
- package/src/fields/field-renderer.tsx +288 -0
- package/src/fields/field-services-context.tsx +35 -0
- package/src/fields/field-services-types.ts +68 -0
- package/src/fields/file/file-field.module.css +153 -0
- package/src/fields/file/file-field.tsx +286 -0
- package/src/fields/file/file-upload-field.module.css +101 -0
- package/src/fields/file/file-upload-field.tsx +187 -0
- package/src/fields/group/group-field.module.css +43 -0
- package/src/fields/group/group-field.tsx +84 -0
- package/src/fields/image/image-field.module.css +155 -0
- package/src/fields/image/image-field.tsx +306 -0
- package/src/fields/image/image-upload-field.module.css +123 -0
- package/src/fields/image/image-upload-field.tsx +276 -0
- package/src/fields/local-date-time.tsx +88 -0
- package/src/fields/locale-badge.module.css +37 -0
- package/src/fields/locale-badge.tsx +32 -0
- package/src/fields/numerical/numerical-field.tsx +114 -0
- package/src/fields/relation/relation-display.module.css +36 -0
- package/src/fields/relation/relation-display.tsx +138 -0
- package/src/fields/relation/relation-field.module.css +83 -0
- package/src/fields/relation/relation-field.tsx +211 -0
- package/src/fields/relation/relation-picker.module.css +168 -0
- package/src/fields/relation/relation-picker.tsx +343 -0
- package/src/fields/relation/relation-summary.module.css +55 -0
- package/src/fields/relation/relation-summary.tsx +123 -0
- package/src/fields/select/select-field.module.css +13 -0
- package/src/fields/select/select-field.tsx +61 -0
- package/src/fields/sortable-item.module.css +167 -0
- package/src/fields/sortable-item.tsx +106 -0
- package/src/fields/text/text-field.module.css +13 -0
- package/src/fields/text/text-field.tsx +146 -0
- package/src/fields/text-area/text-area-field.module.css +13 -0
- package/src/fields/text-area/text-area-field.tsx +147 -0
- package/src/fields/use-field-change-handler.ts +112 -0
- package/src/forms/document-actions.module.css +160 -0
- package/src/forms/document-actions.tsx +482 -0
- package/src/forms/form-context.tsx +704 -0
- package/src/forms/form-renderer.module.css +321 -0
- package/src/forms/form-renderer.tsx +891 -0
- package/src/forms/navigation-guard.tsx +98 -0
- package/src/forms/path-widget.module.css +41 -0
- package/src/forms/path-widget.test.tsx +217 -0
- package/src/forms/path-widget.tsx +183 -0
- package/src/forms/upload-executor.ts +192 -0
- package/src/lib/translate-validation-error.ts +56 -0
- package/src/modules/admin-account/commands.ts +13 -0
- package/src/modules/admin-account/components/change-password.tsx +46 -31
- package/src/modules/admin-account/components/container.tsx +83 -38
- package/src/modules/admin-account/components/preferences.module.css +60 -0
- package/src/modules/admin-account/components/preferences.tsx +203 -0
- package/src/modules/admin-account/components/update.tsx +53 -27
- package/src/modules/admin-account/index.ts +3 -0
- package/src/modules/admin-account/schemas.ts +13 -0
- package/src/modules/admin-account/service.ts +12 -0
- package/src/modules/admin-permissions/components/inspector.tsx +22 -14
- package/src/modules/admin-roles/components/create.tsx +51 -23
- package/src/modules/admin-roles/components/permissions.tsx +25 -21
- package/src/modules/admin-roles/components/update.tsx +37 -19
- package/src/modules/admin-users/components/create.tsx +63 -34
- package/src/modules/admin-users/components/roles.tsx +9 -8
- package/src/modules/admin-users/components/set-password.tsx +34 -28
- package/src/modules/admin-users/components/update.tsx +58 -36
- package/src/modules/admin-users/dto.ts +1 -0
- package/src/modules/admin-users/repository.ts +17 -0
- package/src/modules/admin-users/schemas.ts +12 -0
- package/src/modules/auth/components/sign-in-form.tsx +14 -8
- package/src/presentation/group.module.css +41 -0
- package/src/presentation/group.tsx +40 -0
- package/src/presentation/row.module.css +32 -0
- package/src/presentation/row.tsx +33 -0
- package/src/presentation/tabs.module.css +107 -0
- package/src/presentation/tabs.tsx +84 -0
- package/src/react.ts +84 -0
- package/src/services/admin-services-types.ts +18 -0
- package/src/widgets/diff-viewer/diff-modal.module.css +79 -0
- package/src/widgets/diff-viewer/diff-modal.tsx +186 -0
- package/src/widgets/status-badge/status-badge.module.css +31 -0
- 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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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('
|
|
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:
|
|
93
|
-
errors: [
|
|
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('
|
|
122
|
+
setFormError(t('common.errors.accountNotFound'))
|
|
105
123
|
return
|
|
106
124
|
}
|
|
107
|
-
setFormError('
|
|
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=
|
|
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=
|
|
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=
|
|
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 ? '
|
|
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} /> : '
|
|
222
|
+
{isSubmitting === true ? <LoaderEllipsis size={42} /> : t('common.actions.save')}
|
|
208
223
|
</Button>
|
|
209
224
|
)}
|
|
210
225
|
</form.Subscribe>
|