@gopherium/react-auth 0.14.0 → 0.16.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 (53) hide show
  1. package/README.md +65 -7
  2. package/dist/account.d.ts +3 -0
  3. package/dist/account.js +3 -0
  4. package/dist/admin/errors.d.ts +62 -0
  5. package/dist/admin/errors.js +63 -0
  6. package/dist/admin/fieldError.d.ts +19 -0
  7. package/dist/admin/fieldError.js +46 -0
  8. package/dist/admin/index.d.ts +10 -37
  9. package/dist/admin/index.js +88 -94
  10. package/dist/languages/es-ES.json +1 -1
  11. package/dist/languages/fr-FR.json +1 -0
  12. package/dist/languages/languages/es-ES.json +1 -1
  13. package/dist/languages/languages/fr-FR.json +1 -0
  14. package/dist/languages.js +1 -0
  15. package/dist/testing/index.d.ts +9 -0
  16. package/dist/testing/index.js +21 -0
  17. package/dist/testing/setup.js +2 -0
  18. package/dist/testing/users.d.ts +91 -0
  19. package/dist/testing/users.js +84 -0
  20. package/dist/transport.d.ts +1 -0
  21. package/dist/wpds/PasswordField.d.ts +21 -0
  22. package/dist/wpds/PasswordField.js +130 -0
  23. package/dist/wpds/SetupAccountStep.js +17 -177
  24. package/dist/wpds/index.d.ts +4 -3
  25. package/dist/wpds/index.js +3 -3
  26. package/dist/wpds/nav.d.ts +2 -5
  27. package/dist/wpds/nav.js +4 -8
  28. package/dist/wpds/style.css +11 -22
  29. package/dist/wpds/usePasswordField.d.ts +37 -0
  30. package/dist/wpds/usePasswordField.js +86 -0
  31. package/dist/wpds/users/ChangeRoleDialog.d.ts +35 -0
  32. package/dist/wpds/users/ChangeRoleDialog.js +90 -0
  33. package/dist/wpds/users/ResendModal.d.ts +13 -0
  34. package/dist/wpds/users/ResendModal.js +60 -0
  35. package/dist/wpds/users/RoleSelect.d.ts +20 -0
  36. package/dist/wpds/users/RoleSelect.js +18 -0
  37. package/dist/wpds/users/UsersPage.d.ts +34 -0
  38. package/dist/wpds/users/UsersPage.js +91 -0
  39. package/dist/wpds/users/actions.d.ts +31 -0
  40. package/dist/wpds/users/actions.js +79 -0
  41. package/dist/wpds/users/defaults.d.ts +14 -0
  42. package/dist/wpds/users/defaults.js +17 -0
  43. package/dist/wpds/users/fields.d.ts +40 -0
  44. package/dist/wpds/users/fields.js +172 -0
  45. package/dist/wpds/users/index.d.ts +9 -0
  46. package/dist/wpds/users/index.js +5 -0
  47. package/dist/wpds/users/words.d.ts +47 -0
  48. package/dist/wpds/users/words.js +151 -0
  49. package/package.json +38 -1
  50. package/dist/wpds/NewUserScreen.d.ts +0 -9
  51. package/dist/wpds/NewUserScreen.js +0 -70
  52. package/dist/wpds/UsersScreen.d.ts +0 -11
  53. package/dist/wpds/UsersScreen.js +0 -46
