@byline/admin 5.1.4 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -14
- package/dist/exports-parity.test.node.js +0 -37
- package/dist/fields/array/array-field.jsx +0 -209
- package/dist/fields/blocks/blocks-field.jsx +0 -248
- package/dist/fields/checkbox/checkbox-field.jsx +0 -24
- package/dist/fields/code/code-editor.jsx +0 -211
- package/dist/fields/code/code-field.jsx +0 -109
- package/dist/fields/column-formatter.jsx +0 -25
- package/dist/fields/date-time-formatter.jsx +0 -18
- package/dist/fields/datetime/datetime-field.jsx +0 -22
- package/dist/fields/draggable-context-menu.jsx +0 -50
- package/dist/fields/field-admin.test.node.js +0 -40
- package/dist/fields/field-renderer.jsx +0 -136
- package/dist/fields/field-services-context.jsx +0 -17
- package/dist/fields/file/file-field.jsx +0 -176
- package/dist/fields/file/file-upload-field.jsx +0 -89
- package/dist/fields/group/group-field.jsx +0 -46
- package/dist/fields/image/image-field.jsx +0 -183
- package/dist/fields/image/image-upload-field.jsx +0 -157
- package/dist/fields/local-date-time.jsx +0 -69
- package/dist/fields/locale-badge.jsx +0 -19
- package/dist/fields/numerical/numerical-field.jsx +0 -93
- package/dist/fields/relation/relation-column-formatter.jsx +0 -74
- package/dist/fields/relation/relation-display.jsx +0 -118
- package/dist/fields/relation/relation-field.jsx +0 -108
- package/dist/fields/relation/relation-many-field.jsx +0 -142
- package/dist/fields/relation/relation-picker.jsx +0 -240
- package/dist/fields/relation/relation-summary.jsx +0 -45
- package/dist/fields/select/select-field.jsx +0 -27
- package/dist/fields/sortable-item.jsx +0 -54
- package/dist/fields/text/text-field.jsx +0 -79
- package/dist/fields/text-area/text-area-field.jsx +0 -79
- package/dist/forms/available-locales-reconcile.test.node.js +0 -43
- package/dist/forms/available-locales-widget.jsx +0 -63
- package/dist/forms/document-actions.jsx +0 -486
- package/dist/forms/form-context.jsx +0 -598
- package/dist/forms/form-modals.jsx +0 -128
- package/dist/forms/form-renderer.jsx +0 -532
- package/dist/forms/form-status-display.jsx +0 -69
- package/dist/forms/navigation-guard.jsx +0 -68
- package/dist/forms/nested-path.test.node.js +0 -115
- package/dist/forms/path-widget.jsx +0 -125
- package/dist/forms/pending-uploads.test.node.js +0 -19
- package/dist/forms/repeating-items.test.node.js +0 -29
- package/dist/forms/scheduled-publication-control.jsx +0 -389
- package/dist/forms/scheduled-publication-state.test.node.js +0 -121
- package/dist/forms/scheduled-publication-time.test.node.js +0 -70
- package/dist/forms/status-transitions.test.node.js +0 -76
- package/dist/forms/tree-placement-widget.jsx +0 -160
- package/dist/forms/upload-executor.test.node.js +0 -368
- package/dist/forms/use-form-layout.test.node.js +0 -69
- package/dist/modules/admin-account/components/change-password.jsx +0 -169
- package/dist/modules/admin-account/components/container.jsx +0 -164
- package/dist/modules/admin-account/components/preferences.jsx +0 -148
- package/dist/modules/admin-account/components/theme-switch.jsx +0 -52
- package/dist/modules/admin-account/components/update.jsx +0 -186
- package/dist/modules/admin-permissions/components/inspector.jsx +0 -200
- package/dist/modules/admin-preferences/schemas.test.node.js +0 -48
- package/dist/modules/admin-roles/components/create.jsx +0 -155
- package/dist/modules/admin-roles/components/permissions.jsx +0 -239
- package/dist/modules/admin-roles/components/update.jsx +0 -148
- package/dist/modules/admin-users/components/create.jsx +0 -206
- package/dist/modules/admin-users/components/roles.jsx +0 -126
- package/dist/modules/admin-users/components/set-password.jsx +0 -133
- package/dist/modules/admin-users/components/update.jsx +0 -215
- package/dist/modules/analytics/components/dashboard.jsx +0 -208
- package/dist/modules/analytics/components/dashboard.test.node.js +0 -162
- package/dist/modules/analytics/components/timeseries.jsx +0 -193
- package/dist/modules/auth/components/sign-in-form.jsx +0 -89
- package/dist/modules/auth/safe-redirect.test.node.js +0 -40
- package/dist/modules/auth/sign-in-form-props.test.node.js +0 -7
- package/dist/presentation/group.jsx +0 -26
- package/dist/presentation/row.jsx +0 -23
- package/dist/presentation/tabs.jsx +0 -39
- package/dist/services/admin-services-context.jsx +0 -17
- package/dist/widgets/diff-viewer/diff-modal.jsx +0 -110
- package/dist/widgets/source-locale-badge/source-locale-badge.jsx +0 -28
- package/dist/widgets/status-badge/status-badge.jsx +0 -46
|
@@ -1,126 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,133 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,215 +0,0 @@
|
|
|
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
|
-
}
|