@byline/admin 5.2.0 → 6.0.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.
Files changed (78) hide show
  1. package/dist/exports-parity.test.node.js +37 -0
  2. package/dist/fields/array/array-field.jsx +209 -0
  3. package/dist/fields/blocks/blocks-field.jsx +248 -0
  4. package/dist/fields/checkbox/checkbox-field.jsx +24 -0
  5. package/dist/fields/code/code-editor.jsx +211 -0
  6. package/dist/fields/code/code-field.jsx +109 -0
  7. package/dist/fields/column-formatter.jsx +25 -0
  8. package/dist/fields/date-time-formatter.jsx +18 -0
  9. package/dist/fields/datetime/datetime-field.jsx +22 -0
  10. package/dist/fields/draggable-context-menu.jsx +50 -0
  11. package/dist/fields/field-admin.test.node.js +40 -0
  12. package/dist/fields/field-renderer.jsx +136 -0
  13. package/dist/fields/field-services-context.jsx +17 -0
  14. package/dist/fields/file/file-field.jsx +176 -0
  15. package/dist/fields/file/file-upload-field.jsx +89 -0
  16. package/dist/fields/group/group-field.jsx +46 -0
  17. package/dist/fields/image/image-field.jsx +183 -0
  18. package/dist/fields/image/image-upload-field.jsx +157 -0
  19. package/dist/fields/local-date-time.jsx +69 -0
  20. package/dist/fields/locale-badge.jsx +19 -0
  21. package/dist/fields/numerical/numerical-field.jsx +93 -0
  22. package/dist/fields/relation/relation-column-formatter.jsx +74 -0
  23. package/dist/fields/relation/relation-display.jsx +118 -0
  24. package/dist/fields/relation/relation-field.jsx +108 -0
  25. package/dist/fields/relation/relation-many-field.jsx +142 -0
  26. package/dist/fields/relation/relation-picker.jsx +240 -0
  27. package/dist/fields/relation/relation-summary.jsx +45 -0
  28. package/dist/fields/select/select-field.jsx +27 -0
  29. package/dist/fields/sortable-item.jsx +54 -0
  30. package/dist/fields/text/text-field.jsx +79 -0
  31. package/dist/fields/text-area/text-area-field.jsx +79 -0
  32. package/dist/forms/available-locales-reconcile.test.node.js +43 -0
  33. package/dist/forms/available-locales-widget.jsx +63 -0
  34. package/dist/forms/document-actions.jsx +486 -0
  35. package/dist/forms/form-context.jsx +598 -0
  36. package/dist/forms/form-modals.jsx +128 -0
  37. package/dist/forms/form-renderer.jsx +532 -0
  38. package/dist/forms/form-status-display.jsx +69 -0
  39. package/dist/forms/navigation-guard.jsx +68 -0
  40. package/dist/forms/nested-path.test.node.js +115 -0
  41. package/dist/forms/path-widget.jsx +125 -0
  42. package/dist/forms/pending-uploads.test.node.js +19 -0
  43. package/dist/forms/repeating-items.test.node.js +29 -0
  44. package/dist/forms/scheduled-publication-control.jsx +389 -0
  45. package/dist/forms/scheduled-publication-state.test.node.js +121 -0
  46. package/dist/forms/scheduled-publication-time.test.node.js +70 -0
  47. package/dist/forms/status-transitions.test.node.js +76 -0
  48. package/dist/forms/tree-placement-widget.jsx +160 -0
  49. package/dist/forms/upload-executor.test.node.js +368 -0
  50. package/dist/forms/use-form-layout.test.node.js +69 -0
  51. package/dist/modules/admin-account/components/change-password.jsx +169 -0
  52. package/dist/modules/admin-account/components/container.jsx +164 -0
  53. package/dist/modules/admin-account/components/preferences.jsx +148 -0
  54. package/dist/modules/admin-account/components/theme-switch.jsx +52 -0
  55. package/dist/modules/admin-account/components/update.jsx +186 -0
  56. package/dist/modules/admin-permissions/components/inspector.jsx +200 -0
  57. package/dist/modules/admin-preferences/schemas.test.node.js +48 -0
  58. package/dist/modules/admin-roles/components/create.jsx +155 -0
  59. package/dist/modules/admin-roles/components/permissions.jsx +239 -0
  60. package/dist/modules/admin-roles/components/update.jsx +148 -0
  61. package/dist/modules/admin-users/components/create.jsx +206 -0
  62. package/dist/modules/admin-users/components/roles.jsx +126 -0
  63. package/dist/modules/admin-users/components/set-password.jsx +133 -0
  64. package/dist/modules/admin-users/components/update.jsx +215 -0
  65. package/dist/modules/analytics/components/dashboard.jsx +208 -0
  66. package/dist/modules/analytics/components/dashboard.test.node.js +162 -0
  67. package/dist/modules/analytics/components/timeseries.jsx +193 -0
  68. package/dist/modules/auth/components/sign-in-form.jsx +89 -0
  69. package/dist/modules/auth/safe-redirect.test.node.js +40 -0
  70. package/dist/modules/auth/sign-in-form-props.test.node.js +7 -0
  71. package/dist/presentation/group.jsx +26 -0
  72. package/dist/presentation/row.jsx +23 -0
  73. package/dist/presentation/tabs.jsx +39 -0
  74. package/dist/services/admin-services-context.jsx +17 -0
  75. package/dist/widgets/diff-viewer/diff-modal.jsx +110 -0
  76. package/dist/widgets/source-locale-badge/source-locale-badge.jsx +28 -0
  77. package/dist/widgets/status-badge/status-badge.jsx +46 -0
  78. package/package.json +7 -7