@@ -0,0 +1,151 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ import { formatNumber } from '@gopherium/gottext';
3
+ import { __, _n, _x, sprintf } from '@wordpress/i18n';
4
+ import { DOMAIN } from '../../domain.js';
5
+ /**
6
+ * Returns the label a role reads as, a role the app names no label for reading as its stored name with a capital.
7
+ * @param role - The role the server stores, empty for an account holding none.
8
+ * @param roleLabels - The labels the app gives its own roles.
9
+ * @returns The label, No role for an empty role the app names no label for.
10
+ */
11
+ export function roleLabelOf(role, roleLabels = {}) {
12
+ if (Object.hasOwn(roleLabels, role)) {
13
+ return roleLabels[role];
14
+ }
15
+ return role === '' ? _x('No role', 'role', DOMAIN) : role.charAt(0).toUpperCase() + role.slice(1);
16
+ }
17
+ /**
18
+ * Returns the words a Role cell reads for an account, None for one holding no role as WordPress writes it.
19
+ * @param role - The role the server stores, empty for an account holding none.
20
+ * @param roleLabels - The labels the app gives its own roles.
21
+ * @returns The words.
22
+ */
23
+ export function roleCellOf(role, roleLabels) {
24
+ return role === '' ? _x('None', 'no user roles', DOMAIN) : roleLabelOf(role, roleLabels);
25
+ }
26
+ /**
27
+ * Returns the words for each error code the account writes answer, WordPress's sentence wherever WordPress has one.
28
+ * @returns The words, keyed by code, read fresh so the loaded catalogue answers.
29
+ */
30
+ function codeMessages() {
31
+ const notGrantable = __('Sorry, you are not allowed to give users that role.', DOMAIN);
32
+ return {
33
+ user_not_editable: __('Sorry, you are not allowed to edit this user.', DOMAIN),
34
+ role_not_grantable: notGrantable,
35
+ role_unknown: notGrantable,
36
+ self_role_refused: __('You cannot change your own role.', DOMAIN),
37
+ self_disable_refused: __('You cannot disable your own account.', DOMAIN),
38
+ last_privileged_refused: __('Somebody must be able to manage users.', DOMAIN),
39
+ role_changed: __('The role of that account just changed. Reload the page and try again.', DOMAIN),
40
+ user_not_found: __('Invalid user ID.', DOMAIN),
41
+ role_insufficient: __('Sorry, you are not allowed to edit users.', DOMAIN),
42
+ };
43
+ }
44
+ /**
45
+ * Returns the code an error carries, as every typed error of the admin calls does.
46
+ * @param error - What a write failed with.
47
+ * @returns The code, none for an error carrying no code.
48
+ */
49
+ function codeOf(error) {
50
+ const code = error?.code;
51
+ return typeof code === 'string' ? code : undefined;
52
+ }
53
+ /**
54
+ * Returns the words a failed account write reads as: the words for its code, else the app's, else the fallback.
55
+ * @param error - What the write failed with.
56
+ * @param errorMessages - The words the app gives the codes only it answers.
57
+ * @param fallback - The words for an error with no code either side words.
58
+ * @returns The words.
59
+ */
60
+ export function errorMessageOf(error, errorMessages, fallback) {
61
+ const code = codeOf(error);
62
+ if (code === undefined) {
63
+ return fallback;
64
+ }
65
+ const known = codeMessages();
66
+ if (Object.hasOwn(known, code)) {
67
+ return known[code];
68
+ }
69
+ return Object.hasOwn(errorMessages, code) ? errorMessages[code] : fallback;
70
+ }
71
+ /**
72
+ * Returns the toast confirming the accounts one Enable or Disable changed.
73
+ * @param disabled - Whether the accounts were disabled rather than enabled.
74
+ * @param done - How many accounts changed.
75
+ * @param only - The one account asked, when only one was.
76
+ * @returns The confirmation, naming the account whole when only one was asked.
77
+ */
78
+ function switchedMessage(disabled, done, only) {
79
+ if (only !== undefined) {
80
+ const template = disabled ? __('"%(name)s" disabled.', DOMAIN) : __('"%(name)s" enabled.', DOMAIN);
81
+ return sprintf(template, { name: only.name });
82
+ }
83
+ const template = disabled
84
+ ? _n('%s user disabled.', '%s users disabled.', done, DOMAIN)
85
+ : _n('%s user enabled.', '%s users enabled.', done, DOMAIN);
86
+ return sprintf(template, formatNumber(done));
87
+ }
88
+ /**
89
+ * Returns the toast naming the accounts one Enable or Disable could not change.
90
+ * @param disabled - Whether the accounts were to be disabled rather than enabled.
91
+ * @param failures - Each unchanged account with the error it failed with.
92
+ * @param asked - How many accounts the action was asked to change.
93
+ * @param errorMessages - The words the app gives the codes only it answers.
94
+ * @returns The reason for one account, the count for several.
95
+ */
96
+ function unswitchedMessage(disabled, failures, asked, errorMessages) {
97
+ if (asked === 1) {
98
+ const fallback = disabled
99
+ ? __('The user could not be disabled.', DOMAIN)
100
+ : __('The user could not be enabled.', DOMAIN);
101
+ return errorMessageOf(failures[0].error, errorMessages, fallback);
102
+ }
103
+ const template = disabled
104
+ ? _n('%s user could not be disabled.', '%s users could not be disabled.', failures.length, DOMAIN)
105
+ : _n('%s user could not be enabled.', '%s users could not be enabled.', failures.length, DOMAIN);
106
+ return sprintf(template, formatNumber(failures.length));
107
+ }
108
+ /**
109
+ * Returns the line Change role opens with, naming the one account or counting several.
110
+ * @param accounts - The accounts the dialog acts on.
111
+ * @returns The line.
112
+ */
113
+ export function roleLineOf(accounts) {
114
+ if (accounts.length === 1) {
115
+ return sprintf(__('Choose the role of %(name)s.', DOMAIN), { name: accounts[0].name });
116
+ }
117
+ const template = _n('Choose the role of %s user.', 'Choose the role of %s users.', accounts.length, DOMAIN);
118
+ return sprintf(template, formatNumber(accounts.length));
119
+ }
120
+ /**
121
+ * Returns the words a Change role run reports its outcome in, WordPress's Changed roles. for several.
122
+ * @param errorMessages - The words the app gives the codes only it answers.
123
+ * @returns The words for the accounts changed, naming one account whole, and for the ones that failed.
124
+ */
125
+ export function roleWords(errorMessages) {
126
+ return {
127
+ done: (_done, only) => only === undefined
128
+ ? __('Changed roles.', DOMAIN)
129
+ : sprintf(__('Role changed for "%(name)s".', DOMAIN), { name: only.name }),
130
+ failed: (failures, asked) => {
131
+ if (asked === 1) {
132
+ return errorMessageOf(failures[0].error, errorMessages, __('The role could not be changed.', DOMAIN));
133
+ }
134
+ const count = failures.length;
135
+ const template = _n('The role of %s user could not be changed.', 'The roles of %s users could not be changed.', count, DOMAIN);
136
+ return sprintf(template, formatNumber(count));
137
+ },
138
+ };
139
+ }
140
+ /**
141
+ * Returns the words an Enable or a Disable run reports its outcome in.
142
+ * @param disabled - Whether the run disables the accounts rather than enables them.
143
+ * @param errorMessages - The words the app gives the codes only it answers.
144
+ * @returns The words for the accounts changed and for the ones that failed.
145
+ */
146
+ export function switchWords(disabled, errorMessages) {
147
+ return {
148
+ done: (done, only) => switchedMessage(disabled, done, only),
149
+ failed: (failures, asked) => unswitchedMessage(disabled, failures, asked, errorMessages),
150
+ };
151
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gopherium/react-auth",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "Session authentication for React apps backed by gouncer's authkit.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -25,6 +25,10 @@
25
25
  "types": "./dist/wpds/index.d.ts",
26
26
  "default": "./dist/wpds/index.js"
27
27
  },
