@gopherium/react-auth 0.15.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 (51) hide show
  1. package/README.md +64 -6
  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 +3 -2
  25. package/dist/wpds/index.js +2 -2
  26. package/dist/wpds/style.css +11 -22
  27. package/dist/wpds/usePasswordField.d.ts +37 -0
  28. package/dist/wpds/usePasswordField.js +86 -0
  29. package/dist/wpds/users/ChangeRoleDialog.d.ts +35 -0
  30. package/dist/wpds/users/ChangeRoleDialog.js +90 -0
  31. package/dist/wpds/users/ResendModal.d.ts +13 -0
  32. package/dist/wpds/users/ResendModal.js +60 -0
  33. package/dist/wpds/users/RoleSelect.d.ts +20 -0
  34. package/dist/wpds/users/RoleSelect.js +18 -0
  35. package/dist/wpds/users/UsersPage.d.ts +34 -0
  36. package/dist/wpds/users/UsersPage.js +91 -0
  37. package/dist/wpds/users/actions.d.ts +31 -0
  38. package/dist/wpds/users/actions.js +79 -0
  39. package/dist/wpds/users/defaults.d.ts +14 -0
  40. package/dist/wpds/users/defaults.js +17 -0
  41. package/dist/wpds/users/fields.d.ts +40 -0
  42. package/dist/wpds/users/fields.js +172 -0
  43. package/dist/wpds/users/index.d.ts +9 -0
  44. package/dist/wpds/users/index.js +5 -0
  45. package/dist/wpds/users/words.d.ts +47 -0
  46. package/dist/wpds/users/words.js +151 -0
  47. package/package.json +33 -1
  48. package/dist/wpds/NewUserScreen.d.ts +0 -9
  49. package/dist/wpds/NewUserScreen.js +0 -70
  50. package/dist/wpds/UsersScreen.d.ts +0 -11
  51. package/dist/wpds/UsersScreen.js +0 -46
@@ -1,21 +1,18 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
  import { useMutation } from '@tanstack/react-query';
4
- import { __, _n, _x, sprintf } from '@wordpress/i18n';
5
- import { Button, CheckboxControl, Field, Input, Text } from '@wordpress/ui';
6
- import { useEffect, useEffectEvent, useId, useRef, useState } from 'react';
4
+ import { __, _x } from '@wordpress/i18n';
5
+ import { Button, Field, Input, Text } from '@wordpress/ui';
6
+ import { useState } from 'react';
7
+ import { passwordHint } from '../admin/fieldError.js';
7
8
  import { ValidationError } from '../api.js';
8
9
  import { DOMAIN } from '../domain.js';
9
10
  import { SetupClosedError, SetupCodeError, SiteStartingError, createFirstAccount, isSetupOpen } from '../setup.js';
10
- import { generatePassword } from './password.js';
11
+ import { PasswordField } from './PasswordField.js';
11
12
  import { setupRefusal } from './setupWords.js';
12
- import { disallowedWords, levelOf, loadMeter } from './strength.js';
13
+ import { usePasswordField } from './usePasswordField.js';
13
14
  /** The length of the password the step starts with, unless the wizard asks for another. */
14
15
  const GENERATED_PASSWORD_LENGTH = 18;
