@byline/ui 2.4.0 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/dist/react.d.ts +10 -18
  2. package/dist/react.js +2 -15
  3. package/dist/{admin/components/collections → widgets/diff-viewer}/diff-modal.d.ts +8 -1
  4. package/dist/{admin/components/collections → widgets/diff-viewer}/diff-modal.js +4 -6
  5. package/dist/widgets/diff-viewer/diff-modal.module.js +14 -0
  6. package/dist/{admin/components/collections → widgets/diff-viewer}/diff-modal_module.css +9 -9
  7. package/dist/{admin/components/collections → widgets/status-badge}/status-badge.js +1 -1
  8. package/dist/{admin/components/collections → widgets/status-badge}/status-badge.module.js +3 -3
  9. package/dist/{admin/components/collections → widgets/status-badge}/status-badge_module.css +3 -3
  10. package/package.json +2 -4
  11. package/src/react.ts +12 -34
  12. package/src/{admin/components/collections → widgets/diff-viewer}/diff-modal.tsx +16 -5
  13. package/src/{admin/components/collections → widgets/status-badge}/status-badge.tsx +1 -1
  14. package/dist/admin/components/admin-account/change-password.d.ts +0 -8
  15. package/dist/admin/components/admin-account/change-password.js +0 -192
  16. package/dist/admin/components/admin-account/change-password.module.js +0 -8
  17. package/dist/admin/components/admin-account/change-password_module.css +0 -27
  18. package/dist/admin/components/admin-account/container.d.ts +0 -29
  19. package/dist/admin/components/admin-account/container.js +0 -299
  20. package/dist/admin/components/admin-account/container.module.js +0 -28
  21. package/dist/admin/components/admin-account/container_module.css +0 -106
  22. package/dist/admin/components/admin-account/update.d.ts +0 -8
  23. package/dist/admin/components/admin-account/update.js +0 -207
  24. package/dist/admin/components/admin-account/update.module.js +0 -8
  25. package/dist/admin/components/admin-account/update_module.css +0 -27
  26. package/dist/admin/components/admin-permissions/inspector.d.ts +0 -4
  27. package/dist/admin/components/admin-permissions/inspector.js +0 -284
  28. package/dist/admin/components/admin-permissions/inspector.module.js +0 -56
  29. package/dist/admin/components/admin-permissions/inspector_module.css +0 -238
  30. package/dist/admin/components/admin-roles/create.d.ts +0 -7
  31. package/dist/admin/components/admin-roles/create.js +0 -177
  32. package/dist/admin/components/admin-roles/create.module.js +0 -8
  33. package/dist/admin/components/admin-roles/create_module.css +0 -27
  34. package/dist/admin/components/admin-roles/permissions.d.ts +0 -10
  35. package/dist/admin/components/admin-roles/permissions.js +0 -303
  36. package/dist/admin/components/admin-roles/permissions.module.js +0 -44
  37. package/dist/admin/components/admin-roles/permissions_module.css +0 -192
  38. package/dist/admin/components/admin-roles/update.d.ts +0 -8
  39. package/dist/admin/components/admin-roles/update.js +0 -166
  40. package/dist/admin/components/admin-roles/update.module.js +0 -8
  41. package/dist/admin/components/admin-roles/update_module.css +0 -27
  42. package/dist/admin/components/admin-users/create.d.ts +0 -8
  43. package/dist/admin/components/admin-users/create.js +0 -268
  44. package/dist/admin/components/admin-users/create.module.js +0 -10
  45. package/dist/admin/components/admin-users/create_module.css +0 -45
  46. package/dist/admin/components/admin-users/roles.d.ts +0 -11
  47. package/dist/admin/components/admin-users/roles.js +0 -148
  48. package/dist/admin/components/admin-users/roles.module.js +0 -18
  49. package/dist/admin/components/admin-users/roles_module.css +0 -75
  50. package/dist/admin/components/admin-users/set-password.d.ts +0 -8
  51. package/dist/admin/components/admin-users/set-password.js +0 -170
  52. package/dist/admin/components/admin-users/set-password.module.js +0 -9
  53. package/dist/admin/components/admin-users/set-password_module.css +0 -31
  54. package/dist/admin/components/admin-users/update.d.ts +0 -8
  55. package/dist/admin/components/admin-users/update.js +0 -254
  56. package/dist/admin/components/admin-users/update.module.js +0 -9
  57. package/dist/admin/components/admin-users/update_module.css +0 -34
  58. package/dist/admin/components/auth/sign-in-form.d.ts +0 -12
  59. package/dist/admin/components/auth/sign-in-form.js +0 -115
  60. package/dist/admin/components/auth/sign-in-form.module.js +0 -12
  61. package/dist/admin/components/auth/sign-in-form_module.css +0 -41
  62. package/dist/admin/components/collections/diff-modal.module.js +0 -14
  63. package/dist/services/admin-services-context.d.ts +0 -16
  64. package/dist/services/admin-services-context.js +0 -13
  65. package/dist/services/admin-services-types.d.ts +0 -129
  66. package/dist/services/admin-services-types.js +0 -1
  67. package/src/admin/components/admin-account/change-password.module.css +0 -40
  68. package/src/admin/components/admin-account/change-password.tsx +0 -232
  69. package/src/admin/components/admin-account/container.module.css +0 -158
  70. package/src/admin/components/admin-account/container.tsx +0 -230
  71. package/src/admin/components/admin-account/update.module.css +0 -40
  72. package/src/admin/components/admin-account/update.tsx +0 -263
  73. package/src/admin/components/admin-permissions/inspector.module.css +0 -326
  74. package/src/admin/components/admin-permissions/inspector.tsx +0 -298
  75. package/src/admin/components/admin-roles/create.module.css +0 -40
  76. package/src/admin/components/admin-roles/create.tsx +0 -218
  77. package/src/admin/components/admin-roles/permissions.module.css +0 -279
  78. package/src/admin/components/admin-roles/permissions.tsx +0 -396
  79. package/src/admin/components/admin-roles/update.module.css +0 -40
  80. package/src/admin/components/admin-roles/update.tsx +0 -218
  81. package/src/admin/components/admin-users/create.module.css +0 -63
  82. package/src/admin/components/admin-users/create.tsx +0 -323
  83. package/src/admin/components/admin-users/roles.module.css +0 -119
  84. package/src/admin/components/admin-users/roles.tsx +0 -172
  85. package/src/admin/components/admin-users/set-password.module.css +0 -46
  86. package/src/admin/components/admin-users/set-password.tsx +0 -199
  87. package/src/admin/components/admin-users/update.module.css +0 -49
  88. package/src/admin/components/admin-users/update.tsx +0 -328
  89. package/src/admin/components/auth/sign-in-form.module.css +0 -62
  90. package/src/admin/components/auth/sign-in-form.tsx +0 -132
  91. package/src/services/admin-services-context.tsx +0 -35
  92. package/src/services/admin-services-types.ts +0 -177
  93. /package/dist/{admin/components/collections → widgets/status-badge}/status-badge.d.ts +0 -0
  94. /package/src/{admin/components/collections → widgets/diff-viewer}/diff-modal.module.css +0 -0
  95. /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">&nbsp;·&nbsp;{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
- }