@rapidmx/web-client 0.11.0 → 0.12.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/apps/admin/mailboxes/new/index.tsx +3 -3
- package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +4 -4
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +60 -14
- package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +65 -16
- package/apps/shared/components/sharing/PrincipalListField.tsx +82 -0
- package/apps/shared/components/sharing/PrincipalPicker.tsx +40 -109
- package/apps/shared/components/sharing/PrincipalResolver.tsx +164 -0
- package/dist/apps/admin/mailboxes/new/index.js +1 -1
- package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +3 -2
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +25 -5
- package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +18 -7
- package/dist/apps/shared/components/sharing/PrincipalListField.d.ts +24 -0
- package/dist/apps/shared/components/sharing/PrincipalListField.js +23 -0
- package/dist/apps/shared/components/sharing/PrincipalPicker.d.ts +8 -6
- package/dist/apps/shared/components/sharing/PrincipalPicker.js +12 -65
- package/dist/apps/shared/components/sharing/PrincipalResolver.d.ts +44 -0
- package/dist/apps/shared/components/sharing/PrincipalResolver.js +92 -0
- package/package.json +2 -2
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import { useEffect, useState } from "react";
|
|
7
|
+
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
9
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
10
|
+
/** Who a resolved person is, for a person to check before confirming: their name and address, else their uid. */
|
|
11
|
+
export function describePerson(person) {
|
|
12
|
+
if (person.displayName && person.address)
|
|
13
|
+
return `${person.displayName} <${person.address}>`;
|
|
14
|
+
return person.displayName ?? person.address ?? `user ${person.userUid}`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* "Who?" - anywhere a person is assigned by typed identifier rather than picked from an already-loaded list. What
|
|
18
|
+
* is typed is RESOLVED by the server (`resolve`) to the person it names before anything is saved, and the person is
|
|
19
|
+
* shown (name and address) for the caller to confirm; only `onResolved` ever sees the resolved, verified person -
|
|
20
|
+
* never the free text. This is deliberately exact-match, type-then-confirm - never a live/fuzzy search-as-you-type
|
|
21
|
+
* autocomplete, which would let anyone with access to the field enumerate the user directory by partial name.
|
|
22
|
+
*
|
|
23
|
+
* Parameterized entirely by `resolve`/`onResolved` (and the wording props) so every caller - mailbox sharing
|
|
24
|
+
* (`PrincipalPicker`, a thin wrapper around this component), mailbox ownership, escrow-scope holders, ... - reuses
|
|
25
|
+
* the same resolve-then-confirm UX and privacy posture rather than each reimplementing it.
|
|
26
|
+
*
|
|
27
|
+
* Deliberately renders no `<form>` of its own (Enter-to-find is wired up by hand instead, see `handleKeyDown()`) -
|
|
28
|
+
* some callers (`MailboxCreateForm`) use this as one field of their own, larger `<form>`, and a nested `<form>` is
|
|
29
|
+
* invalid HTML that real browsers mis-handle (an inner Enter keypress can submit the wrong one).
|
|
30
|
+
*/
|
|
31
|
+
export default function PrincipalResolver({ resolve, onResolved, initialPrincipal, placeholder, ariaLabel, children, confirmLabel, describeConfirm, confirmErrorMessage, onCancel, }) {
|
|
32
|
+
const [typed, setTyped] = useState(initialPrincipal ?? "");
|
|
33
|
+
const [person, setPerson] = useState(null);
|
|
34
|
+
const [busy, setBusy] = useState(false);
|
|
35
|
+
const [error, setError] = useState(null);
|
|
36
|
+
// A stored entry being replaced is looked up straight away, so its person is shown without a click.
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (initialPrincipal) {
|
|
39
|
+
void find(initialPrincipal);
|
|
40
|
+
}
|
|
41
|
+
}, [initialPrincipal]);
|
|
42
|
+
async function find(principal) {
|
|
43
|
+
setBusy(true);
|
|
44
|
+
setError(null);
|
|
45
|
+
setPerson(null);
|
|
46
|
+
try {
|
|
47
|
+
setPerson(await resolve(principal));
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
setError(err instanceof ApiRequestError ? err.message : "Could not look that person up.");
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
setBusy(false);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function submitFind() {
|
|
57
|
+
const principal = typed.trim();
|
|
58
|
+
if (principal) {
|
|
59
|
+
void find(principal);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Enter in the text box finds, same as clicking the button - handled here (not a native `<form>` submit,
|
|
63
|
+
* see this component's own doc comment on why) so it works whether or not a caller nests this inside one of
|
|
64
|
+
* their own forms (`MailboxCreateForm` does). */
|
|
65
|
+
function handleKeyDown(e) {
|
|
66
|
+
if (e.key === "Enter") {
|
|
67
|
+
// Stops the keystroke from also submitting an ANCESTOR `<form>` this component happens to be nested in.
|
|
68
|
+
e.preventDefault();
|
|
69
|
+
submitFind();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Only reachable from the confirm button, which only renders once `person` is set.
|
|
73
|
+
async function handleConfirm() {
|
|
74
|
+
setBusy(true);
|
|
75
|
+
setError(null);
|
|
76
|
+
try {
|
|
77
|
+
await onResolved(person);
|
|
78
|
+
setTyped("");
|
|
79
|
+
setPerson(null);
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
setError(err instanceof ApiRequestError ? err.message : confirmErrorMessage);
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
setBusy(false);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return (_jsxs("div", { className: "flex flex-col gap-2", children: [error && _jsx(Alert, { children: error }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { type: "text", "aria-label": ariaLabel, placeholder: placeholder, className: "flex-1 text-sm border border-border rounded-sm py-1.5 px-2 bg-surface text-text", value: typed, onChange: (e) => {
|
|
89
|
+
setTyped(e.target.value);
|
|
90
|
+
setPerson(null);
|
|
91
|
+
}, onKeyDown: handleKeyDown, disabled: busy }), children, _jsx(Button, { type: "button", variant: "secondary", loading: busy && !person, disabled: busy, className: "!w-auto", onClick: submitFind, children: "Find" }), onCancel && (_jsx(Button, { type: "button", variant: "text", className: "!w-auto", onClick: onCancel, children: "Cancel" }))] }), person && (_jsxs("div", { className: "flex items-center justify-between gap-3 text-sm py-2 px-3 border border-border rounded-sm", children: [_jsx("span", { children: describeConfirm(person) }), _jsx(Button, { type: "button", loading: busy, disabled: busy, className: "!w-auto", onClick: () => void handleConfirm(), children: confirmLabel })] }))] }));
|
|
92
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rapidmx/web-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "RapidMX's webmail and admin console React UI for web-based clients.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": "https://github.com/rapidmx/web-client",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"@dnd-kit/core": "^6.3.1",
|
|
40
40
|
"@journeyapps/wa-sqlite": "^2.0.4",
|
|
41
41
|
"@peculiar/x509": "^2.1.0",
|
|
42
|
-
"@rapidmx/react-shared": "^0.
|
|
42
|
+
"@rapidmx/react-shared": "^0.12.0",
|
|
43
43
|
"@tiptap/core": "^3.31.3",
|
|
44
44
|
"@tiptap/extension-highlight": "^3.31.3",
|
|
45
45
|
"@tiptap/extension-image": "^3.31.3",
|