28
+ "./wpds/users": {
29
+ "types": "./dist/wpds/users/index.d.ts",
30
+ "default": "./dist/wpds/users/index.js"
31
+ },
28
32
  "./wpds/style.css": "./dist/wpds/style.css",
29
33
  "./languages/*": "./dist/languages/*",
30
34
  "./testing": {
@@ -48,10 +52,16 @@
48
52
  "zxcvbn": "4.4.1"
49
53
  },
50
54
  "peerDependencies": {
55
+ "@gopherium/godmin": ">=0.28.0 <0.29.0",
56
+ "@gopherium/gottext": ">=0.6.0 <0.8.0",
51
57
  "@tanstack/react-query": "^5.101.2",
52
58
  "@testing-library/jest-dom": "^6.9.1 || ^7.0.0",
53
59
  "@testing-library/react": "^16.3.2",
60
+ "@wordpress/components": ">=42.0.0 <43.0.0",
61
+ "@wordpress/dataviews": ">=20.0.0 <21.0.0",
62
+ "@wordpress/date": ">=5.57.0 <6.0.0",
54
63
  "@wordpress/i18n": ">=6.30.0 <7.0.0",
64
+ "@wordpress/icons": ">=18.0.0 <19.0.0",
55
65
  "@wordpress/theme": ">=2.3.0 <3.0.0",
56
66
  "@wordpress/ui": ">=0.24.0 <0.25.0",
57
67
  "msw": "^2.15.0",
@@ -59,12 +69,30 @@
59
69
  "vitest": "^4.1.10 || ^5.0.0"
60
70
  },