@@ -0,0 +1,126 @@
1
+ 'use client';
2
+ /**
3
+ * This Source Code is subject to the terms of the Mozilla Public
4
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
5
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
+ *
7
+ * Copyright (c) Infonomic Company Limited
8
+ */
9
+ /**
10
+ * User-roles drawer.
11
+ *
12
+ * Renders a flat checkbox list of every available role, pre-checked
13
+ * from the user's current assignments. On save we wholesale-replace
14
+ * the user's role-set via `setUserRoles`; the response carries the
15
+ * authoritative stored set so the editor's "initial" baseline resets
16
+ * cleanly.
17
+ *
18
+ * Standard drawer pattern (no view/edit mode toggle) — role lists are
19
+ * short by design and the drawer is a short-lived edit context, not
20
+ * a steady-state inspector. Save + Cancel are always visible; Save
21
+ * is disabled until dirty.
22
+ */
23
+ import { useState } from 'react';
24
+ import { useTranslation } from '@byline/i18n/react';
25
+ import { Alert, Button, Checkbox, LoaderEllipsis } from '@byline/ui/react';
26
+ import cx from 'clsx';
27
+ import { useBylineAdminServices } from '../../../services/admin-services-context.js';
28
+ import styles from './roles.module.css';
29
+ function setsEqual(a, b) {
30
+ if (a.size !== b.size)
31
+ return false;
32
+ for (const item of a)
33
+ if (!b.has(item))
34
+ return false;
35
+ return true;
36
+ }
37
+ export function UserRoles({ user, allRoles, initialRoleIds, onClose, onSaved }) {
38
+ const { setUserRoles } = useBylineAdminServices();
39
+ const { t } = useTranslation('byline-admin');
40
+ const [initialSet, setInitialSet] = useState(() => new Set(initialRoleIds));
41
+ const [selected, setSelected] = useState(() => new Set(initialRoleIds));
42
+ const [saving, setSaving] = useState(false);
43
+ const [error, setError] = useState(null);
44
+ const [successMessage, setSuccessMessage] = useState(null);
45
+ const isDirty = !setsEqual(selected, initialSet);
46
+ function handleToggle(roleId, checked) {
47
+ setSelected((current) => {
48
+ const next = new Set(current);
49
+ if (checked)
50
+ next.add(roleId);
51
+ else
52
+ next.delete(roleId);
53
+ return next;
54
+ });
55
+ setSuccessMessage(null);
56
+ }
57
+ async function handleSave() {
58
+ if (saving)
59
+ return;
60
+ setSaving(true);
61
+ setError(null);
62
+ setSuccessMessage(null);
63
+ try {
64
+ const response = await setUserRoles({
65
+ data: { userId: user.id, roleIds: Array.from(selected) },
66
+ });
67
+ const storedSet = new Set(response.roles.map((r) => r.id));
68
+ setInitialSet(storedSet);
69
+ setSelected(new Set(storedSet));
70
+ setSuccessMessage(t('common.feedback.saved'));
71
+ onSaved?.(response);
72
+ }
73
+ catch (err) {
74
+ const code = getErrorCode(err);
75
+ if (code === 'admin.roles.userNotFound') {
76
+ setError(t('adminUsers.roles.errors.userNotFound'));
77
+ }
78
+ else if (code === 'admin.roles.notFound') {
79
+ setError(t('adminUsers.roles.errors.roleNotFound'));
80
+ }
81
+ else {
82
+ setError(t('adminUsers.roles.errors.fallback'));
83
+ }
84
+ }
85
+ finally {
86
+ setSaving(false);
87
+ }
88
+ }
89
+ return (<div className={cx('byline-user-roles-wrap', styles.wrap)}>
90
+ {error ? <Alert intent="danger">{error}</Alert> : null}
91
+ {successMessage ? <Alert intent="success">{successMessage}</Alert> : null}
92
+
93
+ {allRoles.length === 0 ? (<p className={cx('muted', 'byline-user-roles-empty', styles.empty)}>
94
+ {t('adminUsers.roles.emptyCatalog')}
95
+ </p>) : (<div className={cx('byline-user-roles-list', styles.list)}>
96
+ {allRoles.map((role) => (<div key={role.id} className={cx('byline-user-roles-row', styles.row)}>
97
+ <Checkbox id={`role-${role.id}`} name={`role-${role.id}`} checked={selected.has(role.id)} disabled={saving} onCheckedChange={(checked) => handleToggle(role.id, checked === true)} containerClasses={cx('byline-user-roles-checkbox-auto', styles['checkbox-auto'])} componentClasses={cx('byline-user-roles-checkbox-auto', styles['checkbox-auto'])}/>
98
+ <label htmlFor={`role-${role.id}`} className={cx('byline-user-roles-label', styles.label)}>
99
+ <div className={cx('byline-user-roles-label-head', styles['label-head'])}>
100
+ <span className={cx('byline-user-roles-name', styles.name)}>{role.name}</span>
101
+ <code className={cx('byline-user-roles-machine', styles.machine)}>
102
+ {role.machine_name}
103
+ </code>
104
+ </div>
105
+ {role.description ? (<p className={cx('muted', 'byline-user-roles-description', styles.description)}>
106
+ {role.description}
107
+ </p>) : null}
108
+ </label>
109
+ </div>))}
110
+ </div>)}
111
+
112
+ <div className={cx('byline-user-roles-actions', styles.actions)}>
113
+ <Button type="button" intent="secondary" size="xs" onClick={onClose} disabled={saving} className={cx('byline-user-roles-action', styles.action)}>
114
+ {successMessage ? t('common.actions.close') : t('common.actions.cancel')}
115
+ </Button>
116
+ <Button type="button" intent="primary" size="xs" onClick={() => void handleSave()} disabled={saving || !isDirty} className={cx('byline-user-roles-action', styles.action)}>
117
+ {saving ? <LoaderEllipsis size={30}/> : t('common.actions.save')}
118
+ </Button>
119
+ </div>
120
+ </div>);
121
+ }
122
+ function getErrorCode(err) {
123
+ return typeof err?.code === 'string'
124
+ ? err.code
125
+ : null;
126
+ }
@@ -0,0 +1,133 @@
1
+ 'use client';
2
+ /**
3
+ * This Source Code is subject to the terms of the Mozilla Public
4
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
5
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
+ *
7
+ * Copyright (c) Infonomic Company Limited
8
+ */
9
+ /**
10
+ * Set-password drawer form.
11
+ *
12
+ * Admin-facing "set this user's password" flow — used when an admin is
13
+ * resetting someone else's password. Server-side policy (min 12 chars,
14
+ * max 256) is duplicated client-side via Zod for immediate field
15
+ * validation. A matching-confirmation field catches typos without a
16
+ * round-trip.
17
+ *
18
+ * The server fn returns the updated user so we can lift the bumped
19
+ * `vid` back into the container; the drawer doesn't need to re-fetch.
20
+ */
21
+ import { useMemo, useState } from 'react';
22
+ import { revalidateLogic, useForm } from '@tanstack/react-form-start';
23
+ import { passwordSchema } from '@byline/core/validation';
24
+ import { useTranslation } from '@byline/i18n/react';
25
+ import { Alert, Button, InputPassword, LoaderEllipsis } from '@byline/ui/react';
26
+ import cx from 'clsx';
27
+ import { z } from 'zod';
28
+ import { translateValidationError } from '../../../lib/translate-validation-error.js';
29
+ import { useBylineAdminServices } from '../../../services/admin-services-context.js';
30
+ import styles from './set-password.module.css';
31
+ export function SetPassword({ user, onClose, onSuccess }) {
32
+ const { setAdminUserPassword } = useBylineAdminServices();
33
+ const { t } = useTranslation('byline-admin');
34
+ const [formError, setFormError] = useState(null);
35
+ const [successMessage, setSuccessMessage] = useState(null);
36
+ const setPasswordFormSchema = useMemo(() => z
37
+ .object({
38
+ password: passwordSchema,
39
+ confirm: z.string({
40
+ message: t('account.changePassword.errors.confirmRequired'),
41
+ }),
42
+ })
43
+ .refine((v) => v.password === v.confirm, {
44
+ message: t('account.changePassword.errors.mismatch'),
45
+ path: ['confirm'],
46
+ }), [t]);
47
+ const form = useForm({
48
+ defaultValues: { password: '', confirm: '' },
49
+ validationLogic: revalidateLogic({
50
+ mode: 'blur',
51
+ modeAfterSubmission: 'change',
52
+ }),
53
+ validators: {
54
+ onDynamic: setPasswordFormSchema,
55
+ },
56
+ onSubmit: async ({ value }) => {
57
+ setFormError(null);
58
+ setSuccessMessage(null);
59
+ try {
60
+ const updated = await setAdminUserPassword({
61
+ data: { id: user.id, vid: user.vid, password: value.password },
62
+ });
63
+ setSuccessMessage(t('account.changePassword.feedback.updated'));
64
+ form.reset({ password: '', confirm: '' });
65
+ onSuccess?.(updated);
66
+ }
67
+ catch (err) {
68
+ const code = getErrorCode(err);
69
+ if (code === 'admin.users.versionConflict') {
70
+ setFormError(t('adminUsers.update.errors.versionConflict'));
71
+ return;
72
+ }
73
+ if (code === 'admin.users.notFound') {
74
+ setFormError(t('adminUsers.update.errors.notFound'));
75
+ return;
76
+ }
77
+ setFormError(t('adminUsers.setPassword.errors.fallback'));
78
+ }
79
+ },
80
+ });
81
+ return (<div className={cx('byline-user-set-password-wrap', styles.wrap)}>
82
+ <form method="post" noValidate onSubmit={(event) => {
83
+ event.preventDefault();
84
+ event.stopPropagation();
85
+ void form.handleSubmit();
86
+ }} className={cx('byline-user-set-password-form', styles.form)}>
87
+ {formError ? <Alert intent="danger">{formError}</Alert> : null}
88
+ {successMessage ? <Alert intent="success">{successMessage}</Alert> : null}
89
+
90
+ <p className="muted">{t('adminUsers.setPassword.intro', { email: user.email })}</p>
91
+
92
+ <form.Field name="password">
93
+ {(field) => (<InputPassword label={t('account.changePassword.fields.new')} id="password" name={field.name} value={field.state.value} onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.currentTarget.value)} error={field.state.meta.errors.length > 0} errorText={translateValidationError(t, firstError(field.state.meta.errors))} autoComplete="new-password" required/>)}
94
+ </form.Field>
95
+
96
+ <form.Field name="confirm">
97
+ {(field) => (<InputPassword label={t('account.changePassword.fields.confirm')} id="confirm" name={field.name} value={field.state.value} onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.currentTarget.value)} error={field.state.meta.errors.length > 0} errorText={firstError(field.state.meta.errors)} autoComplete="new-password" required/>)}
98
+ </form.Field>
99
+
100
+ <div className={cx('byline-user-set-password-actions', styles.actions)}>
101
+ <Button type="button" intent="secondary" size="sm" onClick={onClose} className={cx('byline-user-set-password-action', styles.action)}>
102
+ {successMessage ? t('common.actions.close') : t('common.actions.cancel')}
103
+ </Button>
104
+ <form.Subscribe selector={(state) => ({
105
+ canSubmit: state.canSubmit,
106
+ isSubmitting: state.isSubmitting,
107
+ isDirty: state.isDirty,
108
+ })}>
109
+ {({ canSubmit, isSubmitting }) => (<Button size="sm" intent="primary" type="submit" disabled={!canSubmit || isSubmitting} className={cx('byline-user-set-password-action', styles.action)}>
110
+ {isSubmitting === true ? <LoaderEllipsis size={42}/> : t('common.actions.save')}
111
+ </Button>)}
112
+ </form.Subscribe>
113
+ </div>
114
+ </form>
115
+ </div>);
116
+ }
117
+ function firstError(errors) {
118
+ for (const err of errors) {
119
+ if (typeof err === 'string')
120
+ return err;
121
+ if (err && typeof err === 'object' && 'message' in err) {
122
+ const msg = err.message;
123
+ if (typeof msg === 'string')
124
+ return msg;
125
+ }
126
+ }
127
+ return undefined;
128
+ }
129
+ function getErrorCode(err) {
130
+ return typeof err?.code === 'string'
131
+ ? err.code
132
+ : null;
133
+ }
@@ -0,0 +1,215 @@
1
+ 'use client';
2
+ /**
3
+ * This Source Code is subject to the terms of the Mozilla Public
4
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
5
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
+ *
7
+ * Copyright (c) Infonomic Company Limited
8
+ */
9
+ /**
10
+ * Account details drawer form.
11
+ *
12
+ * Client-side validation runs through TanStack Form's `onDynamic` +
13
+ * Zod — same rules the server uses, re-declared here so field errors
14
+ * show up without a network round-trip. On submit the form diffs
15
+ * against the original row and sends only the *changed* fields as a
16
+ * patch, plus the `vid` the form was opened with, so a concurrent edit
17
+ * elsewhere comes back as `admin.users.versionConflict` and we surface
18
+ * a reload prompt.
19
+ */
20
+ import { useMemo, useState } from 'react';
21
+ import { revalidateLogic, useForm } from '@tanstack/react-form-start';
22
+ import { useTranslation } from '@byline/i18n/react';
23
+ import { Alert, Button, Checkbox, Input, LoaderEllipsis } from '@byline/ui/react';
24
+ import cx from 'clsx';
25
+ import { z } from 'zod';
26
+ import { useBylineAdminServices } from '../../../services/admin-services-context.js';
27
+ import styles from './update.module.css';
28
+ const MAX_NAME = 100;
29
+ const MAX_USERNAME = 100;
30
+ const MAX_EMAIL = 254;
31
+ function defaultsFrom(user) {
32
+ return {
33
+ given_name: user.given_name ?? '',
34
+ family_name: user.family_name ?? '',
35
+ username: user.username ?? '',
36
+ email: user.email,
37
+ is_super_admin: user.is_super_admin,
38
+ is_enabled: user.is_enabled,
39
+ is_email_verified: user.is_email_verified,
40
+ };
41
+ }
42
+ /** Build a patch object containing only fields whose values differ from the original user row. */
43
+ function buildPatch(values, user) {
44
+ const patch = {};
45
+ // Text fields: treat empty string as null (clear). null === null matches,
46
+ // '' → null ≠ current null stays consistent.
47
+ const normaliseText = (value) => (value.trim().length > 0 ? value : null);
48
+ const nextGiven = normaliseText(values.given_name);
49
+ const nextFamily = normaliseText(values.family_name);
50
+ const nextUsername = normaliseText(values.username);
51
+ if (nextGiven !== user.given_name)
52
+ patch.given_name = nextGiven;
53
+ if (nextFamily !== user.family_name)
54
+ patch.family_name = nextFamily;
55
+ if (nextUsername !== user.username)
56
+ patch.username = nextUsername;
57
+ if (values.email !== user.email)
58
+ patch.email = values.email;
59
+ if (values.is_super_admin !== user.is_super_admin)
60
+ patch.is_super_admin = values.is_super_admin;
61
+ if (values.is_enabled !== user.is_enabled)
62
+ patch.is_enabled = values.is_enabled;
63
+ if (values.is_email_verified !== user.is_email_verified)
64
+ patch.is_email_verified = values.is_email_verified;
65
+ return patch;
66
+ }
67
+ export function UpdateUser({ user, onClose, onSuccess }) {
68
+ const { updateAdminUser } = useBylineAdminServices();
69
+ const { t } = useTranslation('byline-admin');
70
+ const [formError, setFormError] = useState(null);
71
+ const [successMessage, setSuccessMessage] = useState(null);
72
+ const updateUserSchema = useMemo(() => z.object({
73
+ given_name: z
74
+ .string()
75
+ .max(MAX_NAME, t('account.update.errors.givenNameTooLong', { max: MAX_NAME })),
76
+ family_name: z
77
+ .string()
78
+ .max(MAX_NAME, t('account.update.errors.familyNameTooLong', { max: MAX_NAME })),
79
+ username: z
80
+ .string()
81
+ .max(MAX_USERNAME, t('account.update.errors.usernameTooLong', { max: MAX_USERNAME })),
82
+ email: z
83
+ .email({ message: t('account.update.errors.invalidEmail') })
84
+ .min(3)
85
+ .max(MAX_EMAIL, t('account.update.errors.emailTooLong', { max: MAX_EMAIL })),
86
+ is_super_admin: z.boolean(),
87
+ is_enabled: z.boolean(),
88
+ is_email_verified: z.boolean(),
89
+ }), [t]);
90
+ const form = useForm({
91
+ defaultValues: defaultsFrom(user),
92
+ validationLogic: revalidateLogic({
93
+ mode: 'blur',
94
+ modeAfterSubmission: 'change',
95
+ }),
96
+ validators: {
97
+ onDynamic: updateUserSchema,
98
+ },
99
+ onSubmit: async ({ value }) => {
100
+ setFormError(null);
101
+ setSuccessMessage(null);
102
+ const patch = buildPatch(value, user);
103
+ if (Object.keys(patch).length === 0) {
104
+ setSuccessMessage(t('common.feedback.noChanges'));
105
+ return;
106
+ }
107
+ try {
108
+ const updated = await updateAdminUser({
109
+ data: { id: user.id, vid: user.vid, patch },
110
+ });
111
+ setSuccessMessage(t('common.feedback.saved'));
112
+ onSuccess?.(updated);
113
+ }
114
+ catch (err) {
115
+ const code = getErrorCode(err);
116
+ if (code === 'admin.users.emailInUse') {
117
+ const message = t('account.update.errors.emailInUse');
118
+ form.setFieldMeta('email', (meta) => ({
119
+ ...meta,
120
+ errorMap: { ...meta.errorMap, onServer: message },
121
+ errors: [message],
122
+ }));
123
+ return;
124
+ }
125
+ if (code === 'admin.users.versionConflict') {
126
+ setFormError(t('adminUsers.update.errors.versionConflict'));
127
+ return;
128
+ }
129
+ if (code === 'admin.users.notFound') {
130
+ setFormError(t('adminUsers.update.errors.notFound'));
131
+ return;
132
+ }
133
+ setFormError(t('common.errors.couldNotSave'));
134
+ }
135
+ },
136
+ });
137
+ return (<div className={cx('byline-user-update-wrap', styles.wrap)}>
138
+ <form method="post" noValidate onSubmit={(event) => {
139
+ event.preventDefault();
140
+ event.stopPropagation();
141
+ void form.handleSubmit();
142
+ }} className={cx('byline-user-update-form', styles.form)}>
143
+ {formError ? <Alert intent="danger">{formError}</Alert> : null}
144
+ {successMessage ? <Alert intent="success">{successMessage}</Alert> : null}
145
+
146
+ <form.Field name="given_name">
147
+ {(field) => (<Input label={t('account.update.fields.givenName')} id="given_name" name={field.name} value={field.state.value} onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.currentTarget.value)} error={field.state.meta.errors.length > 0} errorText={firstError(field.state.meta.errors)} autoComplete="given-name"/>)}
148
+ </form.Field>
149
+
150
+ <form.Field name="family_name">
151
+ {(field) => (<Input label={t('account.update.fields.familyName')} id="family_name" name={field.name} value={field.state.value} onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.currentTarget.value)} error={field.state.meta.errors.length > 0} errorText={firstError(field.state.meta.errors)} autoComplete="family-name"/>)}
152
+ </form.Field>
153
+
154
+ <form.Field name="username">
155
+ {(field) => (<Input label={t('account.update.fields.username')} id="username" name={field.name} value={field.state.value} onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.currentTarget.value)} error={field.state.meta.errors.length > 0} errorText={firstError(field.state.meta.errors)} helpText={t('account.update.fields.usernameHelp')} autoComplete="username"/>)}
156
+ </form.Field>
157
+
158
+ <form.Field name="email">
159
+ {(field) => (<Input label={t('common.fields.email')} id="email" name={field.name} type="email" value={field.state.value} onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.currentTarget.value)} error={field.state.meta.errors.length > 0} errorText={firstError(field.state.meta.errors)} autoComplete="email" required/>)}
160
+ </form.Field>
161
+
162
+ <div className={cx('byline-user-update-flags', styles.flags)}>
163
+ <form.Field name="is_enabled">
164
+ {(field) => (<Checkbox id="is_enabled" name={field.name} label={t('adminUsers.create.flags.enabledLabel')} checked={field.state.value} onCheckedChange={(checked) => field.handleChange(checked === true)} helpText={t('adminUsers.create.flags.enabledHelp')}/>)}
165
+ </form.Field>
166
+
167
+ <form.Field name="is_email_verified">
168
+ {(field) => (<Checkbox id="is_email_verified" name={field.name} label={t('adminUsers.create.flags.emailVerifiedLabel')} checked={field.state.value} onCheckedChange={(checked) => field.handleChange(checked === true)}/>)}
169
+ </form.Field>
170
+
171
+ <form.Field name="is_super_admin">
172
+ {(field) => (<Checkbox id="is_super_admin" name={field.name} label={t('adminUsers.create.flags.superAdminLabel')} checked={field.state.value} onCheckedChange={(checked) => field.handleChange(checked === true)} helpText={t('adminUsers.create.flags.superAdminHelp')}/>)}
173
+ </form.Field>
174
+ </div>
175
+
176
+ <div className={cx('byline-user-update-actions', styles.actions)}>
177
+ <Button type="button" intent="secondary" size="sm" onClick={onClose} className={cx('byline-user-update-action', styles.action)}>
178
+ {successMessage ? t('common.actions.close') : t('common.actions.cancel')}
179
+ </Button>
180
+ <form.Subscribe selector={(state) => ({
181
+ canSubmit: state.canSubmit,
182
+ isSubmitting: state.isSubmitting,
183
+ isDirty: state.isDirty,
184
+ })}>
185
+ {({ canSubmit, isSubmitting }) => (<Button size="sm" intent="primary" type="submit" disabled={!canSubmit || isSubmitting} className={cx('byline-user-update-action', styles.action)}>
186
+ {isSubmitting === true ? <LoaderEllipsis size={42}/> : t('common.actions.save')}
187
+ </Button>)}
188
+ </form.Subscribe>
189
+ </div>
190
+ </form>
191
+ </div>);
192
+ }
193
+ function firstError(errors) {
194
+ for (const err of errors) {
195
+ if (typeof err === 'string')
196
+ return err;
197
+ if (err && typeof err === 'object' && 'message' in err) {
198
+ const msg = err.message;
199
+ if (typeof msg === 'string')
200
+ return msg;
201
+ }
202
+ }
203
+ return undefined;
204
+ }
205
+ /**
206
+ * Extract the admin-users error code from a thrown server-fn response.
207
+ * Typed errors (`AdminUsersError`, `AuthError`) survive the server-fn
208
+ * boundary with their `code` intact thanks to the `BylineCodedError`
209
+ * serialization adapter registered in `src/start.ts`.
210
+ */
211
+ function getErrorCode(err) {
212
+ return typeof err?.code === 'string'
213
+ ? err.code
214
+ : null;
215
+ }