@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,298 +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
|
-
* Read-only abilities inspector — see docs/AUTHN-AUTHZ.md.
|
|
13
|
-
*
|
|
14
|
-
* Top level: a collapsible group per ability source (collections.docs,
|
|
15
|
-
* admin.users, etc.), each containing the abilities that group
|
|
16
|
-
* registered. Group buckets and ordering come straight from the
|
|
17
|
-
* `AbilityRegistry.byGroup()` shape (registration order preserved).
|
|
18
|
-
*
|
|
19
|
-
* Per-ability: an inline-expandable row showing the roles that grant
|
|
20
|
-
* the ability and the distinct admin users who hold it transitively.
|
|
21
|
-
* The matrix is fetched lazily on first expand and cached for the
|
|
22
|
-
* lifetime of the page — the registry is small (~40 keys) but the
|
|
23
|
-
* matrix queries are not free, and most visitors only inspect a few
|
|
24
|
-
* keys.
|
|
25
|
-
*
|
|
26
|
-
* Stable override handles: see `inspector.module.css`.
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
import { useState } from 'react'
|
|
30
|
-
|
|
31
|
-
import type {
|
|
32
|
-
AbilityDescriptorResponse,
|
|
33
|
-
AbilityGroupResponse,
|
|
34
|
-
ListRegisteredAbilitiesResponse,
|
|
35
|
-
WhoHasAbilityResponse,
|
|
36
|
-
} from '@byline/admin/admin-permissions'
|
|
37
|
-
import cx from 'classnames'
|
|
38
|
-
|
|
39
|
-
import { useBylineAdminServices } from '../../../services/admin-services-context.js'
|
|
40
|
-
import { Button, Container, LoaderRing, Section } from '../../../uikit.js'
|
|
41
|
-
import styles from './inspector.module.css'
|
|
42
|
-
|
|
43
|
-
// --- helpers ---------------------------------------------------------------
|
|
44
|
-
|
|
45
|
-
function sourceVariant(source: AbilityDescriptorResponse['source']) {
|
|
46
|
-
switch (source) {
|
|
47
|
-
case 'collection':
|
|
48
|
-
return {
|
|
49
|
-
global: 'byline-inspector-row-source-collection',
|
|
50
|
-
local: styles['row-source-collection'],
|
|
51
|
-
}
|
|
52
|
-
case 'admin':
|
|
53
|
-
return {
|
|
54
|
-
global: 'byline-inspector-row-source-admin',
|
|
55
|
-
local: styles['row-source-admin'],
|
|
56
|
-
}
|
|
57
|
-
case 'plugin':
|
|
58
|
-
return {
|
|
59
|
-
global: 'byline-inspector-row-source-plugin',
|
|
60
|
-
local: styles['row-source-plugin'],
|
|
61
|
-
}
|
|
62
|
-
case 'core':
|
|
63
|
-
return {
|
|
64
|
-
global: 'byline-inspector-row-source-core',
|
|
65
|
-
local: styles['row-source-core'],
|
|
66
|
-
}
|
|
67
|
-
default:
|
|
68
|
-
return {
|
|
69
|
-
global: 'byline-inspector-row-source-unknown',
|
|
70
|
-
local: styles['row-source-unknown'],
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function displayUser(user: WhoHasAbilityResponse['users'][number]): string {
|
|
76
|
-
const parts = [user.given_name, user.family_name].filter(
|
|
77
|
-
(p): p is string => typeof p === 'string' && p.length > 0
|
|
78
|
-
)
|
|
79
|
-
return parts.length > 0 ? `${parts.join(' ')} (${user.email})` : user.email
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// --- expandable matrix row ------------------------------------------------
|
|
83
|
-
|
|
84
|
-
function MatrixPanel({ matrix }: { matrix: WhoHasAbilityResponse }) {
|
|
85
|
-
return (
|
|
86
|
-
<div className={cx('byline-inspector-matrix', styles.matrix)}>
|
|
87
|
-
<div>
|
|
88
|
-
<h4 className={cx('byline-inspector-matrix-title', styles['matrix-title'])}>
|
|
89
|
-
Roles ({matrix.roles.length})
|
|
90
|
-
</h4>
|
|
91
|
-
{matrix.roles.length === 0 ? (
|
|
92
|
-
<p className={cx('muted', 'byline-inspector-matrix-empty', styles['matrix-empty'])}>
|
|
93
|
-
No role grants this ability.
|
|
94
|
-
</p>
|
|
95
|
-
) : (
|
|
96
|
-
<ul className={cx('byline-inspector-matrix-list', styles['matrix-list'])}>
|
|
97
|
-
{matrix.roles.map((role) => (
|
|
98
|
-
<li
|
|
99
|
-
key={role.id}
|
|
100
|
-
className={cx('byline-inspector-matrix-item', styles['matrix-item'])}
|
|
101
|
-
>
|
|
102
|
-
<span className={cx('byline-inspector-matrix-name', styles['matrix-name'])}>
|
|
103
|
-
{role.name}
|
|
104
|
-
</span>
|
|
105
|
-
<span className="muted"> · {role.machine_name}</span>
|
|
106
|
-
</li>
|
|
107
|
-
))}
|
|
108
|
-
</ul>
|
|
109
|
-
)}
|
|
110
|
-
</div>
|
|
111
|
-
<div>
|
|
112
|
-
<h4 className={cx('byline-inspector-matrix-title', styles['matrix-title'])}>
|
|
113
|
-
Admin users ({matrix.users.length})
|
|
114
|
-
</h4>
|
|
115
|
-
{matrix.users.length === 0 ? (
|
|
116
|
-
<p className={cx('muted', 'byline-inspector-matrix-empty', styles['matrix-empty'])}>
|
|
117
|
-
No admin user holds this ability.
|
|
118
|
-
</p>
|
|
119
|
-
) : (
|
|
120
|
-
<ul className={cx('byline-inspector-matrix-list', styles['matrix-list'])}>
|
|
121
|
-
{matrix.users.map((user) => (
|
|
122
|
-
<li
|
|
123
|
-
key={user.id}
|
|
124
|
-
className={cx('byline-inspector-matrix-item', styles['matrix-item'])}
|
|
125
|
-
>
|
|
126
|
-
{displayUser(user)}
|
|
127
|
-
</li>
|
|
128
|
-
))}
|
|
129
|
-
</ul>
|
|
130
|
-
)}
|
|
131
|
-
</div>
|
|
132
|
-
</div>
|
|
133
|
-
)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
interface AbilityRowProps {
|
|
137
|
-
ability: AbilityDescriptorResponse
|
|
138
|
-
matrix: WhoHasAbilityResponse | undefined
|
|
139
|
-
loading: boolean
|
|
140
|
-
onToggle: () => void
|
|
141
|
-
expanded: boolean
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function AbilityRow({ ability, matrix, loading, onToggle, expanded }: AbilityRowProps) {
|
|
145
|
-
const sv = sourceVariant(ability.source)
|
|
146
|
-
return (
|
|
147
|
-
<div className={cx('byline-inspector-row', styles.row)}>
|
|
148
|
-
<div className={cx('byline-inspector-row-head', styles['row-head'])}>
|
|
149
|
-
<div className={cx('byline-inspector-row-info', styles['row-info'])}>
|
|
150
|
-
<div className={cx('byline-inspector-row-meta', styles['row-meta'])}>
|
|
151
|
-
<code className={cx('byline-inspector-row-key', styles['row-key'])}>{ability.key}</code>
|
|
152
|
-
<span
|
|
153
|
-
className={cx(
|
|
154
|
-
'byline-inspector-row-source',
|
|
155
|
-
styles['row-source'],
|
|
156
|
-
sv.global,
|
|
157
|
-
sv.local
|
|
158
|
-
)}
|
|
159
|
-
>
|
|
160
|
-
{ability.source ?? 'unknown'}
|
|
161
|
-
</span>
|
|
162
|
-
</div>
|
|
163
|
-
<p className={cx('byline-inspector-row-label', styles['row-label'])}>{ability.label}</p>
|
|
164
|
-
{ability.description ? (
|
|
165
|
-
<p
|
|
166
|
-
className={cx('muted', 'byline-inspector-row-description', styles['row-description'])}
|
|
167
|
-
>
|
|
168
|
-
{ability.description}
|
|
169
|
-
</p>
|
|
170
|
-
) : null}
|
|
171
|
-
</div>
|
|
172
|
-
<Button size="xs" intent="secondary" onClick={onToggle}>
|
|
173
|
-
{expanded ? 'Hide' : 'Holders'}
|
|
174
|
-
</Button>
|
|
175
|
-
</div>
|
|
176
|
-
{expanded ? (
|
|
177
|
-
loading ? (
|
|
178
|
-
<div className={cx('byline-inspector-loader', styles.loader)}>
|
|
179
|
-
<LoaderRing size={20} color="#888" />
|
|
180
|
-
<span className="muted">Loading…</span>
|
|
181
|
-
</div>
|
|
182
|
-
) : matrix ? (
|
|
183
|
-
<MatrixPanel matrix={matrix} />
|
|
184
|
-
) : null
|
|
185
|
-
) : null}
|
|
186
|
-
</div>
|
|
187
|
-
)
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// --- group section --------------------------------------------------------
|
|
191
|
-
|
|
192
|
-
interface GroupSectionProps {
|
|
193
|
-
group: AbilityGroupResponse
|
|
194
|
-
matrices: Record<string, WhoHasAbilityResponse>
|
|
195
|
-
loading: Set<string>
|
|
196
|
-
expanded: Set<string>
|
|
197
|
-
onToggle: (abilityKey: string) => void
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function GroupSection({ group, matrices, loading, expanded, onToggle }: GroupSectionProps) {
|
|
201
|
-
return (
|
|
202
|
-
<details open className={cx('byline-inspector-group', styles.group)}>
|
|
203
|
-
<summary className={cx('byline-inspector-group-summary', styles['group-summary'])}>
|
|
204
|
-
<span className={cx('byline-inspector-group-name', styles['group-name'])}>
|
|
205
|
-
{group.group}
|
|
206
|
-
</span>
|
|
207
|
-
<span className={cx('muted', 'byline-inspector-group-count', styles['group-count'])}>
|
|
208
|
-
{group.abilities.length} abilities
|
|
209
|
-
</span>
|
|
210
|
-
</summary>
|
|
211
|
-
<div className={cx('byline-inspector-group-body', styles['group-body'])}>
|
|
212
|
-
{group.abilities.map((ability) => (
|
|
213
|
-
<AbilityRow
|
|
214
|
-
key={ability.key}
|
|
215
|
-
ability={ability}
|
|
216
|
-
matrix={matrices[ability.key]}
|
|
217
|
-
loading={loading.has(ability.key)}
|
|
218
|
-
expanded={expanded.has(ability.key)}
|
|
219
|
-
onToggle={() => onToggle(ability.key)}
|
|
220
|
-
/>
|
|
221
|
-
))}
|
|
222
|
-
</div>
|
|
223
|
-
</details>
|
|
224
|
-
)
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// --- top level ------------------------------------------------------------
|
|
228
|
-
|
|
229
|
-
export function AbilitiesInspector({ data }: { data: ListRegisteredAbilitiesResponse }) {
|
|
230
|
-
const { whoHasAbility } = useBylineAdminServices()
|
|
231
|
-
const [expanded, setExpanded] = useState<Set<string>>(new Set())
|
|
232
|
-
const [loading, setLoading] = useState<Set<string>>(new Set())
|
|
233
|
-
const [matrices, setMatrices] = useState<Record<string, WhoHasAbilityResponse>>({})
|
|
234
|
-
|
|
235
|
-
async function handleToggle(abilityKey: string): Promise<void> {
|
|
236
|
-
setExpanded((current) => {
|
|
237
|
-
const next = new Set(current)
|
|
238
|
-
if (next.has(abilityKey)) {
|
|
239
|
-
next.delete(abilityKey)
|
|
240
|
-
} else {
|
|
241
|
-
next.add(abilityKey)
|
|
242
|
-
}
|
|
243
|
-
return next
|
|
244
|
-
})
|
|
245
|
-
|
|
246
|
-
// Lazy-load the matrix the first time a row expands. Cached for
|
|
247
|
-
// the lifetime of the page after that.
|
|
248
|
-
if (!matrices[abilityKey] && !loading.has(abilityKey)) {
|
|
249
|
-
setLoading((current) => new Set(current).add(abilityKey))
|
|
250
|
-
try {
|
|
251
|
-
const result = await whoHasAbility({ data: { ability: abilityKey } })
|
|
252
|
-
setMatrices((current) => ({ ...current, [abilityKey]: result }))
|
|
253
|
-
} finally {
|
|
254
|
-
setLoading((current) => {
|
|
255
|
-
const next = new Set(current)
|
|
256
|
-
next.delete(abilityKey)
|
|
257
|
-
return next
|
|
258
|
-
})
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
return (
|
|
264
|
-
<Section>
|
|
265
|
-
<Container>
|
|
266
|
-
<div className={cx('byline-inspector-head', styles.head)}>
|
|
267
|
-
<h1 className={cx('byline-inspector-title', styles.title)}>Abilities Inspector</h1>
|
|
268
|
-
<span className={cx('byline-inspector-count-pill', styles['count-pill'])}>
|
|
269
|
-
{data.total} registered
|
|
270
|
-
</span>
|
|
271
|
-
</div>
|
|
272
|
-
<p className={cx('muted', 'byline-inspector-lead', styles.lead)}>
|
|
273
|
-
Read-only view of every ability registered through <code>bylineCore.abilities</code>.
|
|
274
|
-
Collections auto-register CRUD + workflow abilities; admin subsystems contribute their own
|
|
275
|
-
keys at composition root via <code>registerAdminAbilities</code>.
|
|
276
|
-
</p>
|
|
277
|
-
{data.groups.length === 0 ? (
|
|
278
|
-
<p className={cx('muted', 'byline-inspector-empty', styles.empty)}>
|
|
279
|
-
No abilities are registered.
|
|
280
|
-
</p>
|
|
281
|
-
) : (
|
|
282
|
-
<div className={cx('byline-inspector-groups', styles.groups)}>
|
|
283
|
-
{data.groups.map((group) => (
|
|
284
|
-
<GroupSection
|
|
285
|
-
key={group.group}
|
|
286
|
-
group={group}
|
|
287
|
-
matrices={matrices}
|
|
288
|
-
loading={loading}
|
|
289
|
-
expanded={expanded}
|
|
290
|
-
onToggle={(key) => void handleToggle(key)}
|
|
291
|
-
/>
|
|
292
|
-
))}
|
|
293
|
-
</div>
|
|
294
|
-
)}
|
|
295
|
-
</Container>
|
|
296
|
-
</Section>
|
|
297
|
-
)
|
|
298
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CreateAdminRole — drawer form for creating a new role.
|
|
3
|
-
*
|
|
4
|
-
* Override handles:
|
|
5
|
-
* .byline-role-create-wrap — outer container
|
|
6
|
-
* .byline-role-create-form — vertical-stack form element
|
|
7
|
-
* .byline-role-create-actions — Cancel/Save row
|
|
8
|
-
* .byline-role-create-action — buttons in the actions row
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
.wrap,
|
|
12
|
-
:global(.byline-role-create-wrap) {
|
|
13
|
-
display: flex;
|
|
14
|
-
flex-direction: column;
|
|
15
|
-
gap: var(--spacing-8);
|
|
16
|
-
padding: var(--spacing-4);
|
|
17
|
-
margin-top: var(--spacing-4);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
.form,
|
|
21
|
-
:global(.byline-role-create-form) {
|
|
22
|
-
display: flex;
|
|
23
|
-
flex-direction: column;
|
|
24
|
-
gap: var(--spacing-16);
|
|
25
|
-
padding-top: var(--spacing-8);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
.actions,
|
|
29
|
-
:global(.byline-role-create-actions) {
|
|
30
|
-
display: flex;
|
|
31
|
-
align-items: center;
|
|
32
|
-
justify-content: flex-end;
|
|
33
|
-
gap: var(--spacing-8);
|
|
34
|
-
margin-top: var(--spacing-16);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
.action,
|
|
38
|
-
:global(.byline-role-create-action) {
|
|
39
|
-
min-width: 4rem;
|
|
40
|
-
}
|
|
@@ -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
|
-
* Create-admin-role drawer form.
|
|
13
|
-
*
|
|
14
|
-
* Same TanStack Form + Zod shape as the admin-users equivalent. The
|
|
15
|
-
* `machine_name` field is captured at create time only — it is the
|
|
16
|
-
* stable code-side handle for the role and is immutable thereafter
|
|
17
|
-
* (see the repository contract).
|
|
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 './create.module.css'
|
|
30
|
-
|
|
31
|
-
const createAdminRoleFormSchema = z.object({
|
|
32
|
-
name: z.string().min(1, 'Name is required').max(128, 'Name must not exceed 128 characters'),
|
|
33
|
-
machine_name: z
|
|
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
|
-
})
|
|
42
|
-
|
|
43
|
-
type CreateAdminRoleValues = z.infer<typeof createAdminRoleFormSchema>
|
|
44
|
-
|
|
45
|
-
const initialValues: CreateAdminRoleValues = {
|
|
46
|
-
name: '',
|
|
47
|
-
machine_name: '',
|
|
48
|
-
description: '',
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function normaliseText(value: string): string | null {
|
|
52
|
-
return value.trim().length > 0 ? value : null
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
interface CreateAdminRoleProps {
|
|
56
|
-
onClose?: () => void
|
|
57
|
-
onSuccess?: (role: AdminRoleResponse) => void
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function CreateAdminRole({ onClose, onSuccess }: CreateAdminRoleProps) {
|
|
61
|
-
const { createAdminRole } = useBylineAdminServices()
|
|
62
|
-
const [formError, setFormError] = useState<string | null>(null)
|
|
63
|
-
|
|
64
|
-
const form = useForm({
|
|
65
|
-
defaultValues: initialValues,
|
|
66
|
-
validationLogic: revalidateLogic({
|
|
67
|
-
mode: 'blur',
|
|
68
|
-
modeAfterSubmission: 'change',
|
|
69
|
-
}),
|
|
70
|
-
validators: {
|
|
71
|
-
onDynamic: createAdminRoleFormSchema,
|
|
72
|
-
},
|
|
73
|
-
onSubmit: async ({ value }) => {
|
|
74
|
-
setFormError(null)
|
|
75
|
-
try {
|
|
76
|
-
const created = await createAdminRole({
|
|
77
|
-
data: {
|
|
78
|
-
name: value.name.trim(),
|
|
79
|
-
machine_name: value.machine_name.trim(),
|
|
80
|
-
description: normaliseText(value.description),
|
|
81
|
-
},
|
|
82
|
-
})
|
|
83
|
-
form.reset(initialValues)
|
|
84
|
-
onSuccess?.(created)
|
|
85
|
-
} catch (err) {
|
|
86
|
-
const code = getErrorCode(err)
|
|
87
|
-
if (code === 'admin.roles.machineNameInUse') {
|
|
88
|
-
form.setFieldMeta('machine_name', (meta) => ({
|
|
89
|
-
...meta,
|
|
90
|
-
errorMap: { ...meta.errorMap, onServer: 'This machine name is already in use.' },
|
|
91
|
-
errors: ['This machine name is already in use.'],
|
|
92
|
-
}))
|
|
93
|
-
return
|
|
94
|
-
}
|
|
95
|
-
setFormError('Could not create this admin role. Please try again.')
|
|
96
|
-
}
|
|
97
|
-
},
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
return (
|
|
101
|
-
<div className={cx('byline-role-create-wrap', styles.wrap)}>
|
|
102
|
-
<form
|
|
103
|
-
noValidate
|
|
104
|
-
onSubmit={(event) => {
|
|
105
|
-
event.preventDefault()
|
|
106
|
-
event.stopPropagation()
|
|
107
|
-
void form.handleSubmit()
|
|
108
|
-
}}
|
|
109
|
-
className={cx('byline-role-create-form', styles.form)}
|
|
110
|
-
>
|
|
111
|
-
{formError ? <Alert intent="danger">{formError}</Alert> : null}
|
|
112
|
-
|
|
113
|
-
<form.Field name="name">
|
|
114
|
-
{(field) => (
|
|
115
|
-
<Input
|
|
116
|
-
label="Name"
|
|
117
|
-
id="new-role-name"
|
|
118
|
-
name={field.name}
|
|
119
|
-
value={field.state.value}
|
|
120
|
-
onBlur={field.handleBlur}
|
|
121
|
-
onChange={(e) => field.handleChange(e.currentTarget.value)}
|
|
122
|
-
error={field.state.meta.errors.length > 0}
|
|
123
|
-
errorText={firstError(field.state.meta.errors)}
|
|
124
|
-
helpText="Human-readable label, e.g. 'Editor'."
|
|
125
|
-
required
|
|
126
|
-
/>
|
|
127
|
-
)}
|
|
128
|
-
</form.Field>
|
|
129
|
-
|
|
130
|
-
<form.Field name="machine_name">
|
|
131
|
-
{(field) => (
|
|
132
|
-
<Input
|
|
133
|
-
label="Machine name"
|
|
134
|
-
id="new-role-machine-name"
|
|
135
|
-
name={field.name}
|
|
136
|
-
value={field.state.value}
|
|
137
|
-
onBlur={field.handleBlur}
|
|
138
|
-
onChange={(e) => field.handleChange(e.currentTarget.value)}
|
|
139
|
-
error={field.state.meta.errors.length > 0}
|
|
140
|
-
errorText={firstError(field.state.meta.errors)}
|
|
141
|
-
helpText="Stable code-side handle, e.g. 'editor'. Cannot be changed later."
|
|
142
|
-
required
|
|
143
|
-
/>
|
|
144
|
-
)}
|
|
145
|
-
</form.Field>
|
|
146
|
-
|
|
147
|
-
<form.Field name="description">
|
|
148
|
-
{(field) => (
|
|
149
|
-
<TextArea
|
|
150
|
-
label="Description"
|
|
151
|
-
id="new-role-description"
|
|
152
|
-
name={field.name}
|
|
153
|
-
value={field.state.value}
|
|
154
|
-
onBlur={field.handleBlur}
|
|
155
|
-
onChange={(e) => field.handleChange(e.currentTarget.value)}
|
|
156
|
-
error={field.state.meta.errors.length > 0}
|
|
157
|
-
errorText={firstError(field.state.meta.errors)}
|
|
158
|
-
rows={3}
|
|
159
|
-
/>
|
|
160
|
-
)}
|
|
161
|
-
</form.Field>
|
|
162
|
-
|
|
163
|
-
<div className={cx('byline-role-create-actions', styles.actions)}>
|
|
164
|
-
<Button
|
|
165
|
-
type="button"
|
|
166
|
-
intent="secondary"
|
|
167
|
-
size="sm"
|
|
168
|
-
onClick={onClose}
|
|
169
|
-
className={cx('byline-role-create-action', styles.action)}
|
|
170
|
-
>
|
|
171
|
-
Cancel
|
|
172
|
-
</Button>
|
|
173
|
-
<form.Subscribe
|
|
174
|
-
selector={(state) => ({
|
|
175
|
-
canSubmit: state.canSubmit,
|
|
176
|
-
isSubmitting: state.isSubmitting,
|
|
177
|
-
})}
|
|
178
|
-
>
|
|
179
|
-
{({ canSubmit, isSubmitting }) => (
|
|
180
|
-
<Button
|
|
181
|
-
size="sm"
|
|
182
|
-
intent="primary"
|
|
183
|
-
type="submit"
|
|
184
|
-
disabled={!canSubmit || isSubmitting}
|
|
185
|
-
className={cx('byline-role-create-action', styles.action)}
|
|
186
|
-
>
|
|
187
|
-
{isSubmitting === true ? <LoaderEllipsis size={42} /> : 'Save'}
|
|
188
|
-
</Button>
|
|
189
|
-
)}
|
|
190
|
-
</form.Subscribe>
|
|
191
|
-
</div>
|
|
192
|
-
</form>
|
|
193
|
-
</div>
|
|
194
|
-
)
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function firstError(errors: readonly unknown[]): string | undefined {
|
|
198
|
-
for (const err of errors) {
|
|
199
|
-
if (typeof err === 'string') return err
|
|
200
|
-
if (err && typeof err === 'object' && 'message' in err) {
|
|
201
|
-
const msg = (err as { message?: unknown }).message
|
|
202
|
-
if (typeof msg === 'string') return msg
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
return undefined
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function getErrorCode(err: unknown): string | null {
|
|
209
|
-
if (err && typeof err === 'object') {
|
|
210
|
-
const e = err as { code?: unknown; cause?: unknown }
|
|
211
|
-
if (typeof e.code === 'string') return e.code
|
|
212
|
-
if (e.cause && typeof e.cause === 'object' && 'code' in e.cause) {
|
|
213
|
-
const cause = e.cause as { code?: unknown }
|
|
214
|
-
if (typeof cause.code === 'string') return cause.code
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
return null
|
|
218
|
-
}
|