@byline/ui 2.4.0 → 2.4.2
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/react.d.ts +10 -18
- package/dist/react.js +2 -15
- package/dist/{admin/components/collections → widgets/diff-viewer}/diff-modal.d.ts +8 -1
- package/dist/{admin/components/collections → widgets/diff-viewer}/diff-modal.js +4 -6
- package/dist/widgets/diff-viewer/diff-modal.module.js +14 -0
- package/dist/{admin/components/collections → widgets/diff-viewer}/diff-modal_module.css +9 -9
- package/dist/{admin/components/collections → widgets/status-badge}/status-badge.js +1 -1
- package/dist/{admin/components/collections → widgets/status-badge}/status-badge.module.js +3 -3
- package/dist/{admin/components/collections → widgets/status-badge}/status-badge_module.css +3 -3
- package/package.json +2 -4
- package/src/react.ts +12 -34
- package/src/{admin/components/collections → widgets/diff-viewer}/diff-modal.tsx +16 -5
- package/src/{admin/components/collections → widgets/status-badge}/status-badge.tsx +1 -1
- package/dist/admin/components/admin-account/change-password.d.ts +0 -8
- package/dist/admin/components/admin-account/change-password.js +0 -192
- package/dist/admin/components/admin-account/change-password.module.js +0 -8
- package/dist/admin/components/admin-account/change-password_module.css +0 -27
- package/dist/admin/components/admin-account/container.d.ts +0 -29
- package/dist/admin/components/admin-account/container.js +0 -299
- package/dist/admin/components/admin-account/container.module.js +0 -28
- package/dist/admin/components/admin-account/container_module.css +0 -106
- package/dist/admin/components/admin-account/update.d.ts +0 -8
- package/dist/admin/components/admin-account/update.js +0 -207
- package/dist/admin/components/admin-account/update.module.js +0 -8
- package/dist/admin/components/admin-account/update_module.css +0 -27
- package/dist/admin/components/admin-permissions/inspector.d.ts +0 -4
- package/dist/admin/components/admin-permissions/inspector.js +0 -284
- package/dist/admin/components/admin-permissions/inspector.module.js +0 -56
- package/dist/admin/components/admin-permissions/inspector_module.css +0 -238
- package/dist/admin/components/admin-roles/create.d.ts +0 -7
- package/dist/admin/components/admin-roles/create.js +0 -177
- package/dist/admin/components/admin-roles/create.module.js +0 -8
- package/dist/admin/components/admin-roles/create_module.css +0 -27
- package/dist/admin/components/admin-roles/permissions.d.ts +0 -10
- package/dist/admin/components/admin-roles/permissions.js +0 -303
- package/dist/admin/components/admin-roles/permissions.module.js +0 -44
- package/dist/admin/components/admin-roles/permissions_module.css +0 -192
- package/dist/admin/components/admin-roles/update.d.ts +0 -8
- package/dist/admin/components/admin-roles/update.js +0 -166
- package/dist/admin/components/admin-roles/update.module.js +0 -8
- package/dist/admin/components/admin-roles/update_module.css +0 -27
- package/dist/admin/components/admin-users/create.d.ts +0 -8
- package/dist/admin/components/admin-users/create.js +0 -268
- package/dist/admin/components/admin-users/create.module.js +0 -10
- package/dist/admin/components/admin-users/create_module.css +0 -45
- package/dist/admin/components/admin-users/roles.d.ts +0 -11
- package/dist/admin/components/admin-users/roles.js +0 -148
- package/dist/admin/components/admin-users/roles.module.js +0 -18
- package/dist/admin/components/admin-users/roles_module.css +0 -75
- package/dist/admin/components/admin-users/set-password.d.ts +0 -8
- package/dist/admin/components/admin-users/set-password.js +0 -170
- package/dist/admin/components/admin-users/set-password.module.js +0 -9
- package/dist/admin/components/admin-users/set-password_module.css +0 -31
- package/dist/admin/components/admin-users/update.d.ts +0 -8
- package/dist/admin/components/admin-users/update.js +0 -254
- package/dist/admin/components/admin-users/update.module.js +0 -9
- package/dist/admin/components/admin-users/update_module.css +0 -34
- package/dist/admin/components/auth/sign-in-form.d.ts +0 -12
- package/dist/admin/components/auth/sign-in-form.js +0 -115
- package/dist/admin/components/auth/sign-in-form.module.js +0 -12
- package/dist/admin/components/auth/sign-in-form_module.css +0 -41
- package/dist/admin/components/collections/diff-modal.module.js +0 -14
- package/dist/services/admin-services-context.d.ts +0 -16
- package/dist/services/admin-services-context.js +0 -13
- package/dist/services/admin-services-types.d.ts +0 -129
- package/dist/services/admin-services-types.js +0 -1
- package/src/admin/components/admin-account/change-password.module.css +0 -40
- package/src/admin/components/admin-account/change-password.tsx +0 -232
- package/src/admin/components/admin-account/container.module.css +0 -158
- package/src/admin/components/admin-account/container.tsx +0 -230
- package/src/admin/components/admin-account/update.module.css +0 -40
- package/src/admin/components/admin-account/update.tsx +0 -263
- package/src/admin/components/admin-permissions/inspector.module.css +0 -326
- package/src/admin/components/admin-permissions/inspector.tsx +0 -298
- package/src/admin/components/admin-roles/create.module.css +0 -40
- package/src/admin/components/admin-roles/create.tsx +0 -218
- package/src/admin/components/admin-roles/permissions.module.css +0 -279
- package/src/admin/components/admin-roles/permissions.tsx +0 -396
- package/src/admin/components/admin-roles/update.module.css +0 -40
- package/src/admin/components/admin-roles/update.tsx +0 -218
- package/src/admin/components/admin-users/create.module.css +0 -63
- package/src/admin/components/admin-users/create.tsx +0 -323
- package/src/admin/components/admin-users/roles.module.css +0 -119
- package/src/admin/components/admin-users/roles.tsx +0 -172
- package/src/admin/components/admin-users/set-password.module.css +0 -46
- package/src/admin/components/admin-users/set-password.tsx +0 -199
- package/src/admin/components/admin-users/update.module.css +0 -49
- package/src/admin/components/admin-users/update.tsx +0 -328
- package/src/admin/components/auth/sign-in-form.module.css +0 -62
- package/src/admin/components/auth/sign-in-form.tsx +0 -132
- package/src/services/admin-services-context.tsx +0 -35
- package/src/services/admin-services-types.ts +0 -177
- /package/dist/{admin/components/collections → widgets/status-badge}/status-badge.d.ts +0 -0
- /package/src/{admin/components/collections → widgets/diff-viewer}/diff-modal.module.css +0 -0
- /package/src/{admin/components/collections → widgets/status-badge}/status-badge.module.css +0 -0
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* This Source Code is subject to the terms of the Mozilla Public
|
|
5
|
-
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
-
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
7
|
-
*
|
|
8
|
-
* Copyright (c) Infonomic Company Limited
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Role details drawer form.
|
|
13
|
-
*
|
|
14
|
-
* Mirrors the admin-users update pattern: TanStack Form + Zod, blur-then-
|
|
15
|
-
* change validation, diff-against-original patch, optimistic concurrency
|
|
16
|
-
* via `vid`. `machine_name` is read-only here — it is captured at create
|
|
17
|
-
* time and immutable thereafter.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { useState } from 'react'
|
|
21
|
-
import { revalidateLogic, useForm } from '@tanstack/react-form-start'
|
|
22
|
-
|
|
23
|
-
import type { AdminRoleResponse } from '@byline/admin/admin-roles'
|
|
24
|
-
import cx from 'classnames'
|
|
25
|
-
import { z } from 'zod'
|
|
26
|
-
|
|
27
|
-
import { useBylineAdminServices } from '../../../services/admin-services-context.js'
|
|
28
|
-
import { Alert, Button, Input, LoaderEllipsis, TextArea } from '../../../uikit.js'
|
|
29
|
-
import styles from './update.module.css'
|
|
30
|
-
import type { UpdateAdminRoleInput } from '../../../services/admin-services-types.js'
|
|
31
|
-
|
|
32
|
-
const updateRoleSchema = z.object({
|
|
33
|
-
name: z.string().min(1, 'Name is required').max(128, 'Name must not exceed 128 characters'),
|
|
34
|
-
description: z.string().max(2000, 'Description must not exceed 2000 characters'),
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
type UpdateRoleValues = z.infer<typeof updateRoleSchema>
|
|
38
|
-
|
|
39
|
-
function defaultsFrom(role: AdminRoleResponse): UpdateRoleValues {
|
|
40
|
-
return {
|
|
41
|
-
name: role.name,
|
|
42
|
-
description: role.description ?? '',
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Build a patch object containing only fields whose values differ from the original role. */
|
|
47
|
-
function buildPatch(
|
|
48
|
-
values: UpdateRoleValues,
|
|
49
|
-
role: AdminRoleResponse
|
|
50
|
-
): UpdateAdminRoleInput['patch'] {
|
|
51
|
-
const patch: UpdateAdminRoleInput['patch'] = {}
|
|
52
|
-
const normaliseText = (value: string): string | null => (value.trim().length > 0 ? value : null)
|
|
53
|
-
const nextDescription = normaliseText(values.description)
|
|
54
|
-
const nextName = values.name.trim()
|
|
55
|
-
|
|
56
|
-
if (nextName !== role.name) patch.name = nextName
|
|
57
|
-
if (nextDescription !== role.description) patch.description = nextDescription
|
|
58
|
-
return patch
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
interface UpdateRoleProps {
|
|
62
|
-
role: AdminRoleResponse
|
|
63
|
-
onClose?: () => void
|
|
64
|
-
onSuccess?: (role: AdminRoleResponse) => void
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function UpdateRole({ role, onClose, onSuccess }: UpdateRoleProps) {
|
|
68
|
-
const { updateAdminRole } = useBylineAdminServices()
|
|
69
|
-
const [formError, setFormError] = useState<string | null>(null)
|
|
70
|
-
const [successMessage, setSuccessMessage] = useState<string | null>(null)
|
|
71
|
-
|
|
72
|
-
const form = useForm({
|
|
73
|
-
defaultValues: defaultsFrom(role),
|
|
74
|
-
validationLogic: revalidateLogic({
|
|
75
|
-
mode: 'blur',
|
|
76
|
-
modeAfterSubmission: 'change',
|
|
77
|
-
}),
|
|
78
|
-
validators: {
|
|
79
|
-
onDynamic: updateRoleSchema,
|
|
80
|
-
},
|
|
81
|
-
onSubmit: async ({ value }) => {
|
|
82
|
-
setFormError(null)
|
|
83
|
-
setSuccessMessage(null)
|
|
84
|
-
const patch = buildPatch(value, role)
|
|
85
|
-
if (Object.keys(patch).length === 0) {
|
|
86
|
-
setSuccessMessage('No changes to save.')
|
|
87
|
-
return
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
try {
|
|
91
|
-
const updated = await updateAdminRole({
|
|
92
|
-
data: { id: role.id, vid: role.vid, patch },
|
|
93
|
-
})
|
|
94
|
-
setSuccessMessage('Saved.')
|
|
95
|
-
onSuccess?.(updated)
|
|
96
|
-
} catch (err) {
|
|
97
|
-
const code = getErrorCode(err)
|
|
98
|
-
if (code === 'admin.roles.versionConflict') {
|
|
99
|
-
setFormError(
|
|
100
|
-
'This role has been modified elsewhere since you opened this form. Reload to get the latest values and try again.'
|
|
101
|
-
)
|
|
102
|
-
return
|
|
103
|
-
}
|
|
104
|
-
if (code === 'admin.roles.notFound') {
|
|
105
|
-
setFormError('This role no longer exists.')
|
|
106
|
-
return
|
|
107
|
-
}
|
|
108
|
-
setFormError('Could not save changes. Please try again.')
|
|
109
|
-
}
|
|
110
|
-
},
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
return (
|
|
114
|
-
<div className={cx('byline-role-update-wrap', styles.wrap)}>
|
|
115
|
-
<form
|
|
116
|
-
noValidate
|
|
117
|
-
onSubmit={(event) => {
|
|
118
|
-
event.preventDefault()
|
|
119
|
-
event.stopPropagation()
|
|
120
|
-
void form.handleSubmit()
|
|
121
|
-
}}
|
|
122
|
-
className={cx('byline-role-update-form', styles.form)}
|
|
123
|
-
>
|
|
124
|
-
{formError ? <Alert intent="danger">{formError}</Alert> : null}
|
|
125
|
-
{successMessage ? <Alert intent="success">{successMessage}</Alert> : null}
|
|
126
|
-
|
|
127
|
-
<form.Field name="name">
|
|
128
|
-
{(field) => (
|
|
129
|
-
<Input
|
|
130
|
-
label="Name"
|
|
131
|
-
id="role-name"
|
|
132
|
-
name={field.name}
|
|
133
|
-
value={field.state.value}
|
|
134
|
-
onBlur={field.handleBlur}
|
|
135
|
-
onChange={(e) => field.handleChange(e.currentTarget.value)}
|
|
136
|
-
error={field.state.meta.errors.length > 0}
|
|
137
|
-
errorText={firstError(field.state.meta.errors)}
|
|
138
|
-
required
|
|
139
|
-
/>
|
|
140
|
-
)}
|
|
141
|
-
</form.Field>
|
|
142
|
-
|
|
143
|
-
<Input
|
|
144
|
-
label="Machine name"
|
|
145
|
-
id="role-machine-name"
|
|
146
|
-
name="machine_name"
|
|
147
|
-
value={role.machine_name}
|
|
148
|
-
readOnly
|
|
149
|
-
disabled
|
|
150
|
-
helpText="The stable code-side handle. Cannot be changed after creation."
|
|
151
|
-
/>
|
|
152
|
-
|
|
153
|
-
<form.Field name="description">
|
|
154
|
-
{(field) => (
|
|
155
|
-
<TextArea
|
|
156
|
-
label="Description"
|
|
157
|
-
id="role-description"
|
|
158
|
-
name={field.name}
|
|
159
|
-
value={field.state.value}
|
|
160
|
-
onBlur={field.handleBlur}
|
|
161
|
-
onChange={(e) => field.handleChange(e.currentTarget.value)}
|
|
162
|
-
error={field.state.meta.errors.length > 0}
|
|
163
|
-
errorText={firstError(field.state.meta.errors)}
|
|
164
|
-
rows={3}
|
|
165
|
-
/>
|
|
166
|
-
)}
|
|
167
|
-
</form.Field>
|
|
168
|
-
|
|
169
|
-
<div className={cx('byline-role-update-actions', styles.actions)}>
|
|
170
|
-
<Button
|
|
171
|
-
type="button"
|
|
172
|
-
intent="secondary"
|
|
173
|
-
size="sm"
|
|
174
|
-
onClick={onClose}
|
|
175
|
-
className={cx('byline-role-update-action', styles.action)}
|
|
176
|
-
>
|
|
177
|
-
{successMessage ? 'Close' : 'Cancel'}
|
|
178
|
-
</Button>
|
|
179
|
-
<form.Subscribe
|
|
180
|
-
selector={(state) => ({
|
|
181
|
-
canSubmit: state.canSubmit,
|
|
182
|
-
isSubmitting: state.isSubmitting,
|
|
183
|
-
})}
|
|
184
|
-
>
|
|
185
|
-
{({ canSubmit, isSubmitting }) => (
|
|
186
|
-
<Button
|
|
187
|
-
size="sm"
|
|
188
|
-
intent="primary"
|
|
189
|
-
type="submit"
|
|
190
|
-
disabled={!canSubmit || isSubmitting}
|
|
191
|
-
className={cx('byline-role-update-action', styles.action)}
|
|
192
|
-
>
|
|
193
|
-
{isSubmitting === true ? <LoaderEllipsis size={42} /> : 'Save'}
|
|
194
|
-
</Button>
|
|
195
|
-
)}
|
|
196
|
-
</form.Subscribe>
|
|
197
|
-
</div>
|
|
198
|
-
</form>
|
|
199
|
-
</div>
|
|
200
|
-
)
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function firstError(errors: readonly unknown[]): string | undefined {
|
|
204
|
-
for (const err of errors) {
|
|
205
|
-
if (typeof err === 'string') return err
|
|
206
|
-
if (err && typeof err === 'object' && 'message' in err) {
|
|
207
|
-
const msg = (err as { message?: unknown }).message
|
|
208
|
-
if (typeof msg === 'string') return msg
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
return undefined
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function getErrorCode(err: unknown): string | null {
|
|
215
|
-
return typeof (err as { code?: unknown })?.code === 'string'
|
|
216
|
-
? (err as { code: string }).code
|
|
217
|
-
: null
|
|
218
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CreateAdminUser — drawer form for creating a new admin user.
|
|
3
|
-
*
|
|
4
|
-
* Override handles:
|
|
5
|
-
* .byline-user-create-wrap — outer container
|
|
6
|
-
* .byline-user-create-form — vertical-stack form element
|
|
7
|
-
* .byline-user-create-grid — two-column name grid (collapses on mobile)
|
|
8
|
-
* .byline-user-create-flags — checkbox stack (enabled / verified / super)
|
|
9
|
-
* .byline-user-create-actions — Cancel/Save row
|
|
10
|
-
* .byline-user-create-action — buttons in the actions row
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
.wrap,
|
|
14
|
-
:global(.byline-user-create-wrap) {
|
|
15
|
-
display: flex;
|
|
16
|
-
flex-direction: column;
|
|
17
|
-
gap: var(--spacing-8);
|
|
18
|
-
padding: var(--spacing-4);
|
|
19
|
-
margin-top: var(--spacing-4);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
.form,
|
|
23
|
-
:global(.byline-user-create-form) {
|
|
24
|
-
display: flex;
|
|
25
|
-
flex-direction: column;
|
|
26
|
-
gap: var(--spacing-16);
|
|
27
|
-
padding-top: var(--spacing-8);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
.grid,
|
|
31
|
-
:global(.byline-user-create-grid) {
|
|
32
|
-
display: grid;
|
|
33
|
-
grid-template-columns: 1fr;
|
|
34
|
-
gap: var(--spacing-16);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
@media (min-width: 40rem) {
|
|
38
|
-
.grid,
|
|
39
|
-
:global(.byline-user-create-grid) {
|
|
40
|
-
grid-template-columns: 1fr 1fr;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
.flags,
|
|
45
|
-
:global(.byline-user-create-flags) {
|
|
46
|
-
display: flex;
|
|
47
|
-
flex-direction: column;
|
|
48
|
-
gap: var(--spacing-8);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
.actions,
|
|
52
|
-
:global(.byline-user-create-actions) {
|
|
53
|
-
display: flex;
|
|
54
|
-
align-items: center;
|
|
55
|
-
justify-content: flex-end;
|
|
56
|
-
gap: var(--spacing-8);
|
|
57
|
-
margin-top: var(--spacing-16);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
.action,
|
|
61
|
-
:global(.byline-user-create-action) {
|
|
62
|
-
min-width: 4rem;
|
|
63
|
-
}
|
|
@@ -1,323 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* This Source Code is subject to the terms of the Mozilla Public
|
|
5
|
-
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
-
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
7
|
-
*
|
|
8
|
-
* Copyright (c) Infonomic Company Limited
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Create-admin-user drawer form.
|
|
13
|
-
*
|
|
14
|
-
* Rendered inside the list-view drawer. Same TanStack Form + Zod shape
|
|
15
|
-
* as the AccountDetails form — fields, blur-then-change validation,
|
|
16
|
-
* form-level alerts for errors that don't map onto a single field. On
|
|
17
|
-
* success the parent list-view navigates to the new user's detail page
|
|
18
|
-
* so the admin can finish configuring (roles, initial enablement).
|
|
19
|
-
*
|
|
20
|
-
* Extension point: future props (e.g. a `roles` array once the
|
|
21
|
-
* admin-roles module lands) plug in here — the list-route loader
|
|
22
|
-
* already runs in parallel, so adding a side-data fetch is a one-line
|
|
23
|
-
* change in the loader and a prop here.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
import { useState } from 'react'
|
|
27
|
-
import { revalidateLogic, useForm } from '@tanstack/react-form-start'
|
|
28
|
-
|
|
29
|
-
import type { AdminUserResponse } from '@byline/admin/admin-users'
|
|
30
|
-
import { passwordSchema } from '@byline/core/validation'
|
|
31
|
-
import cx from 'classnames'
|
|
32
|
-
import { z } from 'zod'
|
|
33
|
-
|
|
34
|
-
import { useBylineAdminServices } from '../../../services/admin-services-context.js'
|
|
35
|
-
import { Alert, Button, Checkbox, Input, LoaderEllipsis } from '../../../uikit.js'
|
|
36
|
-
import styles from './create.module.css'
|
|
37
|
-
|
|
38
|
-
const createAdminUserFormSchema = z.object({
|
|
39
|
-
email: z
|
|
40
|
-
.email({ message: 'Enter a valid email address' })
|
|
41
|
-
.min(3)
|
|
42
|
-
.max(254, 'Email must not exceed 254 characters'),
|
|
43
|
-
password: passwordSchema,
|
|
44
|
-
given_name: z.string().max(100, 'Given name must not exceed 100 characters'),
|
|
45
|
-
family_name: z.string().max(100, 'Family name must not exceed 100 characters'),
|
|
46
|
-
username: z.string().max(100, 'Username must not exceed 100 characters'),
|
|
47
|
-
is_super_admin: z.boolean(),
|
|
48
|
-
is_enabled: z.boolean(),
|
|
49
|
-
is_email_verified: z.boolean(),
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
type CreateAdminUserValues = z.infer<typeof createAdminUserFormSchema>
|
|
53
|
-
|
|
54
|
-
const initialValues: CreateAdminUserValues = {
|
|
55
|
-
email: '',
|
|
56
|
-
password: '',
|
|
57
|
-
given_name: '',
|
|
58
|
-
family_name: '',
|
|
59
|
-
username: '',
|
|
60
|
-
is_super_admin: false,
|
|
61
|
-
// Sensible defaults for an admin-created row. A brand-new admin user
|
|
62
|
-
// is almost always meant to sign in straight away; the admin picks
|
|
63
|
-
// whether they arrive pre-verified.
|
|
64
|
-
is_enabled: true,
|
|
65
|
-
is_email_verified: false,
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function normaliseText(value: string): string | null {
|
|
69
|
-
return value.trim().length > 0 ? value : null
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
interface CreateAdminUserProps {
|
|
73
|
-
onClose?: () => void
|
|
74
|
-
/** Called on successful create with the new user so the parent can navigate. */
|
|
75
|
-
onSuccess?: (user: AdminUserResponse) => void
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function CreateAdminUser({ onClose, onSuccess }: CreateAdminUserProps) {
|
|
79
|
-
const { createAdminUser } = useBylineAdminServices()
|
|
80
|
-
const [formError, setFormError] = useState<string | null>(null)
|
|
81
|
-
|
|
82
|
-
const form = useForm({
|
|
83
|
-
defaultValues: initialValues,
|
|
84
|
-
validationLogic: revalidateLogic({
|
|
85
|
-
mode: 'blur',
|
|
86
|
-
modeAfterSubmission: 'change',
|
|
87
|
-
}),
|
|
88
|
-
validators: {
|
|
89
|
-
onDynamic: createAdminUserFormSchema,
|
|
90
|
-
},
|
|
91
|
-
onSubmit: async ({ value }) => {
|
|
92
|
-
setFormError(null)
|
|
93
|
-
try {
|
|
94
|
-
const created = await createAdminUser({
|
|
95
|
-
data: {
|
|
96
|
-
email: value.email,
|
|
97
|
-
password: value.password,
|
|
98
|
-
given_name: normaliseText(value.given_name),
|
|
99
|
-
family_name: normaliseText(value.family_name),
|
|
100
|
-
username: normaliseText(value.username),
|
|
101
|
-
is_super_admin: value.is_super_admin,
|
|
102
|
-
is_enabled: value.is_enabled,
|
|
103
|
-
is_email_verified: value.is_email_verified,
|
|
104
|
-
},
|
|
105
|
-
})
|
|
106
|
-
// Clear the form so the next drawer open starts fresh — the
|
|
107
|
-
// list-view shows the success as a toast and keeps the drawer
|
|
108
|
-
// closed until the user clicks "+" again.
|
|
109
|
-
form.reset(initialValues)
|
|
110
|
-
onSuccess?.(created)
|
|
111
|
-
} catch (err) {
|
|
112
|
-
const code = getErrorCode(err)
|
|
113
|
-
if (code === 'admin.users.emailInUse') {
|
|
114
|
-
form.setFieldMeta('email', (meta) => ({
|
|
115
|
-
...meta,
|
|
116
|
-
errorMap: { ...meta.errorMap, onServer: 'This email is already in use.' },
|
|
117
|
-
errors: ['This email is already in use.'],
|
|
118
|
-
}))
|
|
119
|
-
return
|
|
120
|
-
}
|
|
121
|
-
setFormError('Could not create this admin user. Please try again.')
|
|
122
|
-
}
|
|
123
|
-
},
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
return (
|
|
127
|
-
<div className={cx('byline-user-create-wrap', styles.wrap)}>
|
|
128
|
-
<form
|
|
129
|
-
noValidate
|
|
130
|
-
onSubmit={(event) => {
|
|
131
|
-
event.preventDefault()
|
|
132
|
-
event.stopPropagation()
|
|
133
|
-
void form.handleSubmit()
|
|
134
|
-
}}
|
|
135
|
-
className={cx('byline-user-create-form', styles.form)}
|
|
136
|
-
>
|
|
137
|
-
{formError ? <Alert intent="danger">{formError}</Alert> : null}
|
|
138
|
-
|
|
139
|
-
<div className={cx('byline-user-create-grid', styles.grid)}>
|
|
140
|
-
<form.Field name="given_name">
|
|
141
|
-
{(field) => (
|
|
142
|
-
<Input
|
|
143
|
-
label="Given name"
|
|
144
|
-
id="new-given-name"
|
|
145
|
-
name={field.name}
|
|
146
|
-
value={field.state.value}
|
|
147
|
-
onBlur={field.handleBlur}
|
|
148
|
-
onChange={(e) => field.handleChange(e.currentTarget.value)}
|
|
149
|
-
error={field.state.meta.errors.length > 0}
|
|
150
|
-
errorText={firstError(field.state.meta.errors)}
|
|
151
|
-
autoComplete="given-name"
|
|
152
|
-
/>
|
|
153
|
-
)}
|
|
154
|
-
</form.Field>
|
|
155
|
-
|
|
156
|
-
<form.Field name="family_name">
|
|
157
|
-
{(field) => (
|
|
158
|
-
<Input
|
|
159
|
-
label="Family name"
|
|
160
|
-
id="new-family-name"
|
|
161
|
-
name={field.name}
|
|
162
|
-
value={field.state.value}
|
|
163
|
-
onBlur={field.handleBlur}
|
|
164
|
-
onChange={(e) => field.handleChange(e.currentTarget.value)}
|
|
165
|
-
error={field.state.meta.errors.length > 0}
|
|
166
|
-
errorText={firstError(field.state.meta.errors)}
|
|
167
|
-
autoComplete="family-name"
|
|
168
|
-
/>
|
|
169
|
-
)}
|
|
170
|
-
</form.Field>
|
|
171
|
-
</div>
|
|
172
|
-
|
|
173
|
-
<form.Field name="username">
|
|
174
|
-
{(field) => (
|
|
175
|
-
<Input
|
|
176
|
-
label="Username"
|
|
177
|
-
id="new-username"
|
|
178
|
-
name={field.name}
|
|
179
|
-
value={field.state.value}
|
|
180
|
-
onBlur={field.handleBlur}
|
|
181
|
-
onChange={(e) => field.handleChange(e.currentTarget.value)}
|
|
182
|
-
error={field.state.meta.errors.length > 0}
|
|
183
|
-
errorText={firstError(field.state.meta.errors)}
|
|
184
|
-
helpText="Optional."
|
|
185
|
-
autoComplete="username"
|
|
186
|
-
/>
|
|
187
|
-
)}
|
|
188
|
-
</form.Field>
|
|
189
|
-
|
|
190
|
-
<form.Field name="email">
|
|
191
|
-
{(field) => (
|
|
192
|
-
<Input
|
|
193
|
-
label="Email"
|
|
194
|
-
id="new-email"
|
|
195
|
-
name={field.name}
|
|
196
|
-
type="email"
|
|
197
|
-
value={field.state.value}
|
|
198
|
-
onBlur={field.handleBlur}
|
|
199
|
-
onChange={(e) => field.handleChange(e.currentTarget.value)}
|
|
200
|
-
error={field.state.meta.errors.length > 0}
|
|
201
|
-
errorText={firstError(field.state.meta.errors)}
|
|
202
|
-
autoComplete="email"
|
|
203
|
-
required
|
|
204
|
-
/>
|
|
205
|
-
)}
|
|
206
|
-
</form.Field>
|
|
207
|
-
|
|
208
|
-
<form.Field name="password">
|
|
209
|
-
{(field) => (
|
|
210
|
-
<Input
|
|
211
|
-
label="Initial password"
|
|
212
|
-
id="new-password"
|
|
213
|
-
name={field.name}
|
|
214
|
-
type="password"
|
|
215
|
-
value={field.state.value}
|
|
216
|
-
onBlur={field.handleBlur}
|
|
217
|
-
onChange={(e) => field.handleChange(e.currentTarget.value)}
|
|
218
|
-
error={field.state.meta.errors.length > 0}
|
|
219
|
-
errorText={firstError(field.state.meta.errors)}
|
|
220
|
-
helpText="The user can change it from their own account after signing in."
|
|
221
|
-
autoComplete="new-password"
|
|
222
|
-
required
|
|
223
|
-
/>
|
|
224
|
-
)}
|
|
225
|
-
</form.Field>
|
|
226
|
-
|
|
227
|
-
<div className={cx('byline-user-create-flags', styles.flags)}>
|
|
228
|
-
<form.Field name="is_enabled">
|
|
229
|
-
{(field) => (
|
|
230
|
-
<Checkbox
|
|
231
|
-
id="new-is-enabled"
|
|
232
|
-
name={field.name}
|
|
233
|
-
label="Enabled"
|
|
234
|
-
checked={field.state.value}
|
|
235
|
-
onCheckedChange={(checked) => field.handleChange(checked === true)}
|
|
236
|
-
helpText="Disabled accounts cannot sign in."
|
|
237
|
-
/>
|
|
238
|
-
)}
|
|
239
|
-
</form.Field>
|
|
240
|
-
|
|
241
|
-
<form.Field name="is_email_verified">
|
|
242
|
-
{(field) => (
|
|
243
|
-
<Checkbox
|
|
244
|
-
id="new-is-email-verified"
|
|
245
|
-
name={field.name}
|
|
246
|
-
label="Email verified"
|
|
247
|
-
checked={field.state.value}
|
|
248
|
-
onCheckedChange={(checked) => field.handleChange(checked === true)}
|
|
249
|
-
helpText="Skip the verification flow for this account."
|
|
250
|
-
/>
|
|
251
|
-
)}
|
|
252
|
-
</form.Field>
|
|
253
|
-
|
|
254
|
-
<form.Field name="is_super_admin">
|
|
255
|
-
{(field) => (
|
|
256
|
-
<Checkbox
|
|
257
|
-
id="new-is-super-admin"
|
|
258
|
-
name={field.name}
|
|
259
|
-
label="Super admin"
|
|
260
|
-
checked={field.state.value}
|
|
261
|
-
onCheckedChange={(checked) => field.handleChange(checked === true)}
|
|
262
|
-
helpText="Super admins bypass every ability check — grant with care."
|
|
263
|
-
/>
|
|
264
|
-
)}
|
|
265
|
-
</form.Field>
|
|
266
|
-
</div>
|
|
267
|
-
|
|
268
|
-
<div className={cx('byline-user-create-actions', styles.actions)}>
|
|
269
|
-
<Button
|
|
270
|
-
type="button"
|
|
271
|
-
intent="secondary"
|
|
272
|
-
size="sm"
|
|
273
|
-
onClick={onClose}
|
|
274
|
-
className={cx('byline-user-create-action', styles.action)}
|
|
275
|
-
>
|
|
276
|
-
Cancel
|
|
277
|
-
</Button>
|
|
278
|
-
<form.Subscribe
|
|
279
|
-
selector={(state) => ({
|
|
280
|
-
canSubmit: state.canSubmit,
|
|
281
|
-
isSubmitting: state.isSubmitting,
|
|
282
|
-
})}
|
|
283
|
-
>
|
|
284
|
-
{({ canSubmit, isSubmitting }) => (
|
|
285
|
-
<Button
|
|
286
|
-
size="sm"
|
|
287
|
-
intent="primary"
|
|
288
|
-
type="submit"
|
|
289
|
-
disabled={!canSubmit || isSubmitting}
|
|
290
|
-
className={cx('byline-user-create-action', styles.action)}
|
|
291
|
-
>
|
|
292
|
-
{isSubmitting === true ? <LoaderEllipsis size={42} /> : 'Save'}
|
|
293
|
-
</Button>
|
|
294
|
-
)}
|
|
295
|
-
</form.Subscribe>
|
|
296
|
-
</div>
|
|
297
|
-
</form>
|
|
298
|
-
</div>
|
|
299
|
-
)
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function firstError(errors: readonly unknown[]): string | undefined {
|
|
303
|
-
for (const err of errors) {
|
|
304
|
-
if (typeof err === 'string') return err
|
|
305
|
-
if (err && typeof err === 'object' && 'message' in err) {
|
|
306
|
-
const msg = (err as { message?: unknown }).message
|
|
307
|
-
if (typeof msg === 'string') return msg
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
return undefined
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function getErrorCode(err: unknown): string | null {
|
|
314
|
-
if (err && typeof err === 'object') {
|
|
315
|
-
const e = err as { code?: unknown; cause?: unknown }
|
|
316
|
-
if (typeof e.code === 'string') return e.code
|
|
317
|
-
if (e.cause && typeof e.cause === 'object' && 'code' in e.cause) {
|
|
318
|
-
const cause = e.cause as { code?: unknown }
|
|
319
|
-
if (typeof cause.code === 'string') return cause.code
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
return null
|
|
323
|
-
}
|