61
71
  "peerDependenciesMeta": {
72
+ "@gopherium/godmin": {
73
+ "optional": true
74
+ },
75
+ "@gopherium/gottext": {
76
+ "optional": true
77
+ },
62
78
  "@testing-library/jest-dom": {
63
79
  "optional": true
64
80
  },
65
81
  "@testing-library/react": {
66
82
  "optional": true
67
83
  },
84
+ "@wordpress/components": {
85
+ "optional": true
86
+ },
87
+ "@wordpress/dataviews": {
88
+ "optional": true
89
+ },
90
+ "@wordpress/date": {
91
+ "optional": true
92
+ },
93
+ "@wordpress/icons": {
94
+ "optional": true
95
+ },
68
96
  "@wordpress/theme": {
69
97
  "optional": true
70
98
  },
@@ -79,7 +107,10 @@
79
107
  }
80
108
  },
81
109
  "devDependencies": {
110
+ "@gopherium/godmin": "0.28.0",
111
+ "@gopherium/gottext": "0.7.0",
82
112
  "@tanstack/react-query": "^5.101.2",
113
+ "@tanstack/react-router": "^1.170.44",
83
114
  "@testing-library/jest-dom": "^7.0.0",
84
115
  "@testing-library/react": "^16.3.2",
85
116
  "@testing-library/user-event": "^14.6.1",
@@ -88,7 +119,13 @@
88
119
  "@types/zxcvbn": "4.4.5",
89
120
  "@vitejs/plugin-react": "^6.0.3",
90
121
  "@vitest/coverage-v8": "^5.0.3",
122
+ "@wordpress/a11y": "4.57.0",
123
+ "@wordpress/components": "42.0.0",
124
+ "@wordpress/dataviews": "20.0.0",
125
+ "@wordpress/date": "5.57.0",
91
126
  "@wordpress/i18n": "6.30.0",
127
+ "@wordpress/icons": "18.0.0",
128
+ "@wordpress/style-runtime": "0.13.0",
92
129
  "@wordpress/theme": "2.3.0",
93
130
  "@wordpress/ui": "0.24.0",
94
131
  "eslint": "^9.39.1",
@@ -1,9 +0,0 @@
1
- /**
2
- * Renders the new user form, sending an invitation and reporting success
3
- * upward, or showing the activation link when no mail server delivered it.
4
- * @param onCreated - Called after the invitation is handled.
5
- * @returns The new user screen element.
6
- */
7
- export declare function NewUserScreen({ onCreated, }: {
8
- onCreated?: () => void | Promise<void>;
9
- }): import("react").JSX.Element;
@@ -1,70 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // SPDX-License-Identifier: Apache-2.0
3
- import { useMutation, useQueryClient } from '@tanstack/react-query';
4
- import { __, _x } from '@wordpress/i18n';
5
- import { Button, InputControl, Stack, Text } from '@wordpress/ui';
6
- import { useState } from 'react';
7
- import { ValidationError, invite, usersQueryKey } from '../admin/index.js';
8
- import { DOMAIN } from '../domain.js';
9
- import { Outcome } from './Outcome.js';
10
- /**
11
- * Maps the screen's failures to the message shown beside the form,
12
- * surfacing the backend's explanation for rejected input.
13
- * @param invited - The error the invitation raised, if any.
14
- * @param handedOff - The error handing the invitation onward raised, if any.
15
- * @returns The message to display, or null when nothing failed.
16
- */
17
- function failureMessage(invited, handedOff) {
18
- if (invited instanceof ValidationError) {
19
- return invited.message;
20
- }
21
- if (invited) {
22
- return __('The invitation could not be sent.', DOMAIN);
23
- }
24
- if (handedOff) {
25
- return __('Something went wrong.', DOMAIN);
26
- }
27
- return null;
28
- }
29
- /**
30
- * Renders the new user form, sending an invitation and reporting success
31
- * upward, or showing the activation link when no mail server delivered it.
32
- * @param onCreated - Called after the invitation is handled.
33
- * @returns The new user screen element.
34
- */
35
- export function NewUserScreen({ onCreated, }) {
36
- const queryClient = useQueryClient();
37
- const [email, setEmail] = useState('');
38
- const [name, setName] = useState('');
39
- const [activationLink, setActivationLink] = useState(null);
40
- const finish = useMutation({
41
- mutationFn: async () => {
42
- await onCreated?.();
43
- },
44
- });
45
- const create = useMutation({
46
- mutationFn: () => invite({ email: email.trim(), name: name.trim() }),
47
- onMutate: () => {
48
- finish.reset();
49
- },
50
- onSuccess: async (invitation) => {
51
- await queryClient.invalidateQueries({ queryKey: usersQueryKey });
52
- if (invitation.delivered) {
53
- finish.mutate();
54
- return;
55
- }
56
- setActivationLink(invitation.activation_link);
57
- },
58
- });
59
- const failure = failureMessage(create.error, finish.error);
60
- if (activationLink !== null) {
61
- return (_jsxs(Stack, { direction: "column", gap: "lg", children: [_jsx(Text, { variant: "heading-lg", render: _jsx("h1", {}), children: _x('New user', 'page heading', DOMAIN) }), _jsx(Outcome, { children: _jsx(Text, { children: __('No mail server is configured. Deliver the activation link by hand.', DOMAIN) }) }), _jsx(InputControl, { label: _x('Activation link', 'field label', DOMAIN), readOnly: true, value: activationLink }), _jsx(Button, { type: "button", disabled: finish.isPending, loading: finish.isPending, onClick: () => finish.mutate(), children: __('Done', DOMAIN) }), failure ? _jsx(Text, { role: "alert", children: failure }) : null] }));
62
- }
63
- return (_jsxs(Stack, { direction: "column", gap: "lg", children: [_jsx(Text, { variant: "heading-lg", render: _jsx("h1", {}), children: _x('New user', 'page heading', DOMAIN) }), _jsx("form", { onSubmit: (event) => {
64
- event.preventDefault();
65
- create.mutate();
66
- }, children: _jsxs(Stack, { direction: "column", gap: "md", children: [_jsx(InputControl, { label: _x('Email', 'field label', DOMAIN), type: "email", autoComplete: "off", value: email, onChange: (event) => setEmail(event.target.value) }), _jsx(InputControl, { label: _x('Name', 'field label', DOMAIN), autoComplete: "off", value: name, onChange: (event) => setName(event.target.value) }), _jsx(Button, { type: "submit", disabled: email.trim() === '' ||
67
- name.trim() === '' ||
68
- create.isPending ||
69
- finish.isPending, loading: create.isPending || finish.isPending, children: __('Send invitation', DOMAIN) }), failure ? _jsx(Text, { role: "alert", children: failure }) : null] }) })] }));
70
- }
@@ -1,11 +0,0 @@
1
- import type { ReactElement } from 'react';
2
- /**
3
- * Renders the user administration screen: the account list with status and
4
- * disable controls, plus an optional control to create a new user.
5
- * @param newUserRender - The navigation element the New user button renders
6
- * as, typically the app router's link to its create form.
7
- * @returns The users screen, or a loading or error message.
8
- */
9
- export declare function UsersScreen({ newUserRender, }: {
10
- newUserRender?: ReactElement<Record<string, unknown>>;
11
- }): import("react").JSX.Element;
@@ -1,46 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // SPDX-License-Identifier: Apache-2.0
3
- import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
4
- import { __, _x, sprintf } from '@wordpress/i18n';
5
- import { Badge, Button, Stack, Text, VisuallyHidden } from '@wordpress/ui';
6
- import { fetchUsers, setUserDisabled, usersQueryKey } from '../admin/index.js';
7
- import { DOMAIN } from '../domain.js';
8
- import { useSession } from '../session.js';
9
- /**
10
- * Renders one user row with status and a disable toggle, hidden for the
11
- * signed-in account.
12
- * @param user - The user the row describes.
13
- * @param isSelf - Whether the row is the signed-in account.
14
- * @returns The table row element.
15
- */
16
- function UserRow({ user, isSelf }) {
17
- const queryClient = useQueryClient();
18
- const toggle = useMutation({
19
- mutationFn: () => setUserDisabled(user.id, !user.disabled),
20
- onSuccess: () => queryClient.invalidateQueries({ queryKey: usersQueryKey }),
21
- });
22
- return (_jsxs("tr", { children: [_jsx("td", { children: user.name }), _jsx("td", { children: user.email }), _jsx("td", { children: _jsx(Badge, { intent: user.disabled ? 'draft' : 'stable', children: user.disabled ? __('Disabled', DOMAIN) : __('Active', DOMAIN) }) }), _jsx("td", { children: isSelf ? null : (_jsxs(Stack, { direction: "column", gap: "xs", children: [_jsx(Button, { variant: "outline", "aria-label": user.disabled
23
- ? sprintf(__('Enable %s', DOMAIN), user.name)
24
- : sprintf(__('Disable %s', DOMAIN), user.name), disabled: toggle.isPending, onClick: () => toggle.mutate(), children: user.disabled ? __('Enable', DOMAIN) : __('Disable', DOMAIN) }), toggle.isError ? _jsx(Text, { role: "alert", children: __('Update failed.', DOMAIN) }) : null] })) })] }));
25
- }
26
- /**
27
- * Renders the user administration screen: the account list with status and
28
- * disable controls, plus an optional control to create a new user.
29
- * @param newUserRender - The navigation element the New user button renders
30
- * as, typically the app router's link to its create form.
31
- * @returns The users screen, or a loading or error message.
32
- */
33
- export function UsersScreen({ newUserRender, }) {
34
- const currentUserId = useSession().data?.id;
35
- const users = useQuery({
36
- queryKey: usersQueryKey,
37
- queryFn: ({ signal }) => fetchUsers(signal),
38
- });
39
- if (users.isPending) {
40
- return _jsx(Text, { role: "status", children: __('Loading users…', DOMAIN) });
41
- }
42
- if (users.isError) {
43
- return _jsx(Text, { role: "alert", children: __('Users could not be loaded.', DOMAIN) });
44
- }
45
- return (_jsxs(Stack, { direction: "column", gap: "lg", children: [_jsxs(Stack, { direction: "row", align: "center", gap: "md", children: [_jsx(Text, { variant: "heading-lg", render: _jsx("h1", {}), children: _x('Users', 'page heading', DOMAIN) }), newUserRender ? (_jsx(Button, { render: newUserRender, children: _x('New user', 'button', DOMAIN) })) : null] }), _jsxs("table", { className: "gopherium-users", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: _x('Name', 'column', DOMAIN) }), _jsx("th", { scope: "col", children: _x('Email', 'column', DOMAIN) }), _jsx("th", { scope: "col", children: _x('Status', 'column', DOMAIN) }), _jsx("th", { scope: "col", children: _jsx(VisuallyHidden, { children: __('Actions', DOMAIN) }) })] }) }), _jsx("tbody", { children: users.data.map((user) => (_jsx(UserRow, { user: user, isSelf: user.id === currentUserId }, user.id))) })] })] }));
46
- }