15
- /** The word the meter box holds for an empty password. */
16
- const NO_WORD = '\u00a0';
17
- /** The arrow drawn before the Caps Lock words. */
18
- const CAPS_ARROW = (_jsx("span", { "aria-hidden": "true", className: "gopherium-setup__caps-icon", children: _jsxs("svg", { viewBox: "0 0 24 26", fill: "currentColor", stroke: "currentColor", strokeWidth: "0.5", children: [_jsx("path", { d: "M12 5L19 15H16V19H8V15H5L12 5Z" }), _jsx("rect", { x: "8", y: "21", width: "8", height: "1.5", rx: "0.75" })] }) }));
19
16
  /**
20
17
  * Renders a sentence with the part between its code tags as code.
21
18
  * @param sentence - The translated sentence.
@@ -24,17 +21,6 @@ const CAPS_ARROW = (_jsx("span", { "aria-hidden": "true", className: "gopherium-
24
21
  function withCode(sentence) {
25
22
  return sentence.split(/<\/?code>/).map((part, index) => (index % 2 === 1 ? _jsx("code", { children: part }, index) : part));
26
23
  }
27
- /**
28
- * Returns the password hint with the server's floor as its number.
29
- * @param min - The fewest characters the server accepts.
30
- * @returns The hint.
31
- */
32
- function passwordHint(min) {
33
- const hint = _n('Hint: The password should be at least %d character long. ' +
34
- 'To make it stronger, use upper and lower case letters, numbers, and symbols like ! " ? $ %% ^ & ).', 'Hint: The password should be at least %d characters long. ' +
35
- 'To make it stronger, use upper and lower case letters, numbers, and symbols like ! " ? $ %% ^ & ).', min, DOMAIN);
36
- return sprintf(hint, min);
37
- }
38
24
  const validationWords = new Map([
39
25
  [
40
26
  'email_invalid',
@@ -100,113 +86,15 @@ async function reportRefusal(error, hooks) {
100
86
  await probeClosed(hooks.onClosed);
101
87
  }
102
88
  }
103
- /**
104
- * Returns what the meter knows of a password at the moment it is scored.
105
- * @param password - The password as typed.
106
- * @param meter - The meter, once it has landed.
107
- * @param words - Reads the words the meter treats as easy to guess.
108
- * @returns The strength, -1 while the meter has not landed.
109
- */
110
- function strengthOf(password, meter, words) {
111
- if (password.trim() === '') {
112
- return 'empty';
113
- }
114
- return meter === undefined ? -1 : meter(password, words());
115
- }
116
- /**
117
- * Returns the meter's tone for a strength.
118
- * @param strength - What the meter knows of the password.
119
- * @returns The tone, or undefined while nothing was scored or the field is empty.
120
- */
121
- function toneOf(strength) {
122
- return typeof strength === 'number' ? levelOf(strength).tone : undefined;
123
- }
124
- /**
125
- * Reports whether a password this strong needs its use confirmed.
126
- * @param strength - What the meter knows of the password.
127
- * @returns True for the very weak, weak and unknown levels.
128
- */
129
- function needsConfirmation(strength) {
130
- const tone = toneOf(strength);
131
- return tone === 'short' || tone === 'bad';
132
- }
133
- /**
134
- * Returns the word the meter box holds.
135
- * @param strength - What the meter knows of the password.
136
- * @returns Nothing before scoring, a no-break space for an empty field, or the level's word.
137
- */
138
- function meterWord(strength) {
139
- if (strength === undefined) {
140
- return '';
141
- }
142
- return strength === 'empty' ? NO_WORD : levelOf(strength).word;
143
- }
144
- /**
145
- * Returns a class with its tone modifier when it has a tone.
146
- * @param block - The class without a tone.
147
- * @param tone - The tone to add, if any.
148
- * @returns The class names.
149
- */
150
- function withTone(block, tone) {
151
- return tone === undefined ? block : `${block} ${block}--${tone}`;
152
- }
153
- /**
154
- * Reports whether a key types a character, the only keys Caps Lock is read from.
155
- * @param event - The key the person pressed.
156
- * @returns True for a printable key pressed without Control, Alt or Meta.
157
- */
158
- function isPrintable(event) {
159
- return event.key.length === 1 && !event.ctrlKey && !event.altKey && !event.metaKey;
160
- }
161
- /**
162
- * Returns the words the meter treats as easy to guess, read from the fields when it scores.
163
- * @param name - The name as typed.
164
- * @param email - The email as typed.
165
- * @param userInputs - The wizard's own values, if it gave any.
166
- * @returns The distinct words of four characters or more.
167
- */
168
- function wordsFor(name, email, userInputs = []) {
169
- return disallowedWords([document.title, location.href, name, email, ...userInputs]);
170
- }
171
89
  /**
172
90
  * Reports whether the step may send the account, the meter's confirmation included.
173
91
  * @param name - The name as typed.
174
92
  * @param email - The email as typed.
175
- * @param held - The password and what the meter knows of it.
93
+ * @param passwordReady - Whether the password may be sent.
176
94
  * @returns True once every field holds more than spaces and a weak password is confirmed.
177
95
  */
178
- function isReady(name, email, held) {
179
- return (name.trim() !== '' &&
180
- email.trim() !== '' &&
181
- held.password.trim() !== '' &&
182
- (held.confirmed || !needsConfirmation(held.strength)));
183
- }
184
- /**
185
- * Holds the password, scores it when the meter lands and on each change, and keeps the weak use confirmation.
186
- * @param length - The length of the password the step starts with, if the wizard chose one.
187
- * @param words - Reads the words the meter treats as easy to guess.
188
- * @returns The password, its strength, the confirmation and their setters.
189
- */
190
- function usePassword(length, words) {
191
- const [password, setPassword] = useState(() => generatePassword(length ?? GENERATED_PASSWORD_LENGTH));
192
- const [strength, setStrength] = useState(undefined);
193
- const [confirmed, setConfirmed] = useState(false);
194
- const meter = useRef(undefined);
195
- const landed = useEffectEvent((loaded) => {
196
- meter.current = loaded;
197
- setStrength(strengthOf(password, loaded, words));
198
- });
199
- useEffect(() => {
200
- loadMeter().then((loaded) => landed(loaded), () => undefined);
201
- }, []);
202
- const change = (value) => {
203
- setPassword(value);
204
- setStrength(strengthOf(value, meter.current, words));
205
- if (value.trim() === '') {
206
- setConfirmed(false);
207
- }
208
- };
209
- return { password, strength, confirmed, change, setConfirmed };
96
+ function isReady(name, email, passwordReady) {
97
+ return name.trim() !== '' && email.trim() !== '' && passwordReady;
210
98
  }
211
99
  /**
212
100
  * Sends the first account once and hands it to the wizard, retrying only the hand-over.
@@ -272,56 +160,6 @@ function RefusalMessage({ error, onError }) {
272
160
  function TextRow({ label, help, ...field }) {
273
161
  return (_jsxs(Field.Root, { className: "gopherium-setup__row", render: _jsx("div", {}), children: [_jsx(Field.Label, { className: "gopherium-setup__label", children: label }), _jsxs("div", { children: [_jsx(Input, { ...field }), help === undefined ? null : _jsx(Field.Description, { className: "gopherium-setup__help", children: help })] })] }));
274
162
  }
275
- /**
276
- * Renders the button that shows or hides the password.
277
- * @param masked - Whether the password is hidden.
278
- * @param onToggle - Called when the button is pressed.
279
- * @returns The toggle element.
280
- */
281
- function MaskToggle({ masked, onToggle }) {
282
- const show = __('Show', DOMAIN);
283
- const hide = __('Hide', DOMAIN);
284
- return (_jsx(Button, { type: "button", variant: "outline", className: "gopherium-setup__toggle", "data-other-word": masked ? hide : show, "aria-label": masked ? __('Show password', DOMAIN) : __('Hide password', DOMAIN), onClick: onToggle, children: masked ? show : hide }));
285
- }
286
- /**
287
- * Renders the box that shows the meter's word in its tone.
288
- * @param id - The id the password field is described by.
289
- * @param strength - What the meter knows of the password.
290
- * @returns The meter box element.
291
- */
292
- function MeterBox({ id, strength }) {
293
- return (_jsx("div", { id: id, "aria-live": "polite", className: withTone('gopherium-setup__meter', toneOf(strength)), children: meterWord(strength) }));
294
- }
295
- /**
296
- * Renders the password row with the meter box, the Caps Lock line and the line asking to store it.
297
- * @param password - The password as typed.
298
- * @param strength - What the meter knows of the password.
299
- * @param masked - Whether the password is hidden.
300
- * @param readOnly - Whether the password stays as sent while the account is created.
301
- * @param onPasswordChange - Called with the password on every change.
302
- * @param onToggle - Called when the show or hide button is pressed.
303
- * @returns The row element.
304
- */
305
- function PasswordRow({ password, strength, masked, readOnly, onPasswordChange, onToggle, }) {
306
- const [capsLock, setCapsLock] = useState(false);
307
- const meterId = useId();
308
- const helpId = useId();
309
- return (_jsxs(Field.Root, { className: "gopherium-setup__row", render: _jsx("div", {}), children: [_jsx(Field.Label, { className: "gopherium-setup__label", children: __('Password', DOMAIN) }), _jsxs("div", { children: [_jsxs("div", { className: "gopherium-setup__password", children: [_jsxs("div", { className: "gopherium-setup__password-field", children: [_jsx(Input, { type: masked ? 'password' : 'text', className: withTone('gopherium-setup__input', toneOf(strength)), value: password, readOnly: readOnly, onChange: (event) => onPasswordChange(event.target.value), onKeyDown: (event) => {
310
- if (isPrintable(event)) {
311
- setCapsLock(event.getModifierState('CapsLock'));
312
- }
313
- }, autoComplete: "new-password", spellCheck: false, "aria-describedby": `${meterId} ${helpId}` }), _jsx(MeterBox, { id: meterId, strength: strength }), _jsx("div", { "aria-live": "assertive", className: "gopherium-setup__caps", children: capsLock ? (_jsxs(_Fragment, { children: [CAPS_ARROW, _jsx("span", { className: "gopherium-setup__caps-text", children: __('Caps lock is on.', DOMAIN) })] })) : null })] }), _jsx(MaskToggle, { masked: masked, onToggle: onToggle })] }), _jsxs(Text, { render: _jsx("p", { id: helpId, className: "gopherium-setup__help" }), children: [_jsx("strong", { children: __('Important:', DOMAIN) }), ' ', __('You will need this password to log\u00a0in. Please store it in a secure location.', DOMAIN)] })] })] }));
314
- }
315
- /**
316
- * Renders the row asking to confirm the use of a weak password.
317
- * @param checked - Whether the use is confirmed.
318
- * @param readOnly - Whether the choice stays as sent while the account is created.
319
- * @param onCheckedChange - Called with the new choice.
320
- * @returns The row element.
321
- */
322
- function ConfirmRow({ checked, readOnly, onCheckedChange, }) {
323
- return (_jsxs("div", { className: "gopherium-setup__row", children: [_jsx(Field.VisualLabel, { className: "gopherium-setup__label", children: __('Confirm Password', DOMAIN) }), _jsx("div", { children: _jsx(CheckboxControl, { className: "gopherium-setup__checkbox", label: __('Confirm use of weak password', DOMAIN), checked: checked, readOnly: readOnly, onCheckedChange: (next) => onCheckedChange(next) }) })] }));
324
- }
325
163
  /**
326
164
  * Renders the sentence and the button that hand the created account to the wizard again.
327
165
  * @param pending - Whether the hand-over runs.
@@ -349,19 +187,21 @@ function HandoffFailure({ pending, onRetry }) {
349
187
  export function SetupAccountStep({ code, submitLabel, onCreated, onSubmit, onError, onCodeRefused, onClosed, generatedPasswordLength, userInputs, before, after, }) {
350
188
  const [name, setName] = useState('');
351
189
  const [email, setEmail] = useState('');
352
- const [masked, setMasked] = useState(false);
353
- const held = usePassword(generatedPasswordLength, () => wordsFor(name, email, userInputs));
354
- const account = useFirstAccount(() => (onSubmit ?? createFirstAccount)({ code, email: email.trim(), name: name.trim(), password: held.password }), onCreated, () => setMasked(false), { onCodeRefused, onClosed });
190
+ const password = usePasswordField({
191
+ length: generatedPasswordLength ?? GENERATED_PASSWORD_LENGTH,
192
+ userInputs: [name, email, ...(userInputs ?? [])],
193
+ });
194
+ const account = useFirstAccount(() => (onSubmit ?? createFirstAccount)({ code, email: email.trim(), name: name.trim(), password: password.value }), onCreated, () => password.mask(false), { onCodeRefused, onClosed });
355
195
  if (account.retry !== undefined) {
356
196
  return _jsx(HandoffFailure, { pending: account.retrying, onRetry: account.retry });
357
197
  }
358
- const canSend = !account.locked && isReady(name, email, held);
198
+ const canSend = !account.locked && isReady(name, email, password.ready);
359
199
  return (_jsxs("form", { className: "gopherium-setup", noValidate: true, onSubmit: (event) => {
360
200
  event.preventDefault();
361
201
  if (!canSend) {
362
202
  return;
363
203
  }
364
- setMasked(true);
204
+ password.mask(true);
365
205
  account.submit();
366
- }, children: [_jsx(RefusalMessage, { error: account.error, onError: onError }), before, _jsx(TextRow, { label: _x('Name', 'field label', DOMAIN), type: "text", autoComplete: "name", value: name, readOnly: account.locked, onChange: (event) => setName(event.target.value) }), _jsx(PasswordRow, { password: held.password, strength: held.strength, masked: masked, readOnly: account.locked, onPasswordChange: held.change, onToggle: () => setMasked(!masked) }), needsConfirmation(held.strength) ? (_jsx(ConfirmRow, { checked: held.confirmed, readOnly: account.locked, onCheckedChange: held.setConfirmed })) : null, _jsx(TextRow, { label: __('Your Email', DOMAIN), type: "email", autoComplete: "username", value: email, readOnly: account.locked, onChange: (event) => setEmail(event.target.value), help: __('Double-check your email address before continuing.', DOMAIN) }), after, _jsx("p", { className: "gopherium-setup__step", children: _jsx(Button, { type: "submit", disabled: !canSend, loading: account.busy, children: submitLabel }) })] }));
206
+ }, children: [_jsx(RefusalMessage, { error: account.error, onError: onError }), before, _jsx(TextRow, { label: _x('Name', 'field label', DOMAIN), type: "text", autoComplete: "name", value: name, readOnly: account.locked, onChange: (event) => setName(event.target.value) }), _jsx(PasswordField, { password: password, readOnly: account.locked, help: _jsxs(_Fragment, { children: [_jsx("strong", { children: __('Important:', DOMAIN) }), ' ', __('You will need this password to log\u00a0in. Please store it in a secure location.', DOMAIN)] }) }), _jsx(TextRow, { label: __('Your Email', DOMAIN), type: "email", autoComplete: "username", value: email, readOnly: account.locked, onChange: (event) => setEmail(event.target.value), help: __('Double-check your email address before continuing.', DOMAIN) }), after, _jsx("p", { className: "gopherium-setup__step", children: _jsx(Button, { type: "submit", disabled: !canSend, loading: account.busy, children: submitLabel }) })] }));
367
207
  }
@@ -1,10 +1,11 @@
1
1
  export { AccountPanel } from './AccountPanel.js';
2
2
  export { LoginScreen } from './LoginScreen.js';
3
3
  export { usersMenuItem } from './nav.js';
4
- export { NewUserScreen } from './NewUserScreen.js';
4
+ export { PasswordField } from './PasswordField.js';
5
5
  export { RequestResetScreen } from './RequestResetScreen.js';
6
6
  export { ResetPasswordScreen } from './ResetPasswordScreen.js';
7
7
  export { SetPasswordScreen } from './SetPasswordScreen.js';
8
8
  export { SetupAccountStep } from './SetupAccountStep.js';
9
9
  export { SetupCodeStep } from './SetupCodeStep.js';
10
- export { UsersScreen } from './UsersScreen.js';
10
+ export { usePasswordField } from './usePasswordField.js';
11
+ export type { PasswordFieldState } from './usePasswordField.js';
@@ -2,10 +2,10 @@
2
2
  export { AccountPanel } from './AccountPanel.js';
3
3
  export { LoginScreen } from './LoginScreen.js';
4
4
  export { usersMenuItem } from './nav.js';
5
- export { NewUserScreen } from './NewUserScreen.js';
5
+ export { PasswordField } from './PasswordField.js';
6
6
  export { RequestResetScreen } from './RequestResetScreen.js';
7
7
  export { ResetPasswordScreen } from './ResetPasswordScreen.js';
8
8
  export { SetPasswordScreen } from './SetPasswordScreen.js';
9
9
  export { SetupAccountStep } from './SetupAccountStep.js';
10
10
  export { SetupCodeStep } from './SetupCodeStep.js';
11
- export { UsersScreen } from './UsersScreen.js';
11
+ export { usePasswordField } from './usePasswordField.js';
@@ -101,11 +101,11 @@
101
101
  color: var(--wpds-color-foreground-content-neutral-weak);
102
102
  }
103
103
 
104
- .gopherium-setup__checkbox {
104
+ .gopherium-setup__row .gopherium-setup__checkbox {
105
105
  --_wp-ui-checkbox-control-label-line-height: max(var(--_wp-ui-checkbox-control-checkbox-height), 14px * 1.3);
106
106
  }
107
107
 
108
- .gopherium-setup__checkbox label {
108
+ .gopherium-setup__row .gopherium-setup__checkbox label {
109
109
  font-size: 14px;
110
110
  }
111
111
 
@@ -114,6 +114,10 @@
114
114
  height: calc(100% + 2 * var(--wpds-border-width-xs));
115
115
  }
116
116
 
117
+ .gopherium-setup__generate {
118
+ margin-block-end: 14px;
119
+ }
120
+
117
121
  .gopherium-setup__password {
118
122
  display: flex;
119
123
  column-gap: 4px;
@@ -147,7 +151,6 @@
147
151
  border: var(--wpds-border-width-xs) solid var(--wpds-color-stroke-surface-neutral);
148
152
  border-radius: var(--wpds-border-radius-sm);
149
153
  color: var(--wpds-color-foreground-content-neutral);
150
- font-size: 14px;
151
154
  font-weight: normal;
152
155
  line-height: 1.3;
153
156
  text-align: center;
@@ -211,10 +214,14 @@
211
214
  border: var(--wpds-border-width-xs) solid var(--wpds-color-stroke-surface-caution);
212
215
  background-color: var(--wpds-color-background-surface-caution-weak);
213
216
  color: var(--wpds-color-foreground-content-neutral);
214
- font-size: 14px;
215
217
  line-height: 1.3;
216
218
  }
217
219
 
220
+ .gopherium-setup__row .gopherium-setup__meter,
221
+ .gopherium-setup__row .gopherium-setup__caps {
222
+ font-size: 14px;
223
+ }
224
+
218
225
  .gopherium-setup__caps-icon {
219
226
  display: flex;
220
227
  flex: none;
@@ -249,21 +256,3 @@
249
256
  --_wp-ui-checkbox-control-checkbox-height: 25px;
250
257
  }
251
258
  }
252
-
253
- .gopherium-users {
254
- width: 100%;
255
- border-collapse: collapse;
256
- text-align: left;
257
- }
258
-
259
- .gopherium-users th,
260
- .gopherium-users td {
261
- padding: 8px 12px;
262
- border-bottom: 1px solid var(--wpds-color-stroke-surface-neutral);
263
- vertical-align: middle;
264
- }
265
-
266
- .gopherium-users th {
267
- font-size: var(--wpds-typography-font-size-sm);
268
- color: var(--wpds-color-foreground-content-neutral-weak);
269
- }
@@ -0,0 +1,37 @@
1
+ import type { Tone } from './strength.js';
2
+ /** What the meter knows of the password: nothing yet, an empty field, or a score from -1 to 4. */
3
+ export type Strength = number | 'empty' | undefined;
4
+ /** The password a PasswordField draws, what the meter knows of it, and the setters the field calls. */
5
+ export interface PasswordFieldState {
6
+ value: string;
7
+ strength: Strength;
8
+ confirmed: boolean;
9
+ masked: boolean;
10
+ ready: boolean;
11
+ change: (value: string) => void;
12
+ generate: () => void;
13
+ confirm: (confirmed: boolean) => void;
14
+ mask: (masked: boolean) => void;
15
+ }
16
+ /**
17
+ * Returns the meter's tone for a strength.
18
+ * @param strength - What the meter knows of the password.
19
+ * @returns The tone, or undefined while nothing was scored or the field is empty.
20
+ */
21
+ export declare function toneOf(strength: Strength): Tone | undefined;
22
+ /**
23
+ * Reports whether a password this strong needs its use confirmed.
24
+ * @param strength - What the meter knows of the password.
25
+ * @returns True for the very weak, weak and unknown levels.
26
+ */
27
+ export declare function needsConfirmation(strength: Strength): boolean;
28
+ /**
29
+ * Holds a password, scores it when the meter lands and on each change, and keeps its mask and weak use confirmation.
30
+ * @param length - The length of every password it generates, the one the field starts with included.
31
+ * @param userInputs - The values the meter treats as easy to guess, such as the name and email typed beside it.
32
+ * @returns The password, what the meter knows of it, and its setters.
33
+ */
34
+ export declare function usePasswordField({ length, userInputs, }: {
35
+ length: number;
36
+ userInputs: string[];
37
+ }): PasswordFieldState;
@@ -0,0 +1,86 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ import { useEffect, useEffectEvent, useRef, useState } from 'react';
3
+ import { generatePassword } from './password.js';
4
+ import { disallowedWords, levelOf, loadMeter } from './strength.js';
5
+ /**
6
+ * Returns what the meter knows of a password at the moment it is scored.
7
+ * @param password - The password as typed.
8
+ * @param meter - The meter, once it has landed.
9
+ * @param words - Reads the words the meter treats as easy to guess.
10
+ * @returns The strength, -1 while the meter has not landed.
11
+ */
12
+ function strengthOf(password, meter, words) {
13
+ if (password.trim() === '') {
14
+ return 'empty';
15
+ }
16
+ return meter === undefined ? -1 : meter(password, words());
17
+ }
18
+ /**
19
+ * Returns the meter's tone for a strength.
20
+ * @param strength - What the meter knows of the password.
21
+ * @returns The tone, or undefined while nothing was scored or the field is empty.
22
+ */
23
+ export function toneOf(strength) {
24
+ return typeof strength === 'number' ? levelOf(strength).tone : undefined;
25
+ }
26
+ /**
27
+ * Reports whether a password this strong needs its use confirmed.
28
+ * @param strength - What the meter knows of the password.
29
+ * @returns True for the very weak, weak and unknown levels.
30
+ */
31
+ export function needsConfirmation(strength) {
32
+ const tone = toneOf(strength);
33
+ return tone === 'short' || tone === 'bad';
34
+ }
35
+ /**
36
+ * Reports whether a password may be sent, the meter's confirmation included.
37
+ * @param value - The password as typed.
38
+ * @param strength - What the meter knows of it.
39
+ * @param confirmed - Whether its weak use is confirmed.
40
+ * @returns True once it holds more than spaces and a weak password is confirmed.
41
+ */
42
+ function isReady(value, strength, confirmed) {
43
+ return value.trim() !== '' && (confirmed || !needsConfirmation(strength));
44
+ }
45
+ /**
46
+ * Holds a password, scores it when the meter lands and on each change, and keeps its mask and weak use confirmation.
47
+ * @param length - The length of every password it generates, the one the field starts with included.
48
+ * @param userInputs - The values the meter treats as easy to guess, such as the name and email typed beside it.
49
+ * @returns The password, what the meter knows of it, and its setters.
50
+ */
51
+ export function usePasswordField({ length, userInputs, }) {
52
+ const [value, setValue] = useState(() => generatePassword(length));
53
+ const [strength, setStrength] = useState(undefined);
54
+ const [confirmed, setConfirmed] = useState(false);
55
+ const [masked, setMasked] = useState(false);
56
+ const meter = useRef(undefined);
57
+ const words = () => disallowedWords([document.title, location.href, ...userInputs]);
58
+ const landed = useEffectEvent((loaded) => {
59
+ meter.current = loaded;
60
+ setStrength(strengthOf(value, loaded, words));
61
+ });
62
+ useEffect(() => {
63
+ loadMeter().then((loaded) => landed(loaded), () => undefined);
64
+ }, []);
65
+ const change = (next) => {
66
+ setValue(next);
67
+ setStrength(strengthOf(next, meter.current, words));
68
+ if (next.trim() === '') {
69
+ setConfirmed(false);
70
+ }
71
+ };
72
+ return {
73
+ value,
74
+ strength,
75
+ confirmed,
76
+ masked,
77
+ ready: isReady(value, strength, confirmed),
78
+ change,
79
+ generate: () => {
80
+ change(generatePassword(length));
81
+ setMasked(false);
82
+ },
83
+ confirm: setConfirmed,
84
+ mask: setMasked,
85
+ };
86
+ }
@@ -0,0 +1,35 @@
1
+ import type { Trigger, UnsavedText } from '@gopherium/godmin';
2
+ import type { Account } from '../../account.js';
3
+ import type { ErrorMessages, RoleLabels } from './words.js';
4
+ /** What Change role needs from the page. */
5
+ export interface RoleTools {
6
+ /** The roles the reader may give, the one carrying most capabilities first. */
7
+ grantable: readonly string[];
8
+ /** The labels the app gives its own roles. */
9
+ roleLabels: RoleLabels;
10
+ /** The words the app gives the error codes only it answers. */
11
+ errorMessages: ErrorMessages;
12
+ /** The roles failed changes picked, by the accounts they were for, kept for the next open. */
13
+ unsaved: UnsavedText;
14
+ /** Reads the list again. */
15
+ reload: () => Promise<unknown>;
16
+ }
17
+ /** The Change role dialog the page holds open, on the accounts it acts on and the trigger it opened from. */
18
+ export interface OpenedRole {
19
+ /** The accounts the dialog acts on. */
20
+ accounts: Account[];
21
+ /** The trigger the dialog opened from, which takes the focus back once the dialog is done. */
22
+ trigger: Trigger;
23
+ }
24
+ /**
25
+ * Renders the Change role dialog the page holds open, none while it holds none.
26
+ * @param props - The dialog, what it needs, what takes it away, what clears the selection and the focus back.
27
+ * @returns The dialog, giving the focus back to its trigger once it closed and once its writes read the list again.
28
+ */
29
+ export declare function OpenRoleDialog({ opened, tools, onClose, onClearSelection, onBack, }: {
30
+ opened?: OpenedRole;
31
+ tools: RoleTools;
32
+ onClose: () => void;
33
+ onClearSelection: () => void;
34
+ onBack: (id: string, trigger: Trigger) => void;
35
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,90 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { bulkNotes, markLatest, runEach, useToaster } from '@gopherium/godmin';
4
+ import { FormDialog } from '@gopherium/godmin/dialogs';
5
+ import { __ } from '@wordpress/i18n';
6
+ import { Text } from '@wordpress/ui';
7
+ import { useState } from 'react';
8
+ import { setUserRole } from '../../admin/index.js';
9
+ import { DOMAIN } from '../../domain.js';
10
+ import { RoleSelect } from './RoleSelect.js';
11
+ import { roleLineOf, roleWords } from './words.js';
12
+ /**
13
+ * Returns the key the pick of a failed change is kept under, the ids of the accounts it was for.
14
+ * @param accounts - The accounts the dialog acts on.
15
+ * @returns The key.
16
+ */
17
+ function keyOf(accounts) {
18
+ return accounts.map((account) => account.id).join(' ');
19
+ }
20
+ /**
21
+ * Returns the role the dialog opens on: the one a failed change kept, else the one account's role the reader may give.
22
+ * @param accounts - The accounts the dialog acts on.
23
+ * @param tools - What the dialog needs from the page.
24
+ * @returns The role, empty for several accounts or for a role the reader may not give.
25
+ */
26
+ function startingRole(accounts, tools) {
27
+ const kept = tools.unsaved.get(keyOf(accounts));
28
+ if (kept !== undefined) {
29
+ return kept;
30
+ }
31
+ const [first] = accounts;
32
+ return accounts.length === 1 && tools.grantable.includes(first.role) ? first.role : '';
33
+ }
34
+ /**
35
+ * Writes the role to each account, reads the list again and shows one toast, keeping the pick when a write failed.
36
+ * @param accounts - The accounts the dialog acts on.
37
+ * @param role - The role picked.
38
+ * @param tools - What the dialog needs from the page.
39
+ * @param toaster - Shows the toast.
40
+ * @returns What settles once the toast shows.
41
+ */
42
+ async function changeRoles(accounts, role, tools, toaster) {
43
+ const key = keyOf(accounts);
44
+ tools.unsaved.delete(key);
45
+ const stillLatest = markLatest(tools.unsaved, key);
46
+ const outcome = await runEach(accounts, (account) => setUserRole(account.id, role));
47
+ await tools.reload();
48
+ const notes = bulkNotes(accounts, outcome, roleWords(tools.errorMessages));
49
+ toaster.show(notes.notice ?? notes.toast);
50
+ if (outcome.failures.length > 0 && stillLatest()) {
51
+ tools.unsaved.set(key, role);
52
+ }
53
+ }
54
+ /**
55
+ * Renders Change role, closed the moment it is sent as a WordPress row edit closes, on one account or several.
56
+ * @param props - The accounts, what the dialog needs from the page and what closes, sends and settles it.
57
+ * @returns The dialog.
58
+ */
59
+ function ChangeRoleDialog({ accounts, tools, onClose, onSent, onSettled }) {
60
+ const toaster = useToaster();
61
+ const [start] = useState(() => startingRole(accounts, tools));
62
+ const [chosen, setChosen] = useState(start);
63
+ const send = () => {
64
+ onSent();
65
+ onClose();
66
+ void changeRoles(accounts, chosen, tools, toaster).then(onSettled);
67
+ };
68
+ return (_jsxs(FormDialog, { title: __('Change role', DOMAIN), size: "medium", kind: "edit", submitLabel: __('Change role', DOMAIN), cancelLabel: __('Cancel', DOMAIN), canSubmit: chosen !== '', onSubmit: send, onClose: onClose, children: [_jsx(Text, { children: roleLineOf(accounts) }), _jsx(RoleSelect, { grantable: tools.grantable, value: chosen, onChange: setChosen, roleLabels: tools.roleLabels, placeholder: start === '' ? __('Change role to…', DOMAIN) : undefined })] }));
69
+ }
70
+ /**
71
+ * Renders the Change role dialog the page holds open, none while it holds none.
72
+ * @param props - The dialog, what it needs, what takes it away, what clears the selection and the focus back.
73
+ * @returns The dialog, giving the focus back to its trigger once it closed and once its writes read the list again.
74
+ */
75
+ export function OpenRoleDialog({ opened, tools, onClose, onClearSelection, onBack, }) {
76
+ if (opened === undefined) {
77
+ return null;
78
+ }
79
+ const back = () => onBack(opened.accounts[0].id, opened.trigger);
80
+ const close = () => {
81
+ onClose();
82
+ back();
83
+ };
84
+ const sent = () => {
85
+ if (opened.trigger === 'bulk') {
86
+ onClearSelection();
87
+ }
88
+ };
89
+ return _jsx(ChangeRoleDialog, { accounts: opened.accounts, tools: tools, onClose: close, onSent: sent, onSettled: back });
90
+ }
@@ -0,0 +1,13 @@
1
+ import type { UnsavedText } from '@gopherium/godmin';
2
+ import type { RenderModalProps } from '@wordpress/dataviews';
3
+ import type { Account } from '../../account.js';
4
+ import type { ErrorMessages } from './words.js';
5
+ /**
6
+ * Renders the confirm sending a pending account a new invitation, closing only itself once the write ends.
7
+ * @param props - The account, what closes the dialog, the kept activation links and the app's own error words.
8
+ * @returns The confirm, or the activation link the server answered, kept for the next open if the dialog closed first.
9
+ */
10
+ export declare function ResendModal({ items: [account], closeModal, links, errorMessages, }: RenderModalProps<Account> & {
11
+ links: UnsavedText;
12
+ errorMessages: ErrorMessages;
13
+ }): import("react").JSX.Element;