@rapidmx/web-client 0.4.0 → 0.5.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/audit-log/index.tsx +35 -8
- package/apps/admin/data-requests/index.tsx +480 -485
- package/apps/admin/distribution-lists/[uid].tsx +19 -1
- package/apps/admin/escrow-scopes/[uid].tsx +348 -237
- package/apps/admin/escrow-scopes/new/index.tsx +8 -2
- package/apps/admin/index.tsx +19 -1
- package/apps/admin/ingest-queue/index.tsx +44 -5
- package/apps/admin/mailboxes/[uid].tsx +211 -184
- package/apps/admin/quarantine/index.tsx +46 -14
- package/apps/admin/transport-rules/[uid].tsx +30 -1
- package/apps/admin/transport-rules/_transportRuleConfig.tsx +38 -2
- package/apps/admin/transport-rules/index.tsx +24 -6
- package/apps/admin/transport-rules/new/index.tsx +30 -1
- package/apps/escrow/_layout.tsx +2 -3
- package/apps/escrow/audit-log/index.tsx +196 -168
- package/apps/escrow/matters/[uid].tsx +621 -521
- package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +22 -1
- package/apps/shared/components/admin/layout/AdminShell.tsx +3 -1
- package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -0
- package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
- package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +55 -11
- package/apps/shared/components/admin/settings/BrandingForm.tsx +15 -4
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +18 -7
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +10 -2
- package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +6 -0
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +45 -12
- package/apps/shared/components/admin/settings/PluginsManager.tsx +1235 -595
- package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +174 -98
- package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +49 -13
- package/apps/shared/components/admin/setup/SetupWizard.tsx +150 -34
- package/apps/shared/components/admin/signOut.ts +79 -0
- package/apps/shared/components/admin/usePagedList.tsx +129 -0
- package/apps/shared/components/calendar/EventModal.tsx +717 -549
- package/apps/shared/components/calendar/MonthView.tsx +173 -139
- package/apps/shared/components/calendar/RecurrenceEditor.tsx +222 -180
- package/apps/shared/components/calendar/SplitDayView.tsx +143 -137
- package/apps/shared/components/calendar/TimeGridView.tsx +244 -198
- package/apps/shared/components/calendar/allDay.ts +124 -0
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +1 -1
- package/apps/shared/components/contacts/ContactDetailPane.tsx +100 -21
- package/apps/shared/components/contacts/ContactForm.tsx +377 -348
- package/apps/shared/components/contacts/ContactsSidebar.tsx +6 -2
- package/apps/shared/components/contacts/KeyChangeReview.tsx +192 -0
- package/apps/shared/components/contacts/contactKeys.ts +78 -0
- package/apps/shared/components/escrow/layout/EscrowShell.tsx +148 -134
- package/apps/shared/components/layout/AppShell.tsx +87 -15
- package/apps/shared/components/layout/BrandingChrome.tsx +48 -10
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +389 -298
- package/apps/shared/components/layout/MailboxProvisioning.tsx +19 -3
- package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +350 -0
- package/apps/shared/components/layout/UnlockPromptProvider.tsx +257 -128
- package/apps/shared/components/mail/ConversationThreadPane.tsx +216 -181
- package/apps/shared/components/mail/MessageDetailPane.tsx +1318 -630
- package/apps/shared/components/mail/compose/ComposeContext.tsx +20 -15
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +854 -132
- package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -0
- package/apps/shared/components/mail/layout/MailShell.tsx +12 -2
- package/apps/shared/components/mail/pinnedSigners.ts +124 -0
- package/apps/shared/components/mail/verificationSeals.ts +125 -0
- package/apps/shared/components/mail/writableMailboxes.ts +101 -0
- package/apps/shared/components/rules/RuleBuilder.tsx +311 -276
- package/apps/shared/mail/listAllPages.ts +39 -0
- package/apps/shared/search/LocalIndexLifecycle.tsx +64 -55
- package/apps/shared/search/localIndexBuilder.ts +481 -179
- package/apps/shared/search/localIndexRpcClient.ts +228 -28
- package/apps/shared/search/localIndexSchema.ts +15 -6
- package/apps/shared/search/localIndexStorage.ts +98 -0
- package/apps/shared/search/localIndexVFS.ts +32 -1
- package/apps/shared/search/localIndexWorker.ts +648 -186
- package/apps/www/calendar/index.tsx +438 -417
- package/apps/www/contacts/[uid].tsx +21 -2
- package/apps/www/contacts/index.tsx +116 -12
- package/apps/www/index.tsx +1314 -1041
- package/apps/www/messages/[uid].tsx +18 -6
- package/apps/www/settings/auto-reply/index.tsx +134 -129
- package/apps/www/settings/booking-types/[uid].tsx +306 -300
- package/apps/www/settings/encryption/index.tsx +739 -216
- package/apps/www/settings/filters/[uid].tsx +171 -155
- package/apps/www/settings/filters/new/index.tsx +8 -1
- package/apps/www/settings/focused-inbox/index.tsx +150 -148
- package/apps/www/settings/privacy/index.tsx +494 -436
- package/apps/www/settings/read-receipts/index.tsx +148 -125
- package/apps/www/settings/sharing/index.tsx +17 -14
- package/apps/www/tasks/index.tsx +638 -579
- package/dist/apps/admin/audit-log/index.js +38 -8
- package/dist/apps/admin/data-requests/index.js +52 -72
- package/dist/apps/admin/distribution-lists/[uid].js +1 -1
- package/dist/apps/admin/escrow-scopes/[uid].js +91 -12
- package/dist/apps/admin/escrow-scopes/new/index.js +8 -3
- package/dist/apps/admin/index.js +4 -1
- package/dist/apps/admin/ingest-queue/index.js +24 -6
- package/dist/apps/admin/mailboxes/[uid].js +20 -4
- package/dist/apps/admin/quarantine/index.js +20 -11
- package/dist/apps/admin/transport-rules/[uid].js +18 -2
- package/dist/apps/admin/transport-rules/_transportRuleConfig.js +19 -0
- package/dist/apps/admin/transport-rules/index.js +22 -6
- package/dist/apps/admin/transport-rules/new/index.js +18 -2
- package/dist/apps/escrow/_layout.js +3 -3
- package/dist/apps/escrow/audit-log/index.js +26 -1
- package/dist/apps/escrow/matters/[uid].js +77 -42
- package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +11 -2
- package/dist/apps/shared/components/admin/layout/AdminShell.js +3 -1
- package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +72 -0
- package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
- package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +22 -4
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +15 -4
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +13 -7
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +7 -2
- package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +6 -0
- package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +34 -12
- package/dist/apps/shared/components/admin/settings/PluginsManager.js +390 -78
- package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +40 -11
- package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +29 -13
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +87 -21
- package/dist/apps/shared/components/admin/signOut.js +75 -0
- package/dist/apps/shared/components/admin/usePagedList.js +99 -0
- package/dist/apps/shared/components/calendar/EventModal.js +142 -21
- package/dist/apps/shared/components/calendar/MonthView.js +12 -5
- package/dist/apps/shared/components/calendar/RecurrenceEditor.js +40 -5
- package/dist/apps/shared/components/calendar/SplitDayView.js +10 -4
- package/dist/apps/shared/components/calendar/TimeGridView.js +23 -7
- package/dist/apps/shared/components/calendar/allDay.js +111 -0
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +1 -1
- package/dist/apps/shared/components/contacts/ContactDetailPane.js +43 -5
- package/dist/apps/shared/components/contacts/ContactForm.js +32 -10
- package/dist/apps/shared/components/contacts/ContactsSidebar.js +2 -2
- package/dist/apps/shared/components/contacts/KeyChangeReview.js +63 -0
- package/dist/apps/shared/components/contacts/contactKeys.js +64 -0
- package/dist/apps/shared/components/escrow/layout/EscrowShell.js +24 -5
- package/dist/apps/shared/components/layout/AppShell.js +83 -14
- package/dist/apps/shared/components/layout/BrandingChrome.js +45 -10
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +49 -12
- package/dist/apps/shared/components/layout/MailboxProvisioning.js +10 -2
- package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +174 -0
- package/dist/apps/shared/components/layout/UnlockPromptProvider.js +73 -14
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +50 -12
- package/dist/apps/shared/components/mail/MessageDetailPane.js +441 -36
- package/dist/apps/shared/components/mail/compose/ComposeContext.js +9 -6
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +699 -128
- package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +50 -0
- package/dist/apps/shared/components/mail/layout/MailShell.js +12 -3
- package/dist/apps/shared/components/mail/pinnedSigners.js +98 -0
- package/dist/apps/shared/components/mail/verificationSeals.js +112 -0
- package/dist/apps/shared/components/mail/writableMailboxes.js +87 -0
- package/dist/apps/shared/components/rules/RuleBuilder.js +34 -6
- package/dist/apps/shared/mail/listAllPages.js +25 -0
- package/dist/apps/shared/search/LocalIndexLifecycle.js +58 -54
- package/dist/apps/shared/search/localIndexBuilder.js +292 -37
- package/dist/apps/shared/search/localIndexRpcClient.js +188 -25
- package/dist/apps/shared/search/localIndexSchema.js +12 -6
- package/dist/apps/shared/search/localIndexStorage.js +86 -0
- package/dist/apps/shared/search/localIndexVFS.js +68 -40
- package/dist/apps/shared/search/localIndexWorker.js +519 -173
- package/dist/apps/www/calendar/index.js +28 -10
- package/dist/apps/www/contacts/[uid].js +13 -2
- package/dist/apps/www/contacts/index.js +80 -10
- package/dist/apps/www/index.js +339 -108
- package/dist/apps/www/messages/[uid].js +18 -6
- package/dist/apps/www/settings/auto-reply/index.js +9 -4
- package/dist/apps/www/settings/booking-types/[uid].js +17 -12
- package/dist/apps/www/settings/encryption/index.js +514 -97
- package/dist/apps/www/settings/filters/[uid].js +22 -7
- package/dist/apps/www/settings/filters/new/index.js +8 -2
- package/dist/apps/www/settings/focused-inbox/index.js +3 -1
- package/dist/apps/www/settings/privacy/index.js +57 -22
- package/dist/apps/www/settings/read-receipts/index.js +10 -3
- package/dist/apps/www/settings/sharing/index.js +7 -11
- package/dist/apps/www/tasks/index.js +50 -10
- package/package.json +2 -2
|
@@ -9,6 +9,7 @@ import ContactsShell, { ContactsShellProps } from "../../shared/components/conta
|
|
|
9
9
|
import ContactDetailPane from "../../shared/components/contacts/ContactDetailPane.js";
|
|
10
10
|
import ContactForm from "../../shared/components/contacts/ContactForm.js";
|
|
11
11
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
12
|
+
import { clearPinnedSignerCache } from "../../shared/components/mail/pinnedSigners.js";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Only reached on mobile (below the `md` breakpoint) — desktop's `apps/www/contacts/index.tsx` keeps its
|
|
@@ -30,6 +31,9 @@ function ContactDetailContent({ uid }: { uid: string }) {
|
|
|
30
31
|
const [contact, setContact] = useState<Contact | null>(null);
|
|
31
32
|
const [loading, setLoading] = useState(true);
|
|
32
33
|
const [error, setError] = useState<string | null>(null);
|
|
34
|
+
// Kept apart from the load `error` - a failed delete must leave the contact on screen (with the
|
|
35
|
+
// reason) rather than replacing the whole page with an error as if it had never loaded.
|
|
36
|
+
const [deleteError, setDeleteError] = useState<string | null>(null);
|
|
33
37
|
const [mode, setMode] = useState<Mode>("view");
|
|
34
38
|
|
|
35
39
|
useEffect(() => {
|
|
@@ -46,14 +50,22 @@ function ContactDetailContent({ uid }: { uid: string }) {
|
|
|
46
50
|
// rendered once `contact` is already resolved (see the `error || !contact` guard above it), so this
|
|
47
51
|
// never runs with a stale/absent contact — no redundant null check needed here.
|
|
48
52
|
async function handleDelete(contact: Contact) {
|
|
53
|
+
setDeleteError(null);
|
|
49
54
|
try {
|
|
50
55
|
await deleteContact(contact.uid, contact.version);
|
|
56
|
+
// A deleted contact's pinned signing keys must stop vouching for signatures.
|
|
57
|
+
clearPinnedSignerCache();
|
|
51
58
|
window.location.href = "/contacts";
|
|
52
59
|
} catch (err) {
|
|
53
|
-
|
|
60
|
+
setDeleteError(err instanceof ApiRequestError ? err.message : "Could not delete this contact.");
|
|
54
61
|
}
|
|
55
62
|
}
|
|
56
63
|
|
|
64
|
+
// After a key change was resolved (or found stale): re-read the contact, keeping the one shown if that fails.
|
|
65
|
+
function handleKeysChanged() {
|
|
66
|
+
getContact(uid).then(setContact, () => undefined);
|
|
67
|
+
}
|
|
68
|
+
|
|
57
69
|
if (loading) {
|
|
58
70
|
return <p className="p-8 text-sm text-text-muted">Loading…</p>;
|
|
59
71
|
}
|
|
@@ -82,7 +94,14 @@ function ContactDetailContent({ uid }: { uid: string }) {
|
|
|
82
94
|
|
|
83
95
|
return (
|
|
84
96
|
<div className="p-6">
|
|
85
|
-
|
|
97
|
+
{deleteError && <Alert>{deleteError}</Alert>}
|
|
98
|
+
<ContactDetailPane
|
|
99
|
+
contact={contact}
|
|
100
|
+
onEdit={() => setMode("edit")}
|
|
101
|
+
onDelete={() => handleDelete(contact)}
|
|
102
|
+
backHref={backHref}
|
|
103
|
+
onKeysChanged={handleKeysChanged}
|
|
104
|
+
/>
|
|
86
105
|
</div>
|
|
87
106
|
);
|
|
88
107
|
}
|
|
@@ -22,7 +22,13 @@ import ContactsToolbar from "../../shared/components/contacts/ContactsToolbar.js
|
|
|
22
22
|
import ContactAvatar from "@rapidmx/react-shared/components/avatar/ContactAvatar.js";
|
|
23
23
|
import ContactDetailPane from "../../shared/components/contacts/ContactDetailPane.js";
|
|
24
24
|
import ContactForm from "../../shared/components/contacts/ContactForm.js";
|
|
25
|
+
import { useWritableMailboxes } from "../../shared/components/mail/writableMailboxes.js";
|
|
26
|
+
import { getMyMailboxAccess } from "@rapidmx/react-shared/mail/mailboxAccessApi.js";
|
|
25
27
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
28
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
29
|
+
import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
|
|
30
|
+
import { LIST_PAGE_SIZE, MAX_LIST_PAGES, listAllPages } from "../../shared/mail/listAllPages.js";
|
|
31
|
+
import { clearPinnedSignerCache } from "../../shared/components/mail/pinnedSigners.js";
|
|
26
32
|
|
|
27
33
|
const INPUT_CLASS =
|
|
28
34
|
"w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
@@ -30,7 +36,7 @@ const INPUT_CLASS =
|
|
|
30
36
|
export default function ContactsPage(props: ContactsShellProps) {
|
|
31
37
|
return (
|
|
32
38
|
<ContactsShell {...props}>
|
|
33
|
-
<ContactsContent />
|
|
39
|
+
<ContactsContent userUid={props.userUid} />
|
|
34
40
|
</ContactsShell>
|
|
35
41
|
);
|
|
36
42
|
}
|
|
@@ -38,22 +44,33 @@ export default function ContactsPage(props: ContactsShellProps) {
|
|
|
38
44
|
type Mode = "view" | "edit" | "new";
|
|
39
45
|
type SortColumn = "name" | "info";
|
|
40
46
|
|
|
47
|
+
/** Shown in the Deleted view when the server answered `?deleted=true` with live contacts (see `canViewDeleted`). */
|
|
48
|
+
const DELETED_NOT_PERMITTED_MESSAGE = "You don't have permission to view deleted contacts in this folder.";
|
|
49
|
+
|
|
41
50
|
function primaryInfo(contact: Contact): string {
|
|
42
51
|
return contact.emails[0]?.address ?? contact.phones[0]?.phoneNumber ?? "";
|
|
43
52
|
}
|
|
44
53
|
|
|
54
|
+
/** How long an export's object URL is kept alive after the click - some browsers are still reading the
|
|
55
|
+
* blob after `click()` returns, so revoking it synchronously can cancel the download. */
|
|
56
|
+
const VCARD_DOWNLOAD_URL_LIFETIME_MS = 60_000;
|
|
57
|
+
|
|
45
58
|
function downloadTextFile(filename: string, content: string): void {
|
|
46
59
|
const blob = new Blob([content], { type: "text/vcard" });
|
|
47
60
|
const url = URL.createObjectURL(blob);
|
|
48
61
|
const a = document.createElement("a");
|
|
49
62
|
a.href = url;
|
|
50
63
|
a.download = filename;
|
|
64
|
+
document.body.appendChild(a);
|
|
51
65
|
a.click();
|
|
52
|
-
|
|
66
|
+
a.remove();
|
|
67
|
+
setTimeout(() => URL.revokeObjectURL(url), VCARD_DOWNLOAD_URL_LIFETIME_MS);
|
|
53
68
|
}
|
|
54
69
|
|
|
55
|
-
function ContactsContent() {
|
|
70
|
+
function ContactsContent({ userUid }: { userUid?: string }) {
|
|
56
71
|
const { folderUid, mailboxUid, mailboxes } = useContactsShell();
|
|
72
|
+
// The new-contact form's Mailbox choices - view-only shares are left out (see writableMailboxes.ts).
|
|
73
|
+
const writableMailboxes = useWritableMailboxes(mailboxes, userUid, mailboxUid);
|
|
57
74
|
const { openCompose } = useCompose();
|
|
58
75
|
const isMobile = useIsMobile();
|
|
59
76
|
const [contacts, setContacts] = useState<Contact[]>([]);
|
|
@@ -62,6 +79,9 @@ function ContactsContent() {
|
|
|
62
79
|
const [deletedError, setDeletedError] = useState<string | null>(null);
|
|
63
80
|
const [loading, setLoading] = useState(true);
|
|
64
81
|
const [error, setError] = useState<string | null>(null);
|
|
82
|
+
// Set when `listAllPages()` stopped at its page cap - the list shown isn't every contact.
|
|
83
|
+
const [truncated, setTruncated] = useState(false);
|
|
84
|
+
const [deletedTruncated, setDeletedTruncated] = useState(false);
|
|
65
85
|
const [query, setQuery] = useState("");
|
|
66
86
|
const [view, setView] = useState<ContactsView>({ type: "all" });
|
|
67
87
|
const [sortColumn, setSortColumn] = useState<SortColumn>("name");
|
|
@@ -71,6 +91,7 @@ function ContactsContent() {
|
|
|
71
91
|
const [mode, setMode] = useState<Mode>("view");
|
|
72
92
|
// Set after a contact is created in a different mailbox than the one this list shows.
|
|
73
93
|
const [savedElsewhere, setSavedElsewhere] = useState<{ mailboxUid: string; displayName: string } | null>(null);
|
|
94
|
+
const [confirmingBulkDelete, setConfirmingBulkDelete] = useState(false);
|
|
74
95
|
|
|
75
96
|
/** Returns a promise so bulk actions (below) can wait for the refreshed list before re-asserting their
|
|
76
97
|
* own error message — this always clears `error` first (a legitimate reset for a fresh fetch attempt),
|
|
@@ -79,13 +100,17 @@ function ContactsContent() {
|
|
|
79
100
|
function reload(): Promise<void> {
|
|
80
101
|
if (!folderUid) {
|
|
81
102
|
setContacts([]);
|
|
103
|
+
setTruncated(false);
|
|
82
104
|
setLoading(false);
|
|
83
105
|
return Promise.resolve();
|
|
84
106
|
}
|
|
85
107
|
setLoading(true);
|
|
86
108
|
setError(null);
|
|
87
|
-
return listContacts(folderUid, { limit:
|
|
88
|
-
.then(
|
|
109
|
+
return listAllPages((page) => listContacts(folderUid, { limit: LIST_PAGE_SIZE, page }))
|
|
110
|
+
.then((result) => {
|
|
111
|
+
setContacts(result.items);
|
|
112
|
+
setTruncated(result.truncated);
|
|
113
|
+
})
|
|
89
114
|
.catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load contacts."))
|
|
90
115
|
.finally(() => setLoading(false));
|
|
91
116
|
}
|
|
@@ -94,14 +119,56 @@ function ContactsContent() {
|
|
|
94
119
|
void reload();
|
|
95
120
|
}, [folderUid]);
|
|
96
121
|
|
|
122
|
+
// restapi only honors `?deleted=true` for a caller with both delete and update rights on the contacts *folder*
|
|
123
|
+
// (its ACL, which inherits the mailbox's but can carry its own records) - anyone else silently gets the *live*
|
|
124
|
+
// contacts back, which the Deleted view would then present as deleted. There is no client API for the caller's
|
|
125
|
+
// access to a folder, so the mailbox-level check below is only a first gate (an owner always passes; anyone else
|
|
126
|
+
// asks the server; unknown - still loading, or the check failed - hides the view), and the Deleted view's own
|
|
127
|
+
// load below detects the server ignoring the filter. `ContactsShell` only renders this with a resolved mailbox,
|
|
128
|
+
// and switching mailboxes is a full page load, so `mailboxUid` never changes under this component.
|
|
129
|
+
const ownsMailbox = mailboxes.some((mb) => mb.uid === mailboxUid && mb.ownerUserUid !== undefined && mb.ownerUserUid === userUid);
|
|
130
|
+
const [delegateCanViewDeleted, setDelegateCanViewDeleted] = useState(false);
|
|
131
|
+
// Resolving a contact's key change needs update rights on the mailbox - `undefined` while unknown.
|
|
132
|
+
const [delegateCanUpdate, setDelegateCanUpdate] = useState<boolean | undefined>(undefined);
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
if (ownsMailbox) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
let cancelled = false;
|
|
138
|
+
getMyMailboxAccess(mailboxUid!).then(
|
|
139
|
+
(access) => {
|
|
140
|
+
if (!cancelled) {
|
|
141
|
+
setDelegateCanViewDeleted(access.canDelete && access.canUpdate);
|
|
142
|
+
setDelegateCanUpdate(access.canUpdate);
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
() => undefined,
|
|
146
|
+
);
|
|
147
|
+
return () => {
|
|
148
|
+
cancelled = true;
|
|
149
|
+
};
|
|
150
|
+
}, [ownsMailbox]);
|
|
151
|
+
const canViewDeleted = ownsMailbox || delegateCanViewDeleted;
|
|
152
|
+
|
|
97
153
|
useEffect(() => {
|
|
98
154
|
if (view.type !== "deleted" || !folderUid) {
|
|
99
155
|
return;
|
|
100
156
|
}
|
|
101
157
|
setDeletedLoading(true);
|
|
102
158
|
setDeletedError(null);
|
|
103
|
-
listDeletedContacts(folderUid, { limit:
|
|
104
|
-
.then(
|
|
159
|
+
listAllPages((page) => listDeletedContacts(folderUid, { limit: LIST_PAGE_SIZE, page }))
|
|
160
|
+
.then((result) => {
|
|
161
|
+
// A server that dropped the `deleted` filter (no delete+update right on this folder) answers with live
|
|
162
|
+
// contacts, which must never be listed as deleted.
|
|
163
|
+
if (result.items.some((c) => c.deleted !== true)) {
|
|
164
|
+
setDeletedContacts([]);
|
|
165
|
+
setDeletedTruncated(false);
|
|
166
|
+
setDeletedError(DELETED_NOT_PERMITTED_MESSAGE);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
setDeletedContacts(result.items);
|
|
170
|
+
setDeletedTruncated(result.truncated);
|
|
171
|
+
})
|
|
105
172
|
.catch((err) => setDeletedError(err instanceof ApiRequestError ? err.message : "Could not load deleted contacts."))
|
|
106
173
|
.finally(() => setDeletedLoading(false));
|
|
107
174
|
// Re-fetches every time the Deleted view is (re-)selected, matching this page's own `reload()`
|
|
@@ -213,6 +280,8 @@ function ContactsContent() {
|
|
|
213
280
|
async function handleDelete(contact: Contact) {
|
|
214
281
|
try {
|
|
215
282
|
await deleteContact(contact.uid, contact.version);
|
|
283
|
+
// Trusted signer pins come from contacts - a deleted contact's keys must stop vouching for signatures.
|
|
284
|
+
clearPinnedSignerCache();
|
|
216
285
|
setSelectedUid(null);
|
|
217
286
|
setMode("view");
|
|
218
287
|
void reload();
|
|
@@ -222,6 +291,7 @@ function ContactsContent() {
|
|
|
222
291
|
}
|
|
223
292
|
|
|
224
293
|
async function handleBulkDelete() {
|
|
294
|
+
setConfirmingBulkDelete(false);
|
|
225
295
|
let bulkError: string | null = null;
|
|
226
296
|
for (const contact of checkedContacts) {
|
|
227
297
|
try {
|
|
@@ -230,6 +300,7 @@ function ContactsContent() {
|
|
|
230
300
|
bulkError = err instanceof ApiRequestError ? err.message : "Could not delete one or more contacts.";
|
|
231
301
|
}
|
|
232
302
|
}
|
|
303
|
+
clearPinnedSignerCache();
|
|
233
304
|
setCheckedUids(new Set());
|
|
234
305
|
await reload();
|
|
235
306
|
setError(bulkError);
|
|
@@ -261,6 +332,7 @@ function ContactsContent() {
|
|
|
261
332
|
bulkError = err instanceof ApiRequestError ? err.message : "Could not update one or more contacts.";
|
|
262
333
|
}
|
|
263
334
|
}
|
|
335
|
+
clearPinnedSignerCache();
|
|
264
336
|
await reload();
|
|
265
337
|
setError(bulkError);
|
|
266
338
|
}
|
|
@@ -274,11 +346,14 @@ function ContactsContent() {
|
|
|
274
346
|
for (const contact of checkedContacts) {
|
|
275
347
|
const categories = Array.from(new Set([...(contact.categories ?? []), category.trim()]));
|
|
276
348
|
try {
|
|
277
|
-
|
|
349
|
+
// Only the changed field - sending the whole fetched contact back includes server-managed
|
|
350
|
+
// fields restapi rejects, and would overwrite any concurrent edit to the other fields.
|
|
351
|
+
await updateContact({ uid: contact.uid, version: contact.version, categories });
|
|
278
352
|
} catch (err) {
|
|
279
353
|
bulkError = err instanceof ApiRequestError ? err.message : "Could not update one or more contacts.";
|
|
280
354
|
}
|
|
281
355
|
}
|
|
356
|
+
clearPinnedSignerCache();
|
|
282
357
|
await reload();
|
|
283
358
|
setError(bulkError);
|
|
284
359
|
}
|
|
@@ -304,13 +379,14 @@ function ContactsContent() {
|
|
|
304
379
|
bulkError = err instanceof ApiRequestError ? err.message : "Could not import one or more contacts.";
|
|
305
380
|
}
|
|
306
381
|
}
|
|
382
|
+
clearPinnedSignerCache();
|
|
307
383
|
await reload();
|
|
308
384
|
setError(bulkError);
|
|
309
385
|
}
|
|
310
386
|
|
|
311
387
|
return (
|
|
312
388
|
<div className="flex-1 flex min-h-0">
|
|
313
|
-
<ContactsSidebar mailboxUid={mailboxUid} contacts={contacts} active={view} onSelect={handleSelectView} />
|
|
389
|
+
<ContactsSidebar mailboxUid={mailboxUid} contacts={contacts} active={view} onSelect={handleSelectView} showDeleted={canViewDeleted} />
|
|
314
390
|
{/* Hidden on mobile while the "new contact" form (the one form-pane state mobile keeps
|
|
315
391
|
in-place — see the pane's own comment below) is showing, so the two never compete for the
|
|
316
392
|
same row's width. Desktop always shows both side by side, unchanged. */}
|
|
@@ -325,7 +401,7 @@ function ContactsContent() {
|
|
|
325
401
|
allSelectedFavorited={checkedContacts.length > 0 && checkedContacts.every((c) => c.favorite)}
|
|
326
402
|
onNewContact={handleNew}
|
|
327
403
|
onEdit={handleToolbarEdit}
|
|
328
|
-
onDelete={
|
|
404
|
+
onDelete={() => setConfirmingBulkDelete(true)}
|
|
329
405
|
onEmail={handleEmail}
|
|
330
406
|
onToggleFavorite={handleToggleFavorite}
|
|
331
407
|
onAddCategory={handleAddCategory}
|
|
@@ -347,6 +423,14 @@ function ContactsContent() {
|
|
|
347
423
|
<Alert>{error}</Alert>
|
|
348
424
|
</div>
|
|
349
425
|
)}
|
|
426
|
+
{(isDeletedView ? deletedTruncated : truncated) && (
|
|
427
|
+
<div className="p-3">
|
|
428
|
+
<Alert>
|
|
429
|
+
This folder has more contacts than can be shown at once - only the first {LIST_PAGE_SIZE * MAX_LIST_PAGES} are
|
|
430
|
+
listed.
|
|
431
|
+
</Alert>
|
|
432
|
+
</div>
|
|
433
|
+
)}
|
|
350
434
|
{deletedError && isDeletedView && (
|
|
351
435
|
<div className="p-3">
|
|
352
436
|
<Alert>{deletedError}</Alert>
|
|
@@ -446,18 +530,38 @@ function ContactsContent() {
|
|
|
446
530
|
<ContactForm
|
|
447
531
|
mailboxUid={mailboxUid}
|
|
448
532
|
folderUid={folderUid}
|
|
449
|
-
mailboxes={
|
|
533
|
+
mailboxes={writableMailboxes}
|
|
450
534
|
onSaved={handleSaved}
|
|
451
535
|
onCancel={handleCancel}
|
|
452
536
|
/>
|
|
453
537
|
) : mode === "edit" && selected ? (
|
|
454
538
|
<ContactForm contact={selected} onSaved={handleSaved} onCancel={handleCancel} />
|
|
455
539
|
) : selected ? (
|
|
456
|
-
<ContactDetailPane
|
|
540
|
+
<ContactDetailPane
|
|
541
|
+
contact={selected}
|
|
542
|
+
onEdit={() => setMode("edit")}
|
|
543
|
+
onDelete={() => handleDelete(selected)}
|
|
544
|
+
onKeysChanged={() => void reload()}
|
|
545
|
+
canResolveKeys={ownsMailbox || delegateCanUpdate}
|
|
546
|
+
/>
|
|
457
547
|
) : (
|
|
458
548
|
<p className="text-sm text-text-muted">Select a contact, or create a new one.</p>
|
|
459
549
|
)}
|
|
460
550
|
</div>
|
|
551
|
+
<Modal open={confirmingBulkDelete} onClose={() => setConfirmingBulkDelete(false)} title="Delete contacts">
|
|
552
|
+
<p className="text-sm mb-5">
|
|
553
|
+
Delete {checkedContacts.length} selected {checkedContacts.length === 1 ? "contact" : "contacts"}? They
|
|
554
|
+
move to Deleted contacts.
|
|
555
|
+
</p>
|
|
556
|
+
<div className="flex gap-3 justify-end">
|
|
557
|
+
<Button type="button" variant="secondary" className="!w-auto" onClick={() => setConfirmingBulkDelete(false)}>
|
|
558
|
+
Cancel
|
|
559
|
+
</Button>
|
|
560
|
+
<Button type="button" className="!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger" onClick={handleBulkDelete}>
|
|
561
|
+
Delete
|
|
562
|
+
</Button>
|
|
563
|
+
</div>
|
|
564
|
+
</Modal>
|
|
461
565
|
</div>
|
|
462
566
|
);
|
|
463
567
|
}
|