@byline/admin 2.5.1 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +58 -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 +49 -0
- package/dist/fields/relation/relation-picker.js +236 -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 +130 -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 +326 -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
|
@@ -22,9 +22,10 @@
|
|
|
22
22
|
* `admin.users.versionConflict` and we prompt for reload.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
import { useState } from 'react'
|
|
25
|
+
import { useMemo, useState } from 'react'
|
|
26
26
|
import { revalidateLogic, useForm } from '@tanstack/react-form-start'
|
|
27
27
|
|
|
28
|
+
import { useTranslation } from '@byline/i18n/react'
|
|
28
29
|
import { Alert, Button, Input, LoaderEllipsis } from '@byline/ui/react'
|
|
29
30
|
import cx from 'classnames'
|
|
30
31
|
import { z } from 'zod'
|
|
@@ -33,17 +34,18 @@ import { useBylineAdminServices } from '../../../services/admin-services-context
|
|
|
33
34
|
import styles from './update.module.css'
|
|
34
35
|
import type { AccountResponse } from '../index.js'
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
.email({ message: 'Enter a valid email address' })
|
|
42
|
-
.min(3)
|
|
43
|
-
.max(254, 'Email must not exceed 254 characters'),
|
|
44
|
-
})
|
|
37
|
+
// Field-length ceilings sourced from the `adminUsers` schema in @byline/admin
|
|
38
|
+
// (100/100/100/254). Kept as constants so error messages can ICU-format them.
|
|
39
|
+
const MAX_NAME = 100
|
|
40
|
+
const MAX_USERNAME = 100
|
|
41
|
+
const MAX_EMAIL = 254
|
|
45
42
|
|
|
46
|
-
type UpdateAccountValues =
|
|
43
|
+
type UpdateAccountValues = {
|
|
44
|
+
given_name: string
|
|
45
|
+
family_name: string
|
|
46
|
+
username: string
|
|
47
|
+
email: string
|
|
48
|
+
}
|
|
47
49
|
|
|
48
50
|
function defaultsFrom(account: AccountResponse): UpdateAccountValues {
|
|
49
51
|
return {
|
|
@@ -80,9 +82,34 @@ interface UpdateAccountProps {
|
|
|
80
82
|
|
|
81
83
|
export function UpdateAccount({ account, onClose, onSuccess }: UpdateAccountProps) {
|
|
82
84
|
const { updateAccount } = useBylineAdminServices()
|
|
85
|
+
const { t } = useTranslation('byline-admin')
|
|
83
86
|
const [formError, setFormError] = useState<string | null>(null)
|
|
84
87
|
const [successMessage, setSuccessMessage] = useState<string | null>(null)
|
|
85
88
|
|
|
89
|
+
// Schema is rebuilt per-render so error messages reflect the active
|
|
90
|
+
// locale. Cheap — small schema, no ahead-of-time compilation cost
|
|
91
|
+
// that matters at this scale. `useMemo` keeps Tanstack Form's
|
|
92
|
+
// validator-identity stable across re-renders.
|
|
93
|
+
const updateAccountSchema = useMemo(
|
|
94
|
+
() =>
|
|
95
|
+
z.object({
|
|
96
|
+
given_name: z
|
|
97
|
+
.string()
|
|
98
|
+
.max(MAX_NAME, t('account.update.errors.givenNameTooLong', { max: MAX_NAME })),
|
|
99
|
+
family_name: z
|
|
100
|
+
.string()
|
|
101
|
+
.max(MAX_NAME, t('account.update.errors.familyNameTooLong', { max: MAX_NAME })),
|
|
102
|
+
username: z
|
|
103
|
+
.string()
|
|
104
|
+
.max(MAX_USERNAME, t('account.update.errors.usernameTooLong', { max: MAX_USERNAME })),
|
|
105
|
+
email: z
|
|
106
|
+
.email({ message: t('account.update.errors.invalidEmail') })
|
|
107
|
+
.min(3)
|
|
108
|
+
.max(MAX_EMAIL, t('account.update.errors.emailTooLong', { max: MAX_EMAIL })),
|
|
109
|
+
}),
|
|
110
|
+
[t]
|
|
111
|
+
)
|
|
112
|
+
|
|
86
113
|
const form = useForm({
|
|
87
114
|
defaultValues: defaultsFrom(account),
|
|
88
115
|
validationLogic: revalidateLogic({
|
|
@@ -97,34 +124,33 @@ export function UpdateAccount({ account, onClose, onSuccess }: UpdateAccountProp
|
|
|
97
124
|
setSuccessMessage(null)
|
|
98
125
|
const patch = buildPatch(value, account)
|
|
99
126
|
if (Object.keys(patch).length === 0) {
|
|
100
|
-
setSuccessMessage('
|
|
127
|
+
setSuccessMessage(t('common.feedback.noChanges'))
|
|
101
128
|
return
|
|
102
129
|
}
|
|
103
130
|
try {
|
|
104
131
|
const updated = await updateAccount({ data: { vid: account.vid, patch } })
|
|
105
|
-
setSuccessMessage('
|
|
132
|
+
setSuccessMessage(t('common.feedback.saved'))
|
|
106
133
|
onSuccess?.(updated)
|
|
107
134
|
} catch (err) {
|
|
108
135
|
const code = getErrorCode(err)
|
|
109
136
|
if (code === 'admin.users.emailInUse') {
|
|
137
|
+
const message = t('account.update.errors.emailInUse')
|
|
110
138
|
form.setFieldMeta('email', (meta) => ({
|
|
111
139
|
...meta,
|
|
112
|
-
errorMap: { ...meta.errorMap, onServer:
|
|
113
|
-
errors: [
|
|
140
|
+
errorMap: { ...meta.errorMap, onServer: message },
|
|
141
|
+
errors: [message],
|
|
114
142
|
}))
|
|
115
143
|
return
|
|
116
144
|
}
|
|
117
145
|
if (code === 'admin.users.versionConflict') {
|
|
118
|
-
setFormError(
|
|
119
|
-
'Your account has been modified elsewhere since you opened this form. Reload to refresh and try again.'
|
|
120
|
-
)
|
|
146
|
+
setFormError(t('common.errors.versionConflict'))
|
|
121
147
|
return
|
|
122
148
|
}
|
|
123
149
|
if (code === 'admin.account.notFound') {
|
|
124
|
-
setFormError('
|
|
150
|
+
setFormError(t('common.errors.accountNotFound'))
|
|
125
151
|
return
|
|
126
152
|
}
|
|
127
|
-
setFormError('
|
|
153
|
+
setFormError(t('common.errors.couldNotSave'))
|
|
128
154
|
}
|
|
129
155
|
},
|
|
130
156
|
})
|
|
@@ -146,7 +172,7 @@ export function UpdateAccount({ account, onClose, onSuccess }: UpdateAccountProp
|
|
|
146
172
|
<form.Field name="given_name">
|
|
147
173
|
{(field) => (
|
|
148
174
|
<Input
|
|
149
|
-
label=
|
|
175
|
+
label={t('account.update.fields.givenName')}
|
|
150
176
|
id="given_name"
|
|
151
177
|
name={field.name}
|
|
152
178
|
value={field.state.value}
|
|
@@ -162,7 +188,7 @@ export function UpdateAccount({ account, onClose, onSuccess }: UpdateAccountProp
|
|
|
162
188
|
<form.Field name="family_name">
|
|
163
189
|
{(field) => (
|
|
164
190
|
<Input
|
|
165
|
-
label=
|
|
191
|
+
label={t('account.update.fields.familyName')}
|
|
166
192
|
id="family_name"
|
|
167
193
|
name={field.name}
|
|
168
194
|
value={field.state.value}
|
|
@@ -178,7 +204,7 @@ export function UpdateAccount({ account, onClose, onSuccess }: UpdateAccountProp
|
|
|
178
204
|
<form.Field name="username">
|
|
179
205
|
{(field) => (
|
|
180
206
|
<Input
|
|
181
|
-
label=
|
|
207
|
+
label={t('account.update.fields.username')}
|
|
182
208
|
id="username"
|
|
183
209
|
name={field.name}
|
|
184
210
|
value={field.state.value}
|
|
@@ -186,7 +212,7 @@ export function UpdateAccount({ account, onClose, onSuccess }: UpdateAccountProp
|
|
|
186
212
|
onChange={(e) => field.handleChange(e.currentTarget.value)}
|
|
187
213
|
error={field.state.meta.errors.length > 0}
|
|
188
214
|
errorText={firstError(field.state.meta.errors)}
|
|
189
|
-
helpText=
|
|
215
|
+
helpText={t('account.update.fields.usernameHelp')}
|
|
190
216
|
autoComplete="username"
|
|
191
217
|
/>
|
|
192
218
|
)}
|
|
@@ -195,7 +221,7 @@ export function UpdateAccount({ account, onClose, onSuccess }: UpdateAccountProp
|
|
|
195
221
|
<form.Field name="email">
|
|
196
222
|
{(field) => (
|
|
197
223
|
<Input
|
|
198
|
-
label=
|
|
224
|
+
label={t('common.fields.email')}
|
|
199
225
|
id="email"
|
|
200
226
|
name={field.name}
|
|
201
227
|
type="email"
|
|
@@ -218,7 +244,7 @@ export function UpdateAccount({ account, onClose, onSuccess }: UpdateAccountProp
|
|
|
218
244
|
onClick={onClose}
|
|
219
245
|
className={cx('byline-account-update-action', styles.action)}
|
|
220
246
|
>
|
|
221
|
-
{successMessage ? '
|
|
247
|
+
{successMessage ? t('common.actions.close') : t('common.actions.cancel')}
|
|
222
248
|
</Button>
|
|
223
249
|
<form.Subscribe
|
|
224
250
|
selector={(state) => ({
|
|
@@ -235,7 +261,7 @@ export function UpdateAccount({ account, onClose, onSuccess }: UpdateAccountProp
|
|
|
235
261
|
disabled={!canSubmit || isSubmitting}
|
|
236
262
|
className={cx('byline-account-update-action', styles.action)}
|
|
237
263
|
>
|
|
238
|
-
{isSubmitting === true ? <LoaderEllipsis size={42} /> : '
|
|
264
|
+
{isSubmitting === true ? <LoaderEllipsis size={42} /> : t('common.actions.save')}
|
|
239
265
|
</Button>
|
|
240
266
|
)}
|
|
241
267
|
</form.Subscribe>
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
export {
|
|
34
34
|
changeAccountPasswordCommand,
|
|
35
35
|
getAccountCommand,
|
|
36
|
+
setPreferredLocaleCommand,
|
|
36
37
|
updateAccountCommand,
|
|
37
38
|
} from './commands.js'
|
|
38
39
|
export {
|
|
@@ -47,6 +48,7 @@ export {
|
|
|
47
48
|
changeAccountPasswordRequestSchema,
|
|
48
49
|
getAccountRequestSchema,
|
|
49
50
|
okResponseSchema,
|
|
51
|
+
setPreferredLocaleRequestSchema,
|
|
50
52
|
updateAccountRequestSchema,
|
|
51
53
|
} from './schemas.js'
|
|
52
54
|
export { AdminAccountService } from './service.js'
|
|
@@ -56,5 +58,6 @@ export type {
|
|
|
56
58
|
ChangeAccountPasswordRequest,
|
|
57
59
|
GetAccountRequest,
|
|
58
60
|
OkResponse,
|
|
61
|
+
SetPreferredLocaleRequest,
|
|
59
62
|
UpdateAccountRequest,
|
|
60
63
|
} from './schemas.js'
|
|
@@ -76,6 +76,19 @@ export const changeAccountPasswordRequestSchema = z.object({
|
|
|
76
76
|
})
|
|
77
77
|
export type ChangeAccountPasswordRequest = z.infer<typeof changeAccountPasswordRequestSchema>
|
|
78
78
|
|
|
79
|
+
// Vid-less — admin interface locale is a personal preference, not content
|
|
80
|
+
// state, so the menu can persist without a fresh-vid round trip. `null`
|
|
81
|
+
// clears the column and lets detection take over.
|
|
82
|
+
export const setPreferredLocaleRequestSchema = z.object({
|
|
83
|
+
locale: z
|
|
84
|
+
.string()
|
|
85
|
+
.min(2)
|
|
86
|
+
.max(16)
|
|
87
|
+
.regex(/^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/, 'must be a BCP 47 locale tag')
|
|
88
|
+
.nullable(),
|
|
89
|
+
})
|
|
90
|
+
export type SetPreferredLocaleRequest = z.infer<typeof setPreferredLocaleRequestSchema>
|
|
91
|
+
|
|
79
92
|
// ---------------------------------------------------------------------------
|
|
80
93
|
// Responses (re-exports — same shape as the admin-users module)
|
|
81
94
|
// ---------------------------------------------------------------------------
|
|
@@ -71,6 +71,18 @@ export class AdminAccountService {
|
|
|
71
71
|
return toAdminUser(row)
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
async setPreferredLocale(actorId: string, locale: string | null): Promise<AccountResponse> {
|
|
75
|
+
const current = await this.#repo.getById(actorId)
|
|
76
|
+
if (!current) throw ERR_ADMIN_ACCOUNT_NOT_FOUND()
|
|
77
|
+
await this.#repo.setPreferredLocale(actorId, locale)
|
|
78
|
+
// Re-read for the post-write vid + updated_at — the vid-less repo
|
|
79
|
+
// method bumps `vid`, so the response carries the fresh shape callers
|
|
80
|
+
// need for any subsequent vid-gated edit.
|
|
81
|
+
const updated = await this.#repo.getById(actorId)
|
|
82
|
+
if (!updated) throw ERR_ADMIN_ACCOUNT_NOT_FOUND()
|
|
83
|
+
return toAdminUser(updated)
|
|
84
|
+
}
|
|
85
|
+
|
|
74
86
|
async changePassword(
|
|
75
87
|
actorId: string,
|
|
76
88
|
request: ChangeAccountPasswordRequest
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
import { useState } from 'react'
|
|
30
30
|
|
|
31
|
+
import { useTranslation } from '@byline/i18n/react'
|
|
31
32
|
import { Button, Container, LoaderRing, Section } from '@byline/ui/react'
|
|
32
33
|
import cx from 'classnames'
|
|
33
34
|
|
|
@@ -82,15 +83,16 @@ function displayUser(user: WhoHasAbilityResponse['users'][number]): string {
|
|
|
82
83
|
// --- expandable matrix row ------------------------------------------------
|
|
83
84
|
|
|
84
85
|
function MatrixPanel({ matrix }: { matrix: WhoHasAbilityResponse }) {
|
|
86
|
+
const { t } = useTranslation('byline-admin')
|
|
85
87
|
return (
|
|
86
88
|
<div className={cx('byline-inspector-matrix', styles.matrix)}>
|
|
87
89
|
<div>
|
|
88
90
|
<h4 className={cx('byline-inspector-matrix-title', styles['matrix-title'])}>
|
|
89
|
-
|
|
91
|
+
{t('adminPermissions.matrix.rolesTitle', { count: matrix.roles.length })}
|
|
90
92
|
</h4>
|
|
91
93
|
{matrix.roles.length === 0 ? (
|
|
92
94
|
<p className={cx('muted', 'byline-inspector-matrix-empty', styles['matrix-empty'])}>
|
|
93
|
-
|
|
95
|
+
{t('adminPermissions.matrix.rolesEmpty')}
|
|
94
96
|
</p>
|
|
95
97
|
) : (
|
|
96
98
|
<ul className={cx('byline-inspector-matrix-list', styles['matrix-list'])}>
|
|
@@ -110,11 +112,11 @@ function MatrixPanel({ matrix }: { matrix: WhoHasAbilityResponse }) {
|
|
|
110
112
|
</div>
|
|
111
113
|
<div>
|
|
112
114
|
<h4 className={cx('byline-inspector-matrix-title', styles['matrix-title'])}>
|
|
113
|
-
|
|
115
|
+
{t('adminPermissions.matrix.usersTitle', { count: matrix.users.length })}
|
|
114
116
|
</h4>
|
|
115
117
|
{matrix.users.length === 0 ? (
|
|
116
118
|
<p className={cx('muted', 'byline-inspector-matrix-empty', styles['matrix-empty'])}>
|
|
117
|
-
|
|
119
|
+
{t('adminPermissions.matrix.usersEmpty')}
|
|
118
120
|
</p>
|
|
119
121
|
) : (
|
|
120
122
|
<ul className={cx('byline-inspector-matrix-list', styles['matrix-list'])}>
|
|
@@ -142,7 +144,9 @@ interface AbilityRowProps {
|
|
|
142
144
|
}
|
|
143
145
|
|
|
144
146
|
function AbilityRow({ ability, matrix, loading, onToggle, expanded }: AbilityRowProps) {
|
|
147
|
+
const { t } = useTranslation('byline-admin')
|
|
145
148
|
const sv = sourceVariant(ability.source)
|
|
149
|
+
const sourceKey = ability.source ?? 'unknown'
|
|
146
150
|
return (
|
|
147
151
|
<div className={cx('byline-inspector-row', styles.row)}>
|
|
148
152
|
<div className={cx('byline-inspector-row-head', styles['row-head'])}>
|
|
@@ -157,7 +161,7 @@ function AbilityRow({ ability, matrix, loading, onToggle, expanded }: AbilityRow
|
|
|
157
161
|
sv.local
|
|
158
162
|
)}
|
|
159
163
|
>
|
|
160
|
-
{
|
|
164
|
+
{t(`adminPermissions.source.${sourceKey}`)}
|
|
161
165
|
</span>
|
|
162
166
|
</div>
|
|
163
167
|
<p className={cx('byline-inspector-row-label', styles['row-label'])}>{ability.label}</p>
|
|
@@ -170,14 +174,16 @@ function AbilityRow({ ability, matrix, loading, onToggle, expanded }: AbilityRow
|
|
|
170
174
|
) : null}
|
|
171
175
|
</div>
|
|
172
176
|
<Button size="xs" intent="secondary" onClick={onToggle}>
|
|
173
|
-
{expanded
|
|
177
|
+
{expanded
|
|
178
|
+
? t('adminPermissions.row.hideButton')
|
|
179
|
+
: t('adminPermissions.row.holdersButton')}
|
|
174
180
|
</Button>
|
|
175
181
|
</div>
|
|
176
182
|
{expanded ? (
|
|
177
183
|
loading ? (
|
|
178
184
|
<div className={cx('byline-inspector-loader', styles.loader)}>
|
|
179
185
|
<LoaderRing size={20} color="#888" />
|
|
180
|
-
<span className="muted">
|
|
186
|
+
<span className="muted">{t('common.loading')}</span>
|
|
181
187
|
</div>
|
|
182
188
|
) : matrix ? (
|
|
183
189
|
<MatrixPanel matrix={matrix} />
|
|
@@ -198,6 +204,7 @@ interface GroupSectionProps {
|
|
|
198
204
|
}
|
|
199
205
|
|
|
200
206
|
function GroupSection({ group, matrices, loading, expanded, onToggle }: GroupSectionProps) {
|
|
207
|
+
const { t } = useTranslation('byline-admin')
|
|
201
208
|
return (
|
|
202
209
|
<details open className={cx('byline-inspector-group', styles.group)}>
|
|
203
210
|
<summary className={cx('byline-inspector-group-summary', styles['group-summary'])}>
|
|
@@ -205,7 +212,7 @@ function GroupSection({ group, matrices, loading, expanded, onToggle }: GroupSec
|
|
|
205
212
|
{group.group}
|
|
206
213
|
</span>
|
|
207
214
|
<span className={cx('muted', 'byline-inspector-group-count', styles['group-count'])}>
|
|
208
|
-
{group.abilities.length}
|
|
215
|
+
{t('adminPermissions.group.abilitiesCount', { count: group.abilities.length })}
|
|
209
216
|
</span>
|
|
210
217
|
</summary>
|
|
211
218
|
<div className={cx('byline-inspector-group-body', styles['group-body'])}>
|
|
@@ -228,6 +235,7 @@ function GroupSection({ group, matrices, loading, expanded, onToggle }: GroupSec
|
|
|
228
235
|
|
|
229
236
|
export function AbilitiesInspector({ data }: { data: ListRegisteredAbilitiesResponse }) {
|
|
230
237
|
const { whoHasAbility } = useBylineAdminServices()
|
|
238
|
+
const { t } = useTranslation('byline-admin')
|
|
231
239
|
const [expanded, setExpanded] = useState<Set<string>>(new Set())
|
|
232
240
|
const [loading, setLoading] = useState<Set<string>>(new Set())
|
|
233
241
|
const [matrices, setMatrices] = useState<Record<string, WhoHasAbilityResponse>>({})
|
|
@@ -264,19 +272,19 @@ export function AbilitiesInspector({ data }: { data: ListRegisteredAbilitiesResp
|
|
|
264
272
|
<Section>
|
|
265
273
|
<Container>
|
|
266
274
|
<div className={cx('byline-inspector-head', styles.head)}>
|
|
267
|
-
<h1 className={cx('byline-inspector-title', styles.title)}>
|
|
275
|
+
<h1 className={cx('byline-inspector-title', styles.title)}>
|
|
276
|
+
{t('adminPermissions.title')}
|
|
277
|
+
</h1>
|
|
268
278
|
<span className={cx('byline-inspector-count-pill', styles['count-pill'])}>
|
|
269
|
-
{data.total}
|
|
279
|
+
{t('adminPermissions.countPill', { count: data.total })}
|
|
270
280
|
</span>
|
|
271
281
|
</div>
|
|
272
282
|
<p className={cx('muted', 'byline-inspector-lead', styles.lead)}>
|
|
273
|
-
|
|
274
|
-
Collections auto-register CRUD + workflow abilities; admin subsystems contribute their own
|
|
275
|
-
keys at composition root via <code>registerAdminAbilities</code>.
|
|
283
|
+
{t('adminPermissions.lead')}
|
|
276
284
|
</p>
|
|
277
285
|
{data.groups.length === 0 ? (
|
|
278
286
|
<p className={cx('muted', 'byline-inspector-empty', styles.empty)}>
|
|
279
|
-
|
|
287
|
+
{t('adminPermissions.empty')}
|
|
280
288
|
</p>
|
|
281
289
|
) : (
|
|
282
290
|
<div className={cx('byline-inspector-groups', styles.groups)}>
|
|
@@ -17,9 +17,10 @@
|
|
|
17
17
|
* (see the repository contract).
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { useState } from 'react'
|
|
20
|
+
import { useMemo, useState } from 'react'
|
|
21
21
|
import { revalidateLogic, useForm } from '@tanstack/react-form-start'
|
|
22
22
|
|
|
23
|
+
import { useTranslation } from '@byline/i18n/react'
|
|
23
24
|
import { Alert, Button, Input, LoaderEllipsis, TextArea } from '@byline/ui/react'
|
|
24
25
|
import cx from 'classnames'
|
|
25
26
|
import { z } from 'zod'
|
|
@@ -28,19 +29,15 @@ import { useBylineAdminServices } from '../../../services/admin-services-context
|
|
|
28
29
|
import styles from './create.module.css'
|
|
29
30
|
import type { AdminRoleResponse } from '../index.js'
|
|
30
31
|
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
.string()
|
|
35
|
-
.min(1, 'Machine name is required')
|
|
36
|
-
.max(128, 'Machine name must not exceed 128 characters')
|
|
37
|
-
.regex(/^[a-z0-9][a-z0-9_-]*$/, {
|
|
38
|
-
message: 'Lowercase letters, numbers, hyphens, and underscores only',
|
|
39
|
-
}),
|
|
40
|
-
description: z.string().max(2000, 'Description must not exceed 2000 characters'),
|
|
41
|
-
})
|
|
32
|
+
const MAX_NAME = 128
|
|
33
|
+
const MAX_MACHINE_NAME = 128
|
|
34
|
+
const MAX_DESCRIPTION = 2000
|
|
42
35
|
|
|
43
|
-
type CreateAdminRoleValues =
|
|
36
|
+
type CreateAdminRoleValues = {
|
|
37
|
+
name: string
|
|
38
|
+
machine_name: string
|
|
39
|
+
description: string
|
|
40
|
+
}
|
|
44
41
|
|
|
45
42
|
const initialValues: CreateAdminRoleValues = {
|
|
46
43
|
name: '',
|
|
@@ -59,8 +56,38 @@ interface CreateAdminRoleProps {
|
|
|
59
56
|
|
|
60
57
|
export function CreateAdminRole({ onClose, onSuccess }: CreateAdminRoleProps) {
|
|
61
58
|
const { createAdminRole } = useBylineAdminServices()
|
|
59
|
+
const { t } = useTranslation('byline-admin')
|
|
62
60
|
const [formError, setFormError] = useState<string | null>(null)
|
|
63
61
|
|
|
62
|
+
// Schema rebuilt per-render so error messages reflect the active
|
|
63
|
+
// locale; wrapped in useMemo([t]) to keep validator identity stable.
|
|
64
|
+
const createAdminRoleFormSchema = useMemo(
|
|
65
|
+
() =>
|
|
66
|
+
z.object({
|
|
67
|
+
name: z
|
|
68
|
+
.string()
|
|
69
|
+
.min(1, t('adminRoles.create.errors.nameRequired'))
|
|
70
|
+
.max(MAX_NAME, t('adminRoles.create.errors.nameTooLong', { max: MAX_NAME })),
|
|
71
|
+
machine_name: z
|
|
72
|
+
.string()
|
|
73
|
+
.min(1, t('adminRoles.create.errors.machineNameRequired'))
|
|
74
|
+
.max(
|
|
75
|
+
MAX_MACHINE_NAME,
|
|
76
|
+
t('adminRoles.create.errors.machineNameTooLong', { max: MAX_MACHINE_NAME })
|
|
77
|
+
)
|
|
78
|
+
.regex(/^[a-z0-9][a-z0-9_-]*$/, {
|
|
79
|
+
message: t('adminRoles.create.errors.machineNameInvalid'),
|
|
80
|
+
}),
|
|
81
|
+
description: z
|
|
82
|
+
.string()
|
|
83
|
+
.max(
|
|
84
|
+
MAX_DESCRIPTION,
|
|
85
|
+
t('adminRoles.create.errors.descriptionTooLong', { max: MAX_DESCRIPTION })
|
|
86
|
+
),
|
|
87
|
+
}),
|
|
88
|
+
[t]
|
|
89
|
+
)
|
|
90
|
+
|
|
64
91
|
const form = useForm({
|
|
65
92
|
defaultValues: initialValues,
|
|
66
93
|
validationLogic: revalidateLogic({
|
|
@@ -85,14 +112,15 @@ export function CreateAdminRole({ onClose, onSuccess }: CreateAdminRoleProps) {
|
|
|
85
112
|
} catch (err) {
|
|
86
113
|
const code = getErrorCode(err)
|
|
87
114
|
if (code === 'admin.roles.machineNameInUse') {
|
|
115
|
+
const message = t('adminRoles.create.errors.machineNameInUse')
|
|
88
116
|
form.setFieldMeta('machine_name', (meta) => ({
|
|
89
117
|
...meta,
|
|
90
|
-
errorMap: { ...meta.errorMap, onServer:
|
|
91
|
-
errors: [
|
|
118
|
+
errorMap: { ...meta.errorMap, onServer: message },
|
|
119
|
+
errors: [message],
|
|
92
120
|
}))
|
|
93
121
|
return
|
|
94
122
|
}
|
|
95
|
-
setFormError('
|
|
123
|
+
setFormError(t('adminRoles.create.errors.fallback'))
|
|
96
124
|
}
|
|
97
125
|
},
|
|
98
126
|
})
|
|
@@ -113,7 +141,7 @@ export function CreateAdminRole({ onClose, onSuccess }: CreateAdminRoleProps) {
|
|
|
113
141
|
<form.Field name="name">
|
|
114
142
|
{(field) => (
|
|
115
143
|
<Input
|
|
116
|
-
label=
|
|
144
|
+
label={t('adminRoles.fields.name')}
|
|
117
145
|
id="new-role-name"
|
|
118
146
|
name={field.name}
|
|
119
147
|
value={field.state.value}
|
|
@@ -121,7 +149,7 @@ export function CreateAdminRole({ onClose, onSuccess }: CreateAdminRoleProps) {
|
|
|
121
149
|
onChange={(e) => field.handleChange(e.currentTarget.value)}
|
|
122
150
|
error={field.state.meta.errors.length > 0}
|
|
123
151
|
errorText={firstError(field.state.meta.errors)}
|
|
124
|
-
helpText=
|
|
152
|
+
helpText={t('adminRoles.create.fields.nameHelp')}
|
|
125
153
|
required
|
|
126
154
|
/>
|
|
127
155
|
)}
|
|
@@ -130,7 +158,7 @@ export function CreateAdminRole({ onClose, onSuccess }: CreateAdminRoleProps) {
|
|
|
130
158
|
<form.Field name="machine_name">
|
|
131
159
|
{(field) => (
|
|
132
160
|
<Input
|
|
133
|
-
label=
|
|
161
|
+
label={t('adminRoles.fields.machineName')}
|
|
134
162
|
id="new-role-machine-name"
|
|
135
163
|
name={field.name}
|
|
136
164
|
value={field.state.value}
|
|
@@ -138,7 +166,7 @@ export function CreateAdminRole({ onClose, onSuccess }: CreateAdminRoleProps) {
|
|
|
138
166
|
onChange={(e) => field.handleChange(e.currentTarget.value)}
|
|
139
167
|
error={field.state.meta.errors.length > 0}
|
|
140
168
|
errorText={firstError(field.state.meta.errors)}
|
|
141
|
-
helpText=
|
|
169
|
+
helpText={t('adminRoles.create.fields.machineNameHelp')}
|
|
142
170
|
required
|
|
143
171
|
/>
|
|
144
172
|
)}
|
|
@@ -147,7 +175,7 @@ export function CreateAdminRole({ onClose, onSuccess }: CreateAdminRoleProps) {
|
|
|
147
175
|
<form.Field name="description">
|
|
148
176
|
{(field) => (
|
|
149
177
|
<TextArea
|
|
150
|
-
label=
|
|
178
|
+
label={t('adminRoles.fields.description')}
|
|
151
179
|
id="new-role-description"
|
|
152
180
|
name={field.name}
|
|
153
181
|
value={field.state.value}
|
|
@@ -168,7 +196,7 @@ export function CreateAdminRole({ onClose, onSuccess }: CreateAdminRoleProps) {
|
|
|
168
196
|
onClick={onClose}
|
|
169
197
|
className={cx('byline-role-create-action', styles.action)}
|
|
170
198
|
>
|
|
171
|
-
|
|
199
|
+
{t('common.actions.cancel')}
|
|
172
200
|
</Button>
|
|
173
201
|
<form.Subscribe
|
|
174
202
|
selector={(state) => ({
|
|
@@ -184,7 +212,7 @@ export function CreateAdminRole({ onClose, onSuccess }: CreateAdminRoleProps) {
|
|
|
184
212
|
disabled={!canSubmit || isSubmitting}
|
|
185
213
|
className={cx('byline-role-create-action', styles.action)}
|
|
186
214
|
>
|
|
187
|
-
{isSubmitting === true ? <LoaderEllipsis size={42} /> : '
|
|
215
|
+
{isSubmitting === true ? <LoaderEllipsis size={42} /> : t('common.actions.save')}
|
|
188
216
|
</Button>
|
|
189
217
|
)}
|
|
190
218
|
</form.Subscribe>
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
|
|
32
32
|
import { useMemo, useState } from 'react'
|
|
33
33
|
|
|
34
|
+
import { useTranslation } from '@byline/i18n/react'
|
|
34
35
|
import { Alert, Button, Checkbox, LoaderEllipsis } from '@byline/ui/react'
|
|
35
36
|
import cx from 'classnames'
|
|
36
37
|
|
|
@@ -78,6 +79,7 @@ function GroupSection({
|
|
|
78
79
|
onSelectAll,
|
|
79
80
|
onClearAll,
|
|
80
81
|
}: GroupSectionProps) {
|
|
82
|
+
const { t } = useTranslation('byline-admin')
|
|
81
83
|
const groupKeys = useMemo(() => group.abilities.map((a) => a.key), [group.abilities])
|
|
82
84
|
const selectedInGroup = groupKeys.filter((key) => selected.has(key)).length
|
|
83
85
|
const isEdit = mode === 'edit'
|
|
@@ -92,7 +94,11 @@ function GroupSection({
|
|
|
92
94
|
<span
|
|
93
95
|
className={cx('muted', 'byline-role-permissions-group-count', styles['group-count'])}
|
|
94
96
|
>
|
|
95
|
-
{
|
|
97
|
+
{t('adminRoles.permissions.groupCount', {
|
|
98
|
+
selected: selectedInGroup,
|
|
99
|
+
total: group.abilities.length,
|
|
100
|
+
mode,
|
|
101
|
+
})}
|
|
96
102
|
</span>
|
|
97
103
|
</div>
|
|
98
104
|
{isEdit ? (
|
|
@@ -104,7 +110,7 @@ function GroupSection({
|
|
|
104
110
|
disabled={saving || selectedInGroup === group.abilities.length}
|
|
105
111
|
onClick={() => onSelectAll(groupKeys)}
|
|
106
112
|
>
|
|
107
|
-
|
|
113
|
+
{t('adminRoles.permissions.selectAll')}
|
|
108
114
|
</Button>
|
|
109
115
|
<Button
|
|
110
116
|
size="xs"
|
|
@@ -113,7 +119,7 @@ function GroupSection({
|
|
|
113
119
|
disabled={saving || selectedInGroup === 0}
|
|
114
120
|
onClick={() => onClearAll(groupKeys)}
|
|
115
121
|
>
|
|
116
|
-
|
|
122
|
+
{t('adminRoles.permissions.clear')}
|
|
117
123
|
</Button>
|
|
118
124
|
</div>
|
|
119
125
|
) : null}
|
|
@@ -175,6 +181,7 @@ export function RolePermissions({
|
|
|
175
181
|
onSaved,
|
|
176
182
|
}: RolePermissionsProps) {
|
|
177
183
|
const { setRoleAbilities } = useBylineAdminServices()
|
|
184
|
+
const { t } = useTranslation('byline-admin')
|
|
178
185
|
const [mode, setMode] = useState<Mode>('view')
|
|
179
186
|
const [initialSet, setInitialSet] = useState<ReadonlySet<string>>(() => new Set(initialAbilities))
|
|
180
187
|
const [selected, setSelected] = useState<Set<string>>(() => new Set(initialAbilities))
|
|
@@ -246,13 +253,11 @@ export function RolePermissions({
|
|
|
246
253
|
} catch (err) {
|
|
247
254
|
const code = getErrorCode(err)
|
|
248
255
|
if (code === 'admin.permissions.roleNotFound') {
|
|
249
|
-
setError('
|
|
256
|
+
setError(t('adminRoles.permissions.errors.roleNotFound'))
|
|
250
257
|
} else if (code === 'admin.permissions.abilityUnregistered') {
|
|
251
|
-
setError(
|
|
252
|
-
'One or more selected abilities are no longer registered. Reload the page and try again.'
|
|
253
|
-
)
|
|
258
|
+
setError(t('adminRoles.permissions.errors.abilityUnregistered'))
|
|
254
259
|
} else {
|
|
255
|
-
setError('
|
|
260
|
+
setError(t('adminRoles.permissions.errors.fallback'))
|
|
256
261
|
}
|
|
257
262
|
} finally {
|
|
258
263
|
setSaving(false)
|
|
@@ -272,14 +277,12 @@ export function RolePermissions({
|
|
|
272
277
|
onEdit={handleEnterEdit}
|
|
273
278
|
/>
|
|
274
279
|
<p className={cx('muted', 'byline-role-permissions-counter', styles.counter)}>
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
</span>{' '}
|
|
282
|
-
{isEdit ? 'selected' : 'granted'} for {role.name}
|
|
280
|
+
{t('adminRoles.permissions.counter', {
|
|
281
|
+
selected: totalSelected,
|
|
282
|
+
total: registered.total,
|
|
283
|
+
mode,
|
|
284
|
+
role: role.name,
|
|
285
|
+
})}
|
|
283
286
|
</p>
|
|
284
287
|
{isEdit && isDirty ? (
|
|
285
288
|
<div className={cx('byline-role-permissions-actions', styles.actions)}>
|
|
@@ -291,7 +294,7 @@ export function RolePermissions({
|
|
|
291
294
|
disabled={saving}
|
|
292
295
|
className={cx('byline-role-permissions-action', styles.action)}
|
|
293
296
|
>
|
|
294
|
-
|
|
297
|
+
{t('common.actions.cancel')}
|
|
295
298
|
</Button>
|
|
296
299
|
<Button
|
|
297
300
|
type="button"
|
|
@@ -301,7 +304,7 @@ export function RolePermissions({
|
|
|
301
304
|
disabled={saving}
|
|
302
305
|
className={cx('byline-role-permissions-action', styles.action)}
|
|
303
306
|
>
|
|
304
|
-
{saving ? <LoaderEllipsis size={30} /> : '
|
|
307
|
+
{saving ? <LoaderEllipsis size={30} /> : t('common.actions.save')}
|
|
305
308
|
</Button>
|
|
306
309
|
</div>
|
|
307
310
|
) : null}
|
|
@@ -336,6 +339,7 @@ interface ModeToggleProps {
|
|
|
336
339
|
}
|
|
337
340
|
|
|
338
341
|
function ModeToggle({ mode, dirty, saving, onView, onEdit }: ModeToggleProps) {
|
|
342
|
+
const { t } = useTranslation('byline-admin')
|
|
339
343
|
// Segmented two-state toggle. View is disabled while dirty so the
|
|
340
344
|
// user has to commit to Save or Cancel — avoids accidentally
|
|
341
345
|
// discarding a draft selection.
|
|
@@ -346,7 +350,7 @@ function ModeToggle({ mode, dirty, saving, onView, onEdit }: ModeToggleProps) {
|
|
|
346
350
|
return (
|
|
347
351
|
<div
|
|
348
352
|
role="group"
|
|
349
|
-
aria-label=
|
|
353
|
+
aria-label={t('adminRoles.permissions.modeAriaLabel')}
|
|
350
354
|
className={cx('byline-role-permissions-mode-toggle', styles['mode-toggle'])}
|
|
351
355
|
>
|
|
352
356
|
<button
|
|
@@ -364,7 +368,7 @@ function ModeToggle({ mode, dirty, saving, onView, onEdit }: ModeToggleProps) {
|
|
|
364
368
|
]
|
|
365
369
|
)}
|
|
366
370
|
>
|
|
367
|
-
|
|
371
|
+
{t('adminRoles.permissions.viewMode')}
|
|
368
372
|
</button>
|
|
369
373
|
<button
|
|
370
374
|
type="button"
|
|
@@ -383,7 +387,7 @@ function ModeToggle({ mode, dirty, saving, onView, onEdit }: ModeToggleProps) {
|
|
|
383
387
|
]
|
|
384
388
|
)}
|
|
385
389
|
>
|
|
386
|
-
|
|
390
|
+
{t('adminRoles.permissions.editMode')}
|
|
387
391
|
</button>
|
|
388
392
|
</div>
|
|
389
393
|
)
|