@rapidmx/web-client 0.15.1 → 0.17.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/README.md +381 -373
- package/apps/admin/branding/index.tsx +39 -39
- package/apps/admin/data-requests/index.tsx +482 -480
- package/apps/admin/distribution-lists/new/index.tsx +3 -1
- package/apps/admin/domains/[uid].tsx +166 -164
- package/apps/admin/domains/new/index.tsx +3 -1
- package/apps/admin/escrow-scopes/[uid].tsx +350 -348
- package/apps/admin/escrow-scopes/new/index.tsx +3 -1
- package/apps/admin/index.tsx +129 -124
- package/apps/admin/mailboxes/[uid].tsx +271 -224
- package/apps/admin/mailboxes/new/index.tsx +30 -28
- package/apps/admin/plugins/index.tsx +15 -15
- package/apps/admin/retention-policy/index.tsx +39 -39
- package/apps/admin/signing-certificates/index.tsx +343 -343
- package/apps/admin/transport-rules/new/index.tsx +3 -1
- package/apps/escrow/audit-log/index.tsx +196 -196
- package/apps/escrow/matters/[uid].tsx +621 -621
- package/apps/escrow/matters/new/index.tsx +3 -1
- package/apps/shared/auth/adminAccess.ts +99 -99
- package/apps/shared/components/admin/layout/AdminShell.tsx +381 -378
- package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +238 -0
- package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -152
- package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +194 -0
- package/apps/shared/components/admin/settings/BrandingForm.tsx +423 -423
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +119 -119
- package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +50 -3
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +198 -198
- package/apps/shared/components/admin/settings/PluginsManager.tsx +1545 -1545
- package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +182 -182
- package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +288 -288
- package/apps/shared/components/admin/setup/SetupWizard.tsx +446 -444
- package/apps/shared/components/admin/usePagedList.tsx +129 -129
- package/apps/shared/components/calendar/EventEditor.tsx +5 -10
- package/apps/shared/components/calendar/EventFormParts.tsx +16 -47
- package/apps/shared/components/calendar/EventModal.tsx +206 -206
- package/apps/shared/components/calendar/GuestInput.tsx +100 -0
- package/apps/shared/components/calendar/MonthView.tsx +185 -185
- package/apps/shared/components/calendar/RecurrenceEditor.tsx +227 -227
- package/apps/shared/components/calendar/RequestChangeForm.tsx +23 -54
- package/apps/shared/components/calendar/SplitDayView.tsx +144 -144
- package/apps/shared/components/calendar/TimeGridView.tsx +246 -246
- package/apps/shared/components/calendar/allDay.ts +124 -124
- package/apps/shared/components/calendar/eventForm.ts +2 -2
- package/apps/shared/components/calendar/eventFormat.ts +49 -22
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +201 -200
- package/apps/shared/components/contacts/ContactForm.tsx +383 -383
- package/apps/shared/components/contacts/ContactsSidebar.tsx +1 -1
- package/apps/shared/components/contacts/ContactsToolbar.tsx +103 -98
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +218 -216
- package/apps/shared/components/escrow/layout/EscrowShell.tsx +155 -155
- package/apps/shared/components/layout/AppShell.tsx +461 -480
- package/apps/shared/components/layout/FloatingActionButton.tsx +31 -0
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -398
- package/apps/shared/components/layout/MailboxProvisioning.tsx +168 -168
- package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -315
- package/apps/shared/components/layout/ThemeSwitch.tsx +84 -84
- package/apps/shared/components/layout/UserMenu.tsx +439 -424
- package/apps/shared/components/mail/ConversationList.tsx +323 -323
- package/apps/shared/components/mail/ConversationThreadPane.tsx +613 -491
- package/apps/shared/components/mail/EmptyFolderBar.tsx +41 -0
- package/apps/shared/components/mail/MailSelectionBar.tsx +240 -227
- package/apps/shared/components/mail/MenuButton.tsx +31 -11
- package/apps/shared/components/mail/MessageDetailPane.tsx +1757 -1559
- package/apps/shared/components/mail/PermanentDeleteDialog.tsx +47 -0
- package/apps/shared/components/mail/compose/ComposeContext.tsx +312 -312
- package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -422
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +1681 -1677
- package/apps/shared/components/mail/compose/RecipientInput.tsx +54 -11
- package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -147
- package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -60
- package/apps/shared/components/mail/compose/quotedBody.ts +161 -161
- package/apps/shared/components/mail/layout/FolderBadgeChip.tsx +21 -0
- package/apps/shared/components/mail/layout/MailShell.tsx +161 -134
- package/apps/shared/components/mail/layout/SidebarSection.tsx +66 -0
- package/apps/shared/components/mail/reading/MessageMoreMenu.tsx +258 -0
- package/apps/shared/components/mail/reading/MessageSourceDialog.tsx +79 -0
- package/apps/shared/components/mail/reading/PendingMessageCard.tsx +68 -0
- package/apps/shared/components/mail/reading/messageExport.ts +59 -0
- package/apps/shared/components/mail/reading/printMessage.ts +99 -0
- package/apps/shared/components/mail/reading/useMessageActions.ts +443 -0
- package/apps/shared/components/mail/verificationSeals.ts +125 -125
- package/apps/shared/components/rules/RuleBuilder.tsx +311 -311
- package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -344
- package/apps/shared/components/settings/layout/SettingsShell.tsx +257 -254
- package/apps/shared/components/tasks/TasksSidebar.tsx +1 -1
- package/apps/shared/components/tasks/layout/TasksShell.tsx +219 -217
- package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -51
- package/apps/shared/keyboard/ShortcutProvider.tsx +62 -62
- package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -84
- package/apps/shared/keyboard/dispatch.ts +124 -124
- package/apps/shared/keyboard/format.ts +89 -89
- package/apps/shared/keyboard/keymap.ts +114 -114
- package/apps/shared/keyboard/registry.ts +65 -65
- package/apps/shared/keyboard/targets.ts +79 -79
- package/apps/shared/mail/folderCounts.ts +17 -0
- package/apps/shared/mail/folderOfType.ts +49 -0
- package/apps/shared/mail/folderTree.ts +143 -143
- package/apps/shared/mail/listAllPages.ts +39 -39
- package/apps/shared/mail/newMailNotifications.ts +171 -171
- package/apps/shared/mail/outbox/outgoingReplies.ts +192 -0
- package/apps/shared/mail/outbox/sendJob.ts +19 -1
- package/apps/shared/mail/outbox/sendOutcomes.ts +13 -4
- package/apps/shared/mail/permanentDelete.ts +160 -0
- package/apps/shared/mail/primaryMailbox.ts +71 -0
- package/apps/shared/mail/reportNotices.ts +66 -0
- package/apps/shared/mail/senderBlocking.ts +141 -0
- package/apps/shared/mail/useCollapsedSections.ts +146 -0
- package/apps/shared/mail/useMailConnection.ts +205 -203
- package/apps/shared/mail/useMailLiveUpdates.ts +277 -277
- package/apps/shared/mail/useMailboxUpdateAccess.ts +60 -0
- package/apps/shared/mail/useMarkMessageRead.ts +47 -47
- package/apps/shared/mail/useNewMailNotifications.ts +178 -178
- package/apps/shared/mail/usePermanentDelete.tsx +160 -0
- package/apps/shared/navigation/frameContext.tsx +1 -1
- package/apps/shared/navigation/index.ts +43 -0
- package/apps/shared/navigation/pageTitle.ts +19 -0
- package/apps/shared/notifications/store.ts +560 -560
- package/apps/shared/search/LocalIndexLifecycle.tsx +114 -114
- package/apps/shared/search/crossMailboxSearch.ts +169 -0
- package/apps/shared/search/localIndexBuilder.ts +481 -481
- package/apps/shared/signing/enrollmentStorage.ts +33 -33
- package/apps/shared/signing/enrollmentTracker.ts +385 -385
- package/apps/shared/signing/enrollmentView.ts +251 -251
- package/apps/shared/signing/useNow.ts +19 -19
- package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -90
- package/apps/shared/styles/app.css +396 -386
- package/apps/www/_shell.tsx +82 -0
- package/apps/www/calendar/index.tsx +581 -571
- package/apps/www/contacts/[uid].tsx +112 -109
- package/apps/www/contacts/index.tsx +14 -3
- package/apps/www/index.tsx +2962 -2479
- package/apps/www/messages/[uid].tsx +139 -135
- package/apps/www/settings/appearance/index.tsx +5 -2
- package/apps/www/settings/auto-reply/index.tsx +136 -133
- package/apps/www/settings/blocked-senders/index.tsx +303 -0
- package/apps/www/settings/encryption/index.tsx +1290 -1287
- package/apps/www/settings/filters/[uid].tsx +179 -176
- package/apps/www/settings/filters/index.tsx +105 -102
- package/apps/www/settings/filters/new/index.tsx +165 -140
- package/apps/www/settings/labels/index.tsx +207 -204
- package/apps/www/settings/privacy/index.tsx +495 -492
- package/apps/www/settings/profile/index.tsx +251 -248
- package/apps/www/settings/read-receipts/index.tsx +150 -147
- package/apps/www/settings/sharing/index.tsx +259 -256
- package/apps/www/settings/signatures/[uid].tsx +175 -172
- package/apps/www/settings/signatures/index.tsx +91 -88
- package/apps/www/settings/signatures/new/index.tsx +138 -135
- package/apps/www/tasks/index.tsx +654 -649
- package/dist/apps/admin/data-requests/index.js +1 -1
- package/dist/apps/admin/distribution-lists/new/index.js +3 -1
- package/dist/apps/admin/domains/[uid].js +3 -1
- package/dist/apps/admin/domains/new/index.js +3 -1
- package/dist/apps/admin/escrow-scopes/[uid].js +3 -1
- package/dist/apps/admin/escrow-scopes/new/index.js +3 -1
- package/dist/apps/admin/index.js +5 -2
- package/dist/apps/admin/mailboxes/[uid].js +15 -3
- package/dist/apps/admin/mailboxes/new/index.js +3 -1
- package/dist/apps/admin/transport-rules/new/index.js +3 -1
- package/dist/apps/escrow/matters/new/index.js +3 -1
- package/dist/apps/shared/components/admin/layout/AdminShell.js +6 -3
- package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.d.ts +35 -0
- package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +115 -0
- package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.d.ts +16 -0
- package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +92 -0
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +3 -3
- package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +25 -3
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +3 -1
- package/dist/apps/shared/components/calendar/EventEditor.js +5 -10
- package/dist/apps/shared/components/calendar/EventFormParts.d.ts +3 -2
- package/dist/apps/shared/components/calendar/EventFormParts.js +5 -16
- package/dist/apps/shared/components/calendar/GuestInput.d.ts +45 -0
- package/dist/apps/shared/components/calendar/GuestInput.js +30 -0
- package/dist/apps/shared/components/calendar/RequestChangeForm.js +11 -22
- package/dist/apps/shared/components/calendar/eventForm.d.ts +2 -2
- package/dist/apps/shared/components/calendar/eventFormat.d.ts +21 -8
- package/dist/apps/shared/components/calendar/eventFormat.js +50 -23
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +5 -4
- package/dist/apps/shared/components/contacts/ContactsSidebar.js +1 -1
- package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +3 -1
- package/dist/apps/shared/components/contacts/ContactsToolbar.js +4 -2
- package/dist/apps/shared/components/contacts/layout/ContactsShell.js +7 -5
- package/dist/apps/shared/components/layout/AppShell.d.ts +8 -11
- package/dist/apps/shared/components/layout/AppShell.js +24 -33
- package/dist/apps/shared/components/layout/FloatingActionButton.d.ts +13 -0
- package/dist/apps/shared/components/layout/FloatingActionButton.js +9 -0
- package/dist/apps/shared/components/layout/UserMenu.d.ts +4 -1
- package/dist/apps/shared/components/layout/UserMenu.js +6 -5
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +127 -22
- package/dist/apps/shared/components/mail/EmptyFolderBar.d.ts +18 -0
- package/dist/apps/shared/components/mail/EmptyFolderBar.js +10 -0
- package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +14 -4
- package/dist/apps/shared/components/mail/MailSelectionBar.js +11 -13
- package/dist/apps/shared/components/mail/MenuButton.d.ts +9 -2
- package/dist/apps/shared/components/mail/MenuButton.js +15 -11
- package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +5 -0
- package/dist/apps/shared/components/mail/MessageDetailPane.js +147 -7
- package/dist/apps/shared/components/mail/PermanentDeleteDialog.d.ts +19 -0
- package/dist/apps/shared/components/mail/PermanentDeleteDialog.js +11 -0
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +8 -4
- package/dist/apps/shared/components/mail/compose/RecipientInput.d.ts +21 -1
- package/dist/apps/shared/components/mail/compose/RecipientInput.js +18 -10
- package/dist/apps/shared/components/mail/layout/FolderBadgeChip.d.ts +6 -0
- package/dist/apps/shared/components/mail/layout/FolderBadgeChip.js +9 -0
- package/dist/apps/shared/components/mail/layout/MailShell.d.ts +16 -7
- package/dist/apps/shared/components/mail/layout/MailShell.js +78 -65
- package/dist/apps/shared/components/mail/layout/SidebarSection.d.ts +23 -0
- package/dist/apps/shared/components/mail/layout/SidebarSection.js +16 -0
- package/dist/apps/shared/components/mail/reading/MessageMoreMenu.d.ts +56 -0
- package/dist/apps/shared/components/mail/reading/MessageMoreMenu.js +153 -0
- package/dist/apps/shared/components/mail/reading/MessageSourceDialog.d.ts +27 -0
- package/dist/apps/shared/components/mail/reading/MessageSourceDialog.js +27 -0
- package/dist/apps/shared/components/mail/reading/PendingMessageCard.d.ts +14 -0
- package/dist/apps/shared/components/mail/reading/PendingMessageCard.js +16 -0
- package/dist/apps/shared/components/mail/reading/messageExport.d.ts +19 -0
- package/dist/apps/shared/components/mail/reading/messageExport.js +51 -0
- package/dist/apps/shared/components/mail/reading/printMessage.d.ts +30 -0
- package/dist/apps/shared/components/mail/reading/printMessage.js +70 -0
- package/dist/apps/shared/components/mail/reading/useMessageActions.d.ts +57 -0
- package/dist/apps/shared/components/mail/reading/useMessageActions.js +366 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +10 -7
- package/dist/apps/shared/components/tasks/TasksSidebar.js +1 -1
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +7 -5
- package/dist/apps/shared/keyboard/GlobalShortcuts.js +1 -1
- package/dist/apps/shared/mail/folderCounts.d.ts +10 -0
- package/dist/apps/shared/mail/folderCounts.js +12 -0
- package/dist/apps/shared/mail/folderOfType.d.ts +13 -0
- package/dist/apps/shared/mail/folderOfType.js +40 -0
- package/dist/apps/shared/mail/outbox/outgoingReplies.d.ts +80 -0
- package/dist/apps/shared/mail/outbox/outgoingReplies.js +131 -0
- package/dist/apps/shared/mail/outbox/sendJob.d.ts +5 -1
- package/dist/apps/shared/mail/outbox/sendJob.js +14 -1
- package/dist/apps/shared/mail/outbox/sendOutcomes.js +13 -4
- package/dist/apps/shared/mail/permanentDelete.d.ts +62 -0
- package/dist/apps/shared/mail/permanentDelete.js +127 -0
- package/dist/apps/shared/mail/primaryMailbox.d.ts +29 -0
- package/dist/apps/shared/mail/primaryMailbox.js +61 -0
- package/dist/apps/shared/mail/reportNotices.d.ts +21 -0
- package/dist/apps/shared/mail/reportNotices.js +47 -0
- package/dist/apps/shared/mail/senderBlocking.d.ts +26 -0
- package/dist/apps/shared/mail/senderBlocking.js +92 -0
- package/dist/apps/shared/mail/useCollapsedSections.d.ts +32 -0
- package/dist/apps/shared/mail/useCollapsedSections.js +110 -0
- package/dist/apps/shared/mail/useMailConnection.js +3 -1
- package/dist/apps/shared/mail/useMailboxUpdateAccess.d.ts +11 -0
- package/dist/apps/shared/mail/useMailboxUpdateAccess.js +52 -0
- package/dist/apps/shared/mail/usePermanentDelete.d.ts +35 -0
- package/dist/apps/shared/mail/usePermanentDelete.js +96 -0
- package/dist/apps/shared/navigation/frameContext.d.ts +1 -1
- package/dist/apps/shared/navigation/index.d.ts +16 -0
- package/dist/apps/shared/navigation/index.js +34 -0
- package/dist/apps/shared/navigation/pageTitle.d.ts +11 -0
- package/dist/apps/shared/navigation/pageTitle.js +12 -0
- package/dist/apps/shared/search/crossMailboxSearch.d.ts +72 -0
- package/dist/apps/shared/search/crossMailboxSearch.js +136 -0
- package/dist/apps/shared/styles/app.css +396 -386
- package/dist/apps/www/_shell.d.ts +26 -0
- package/dist/apps/www/_shell.js +31 -0
- package/dist/apps/www/calendar/index.d.ts +7 -2
- package/dist/apps/www/calendar/index.js +7 -4
- package/dist/apps/www/contacts/[uid].d.ts +14 -3
- package/dist/apps/www/contacts/[uid].js +5 -3
- package/dist/apps/www/contacts/index.d.ts +7 -2
- package/dist/apps/www/contacts/index.js +8 -4
- package/dist/apps/www/index.d.ts +7 -2
- package/dist/apps/www/index.js +542 -207
- package/dist/apps/www/messages/[uid].d.ts +14 -3
- package/dist/apps/www/messages/[uid].js +7 -5
- package/dist/apps/www/settings/appearance/index.d.ts +8 -2
- package/dist/apps/www/settings/appearance/index.js +4 -2
- package/dist/apps/www/settings/auto-reply/index.d.ts +7 -2
- package/dist/apps/www/settings/auto-reply/index.js +4 -2
- package/dist/apps/www/settings/blocked-senders/index.d.ts +9 -0
- package/dist/apps/www/settings/blocked-senders/index.js +134 -0
- package/dist/apps/www/settings/encryption/index.d.ts +7 -2
- package/dist/apps/www/settings/encryption/index.js +4 -2
- package/dist/apps/www/settings/filters/[uid].d.ts +7 -2
- package/dist/apps/www/settings/filters/[uid].js +4 -2
- package/dist/apps/www/settings/filters/index.d.ts +7 -2
- package/dist/apps/www/settings/filters/index.js +4 -2
- package/dist/apps/www/settings/filters/new/index.d.ts +7 -2
- package/dist/apps/www/settings/filters/new/index.js +28 -5
- package/dist/apps/www/settings/labels/index.d.ts +7 -2
- package/dist/apps/www/settings/labels/index.js +4 -2
- package/dist/apps/www/settings/privacy/index.d.ts +7 -2
- package/dist/apps/www/settings/privacy/index.js +4 -2
- package/dist/apps/www/settings/profile/index.d.ts +7 -2
- package/dist/apps/www/settings/profile/index.js +4 -2
- package/dist/apps/www/settings/read-receipts/index.d.ts +7 -2
- package/dist/apps/www/settings/read-receipts/index.js +4 -2
- package/dist/apps/www/settings/sharing/index.d.ts +7 -2
- package/dist/apps/www/settings/sharing/index.js +4 -2
- package/dist/apps/www/settings/signatures/[uid].d.ts +7 -2
- package/dist/apps/www/settings/signatures/[uid].js +4 -2
- package/dist/apps/www/settings/signatures/index.d.ts +7 -2
- package/dist/apps/www/settings/signatures/index.js +4 -2
- package/dist/apps/www/settings/signatures/new/index.d.ts +7 -2
- package/dist/apps/www/settings/signatures/new/index.js +5 -3
- package/dist/apps/www/tasks/index.d.ts +7 -2
- package/dist/apps/www/tasks/index.js +5 -3
- package/package.json +4 -2
- package/apps/shared/navigation/AppRouter.tsx +0 -300
- package/apps/shared/navigation/idle.ts +0 -45
- package/apps/shared/navigation/routerContext.tsx +0 -83
- package/apps/shared/navigation/routes.ts +0 -72
- package/apps/www/_routedPage.tsx +0 -24
- package/apps/www/_routes.ts +0 -37
- package/dist/apps/shared/navigation/AppRouter.d.ts +0 -52
- package/dist/apps/shared/navigation/AppRouter.js +0 -242
- package/dist/apps/shared/navigation/idle.d.ts +0 -14
- package/dist/apps/shared/navigation/idle.js +0 -43
- package/dist/apps/shared/navigation/routerContext.d.ts +0 -37
- package/dist/apps/shared/navigation/routerContext.js +0 -56
- package/dist/apps/shared/navigation/routes.d.ts +0 -32
- package/dist/apps/shared/navigation/routes.js +0 -37
- package/dist/apps/www/_routedPage.d.ts +0 -12
- package/dist/apps/www/_routedPage.js +0 -19
- package/dist/apps/www/_routes.d.ts +0 -11
- package/dist/apps/www/_routes.js +0 -31
|
@@ -173,5 +173,5 @@ function ErasureRequestsSection() {
|
|
|
173
173
|
setDenying(false);
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
|
-
return (_jsxs("section", { children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide text-text-muted mb-3", children: "Erasure requests" }),
|
|
176
|
+
return (_jsxs("section", { children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide text-text-muted mb-3", children: "Erasure requests" }), _jsxs("p", { className: "text-xs text-text-muted mb-3", children: ["A mailbox owner requests their own account's erasure and it is reviewed here. Erasing the data a", " ", _jsx("em", { children: "deleted" }), " mailbox left behind is started from the ", _jsx("a", { href: "/admin", className: "text-primary-dark hover:underline", children: "Mailboxes page" }), "; those requests are approved when filed and are marked \u201CDeleted mailbox data\u201D below."] }), list.loadError && _jsx(Alert, { children: list.loadError }), list.loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : list.items.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No erasure requests." })) : (_jsx("ul", { className: "flex flex-col gap-2", children: list.items.map((request) => (_jsxs("li", { className: "border border-border rounded-sm p-3", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("span", { className: "text-sm", children: [request.mailboxUid, " \u00B7 ", new Date(request.dateCreated).toLocaleString(), request.leftoverOnly && _jsx("span", { className: "text-text-muted", children: " \u00B7 Deleted mailbox data" }), request.status === "denied" && request.reason && (_jsxs("span", { className: "text-danger", children: [" \u2014 ", request.reason] })), request.status === "completed" && request.purgedCount !== undefined && (_jsxs("span", { className: "text-text-muted", children: [" \u2014 ", request.purgedCount, " records purged"] }))] }), _jsx("span", { className: `text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill ${statusBadgeClass(request.status)}`, children: statusLabel(request.status) })] }), request.status === "pending" && (_jsxs("div", { className: "flex gap-3 mt-2", children: [_jsx(Button, { type: "button", className: "!w-auto", onClick: () => setApproveTarget(request), children: "Approve" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setDenyTarget(request), children: "Deny" })] }))] }, request.uid))) })), _jsx(LoadMoreButton, { list: list, label: "Load more erasure requests" }), _jsxs(Modal, { open: approveTarget !== null, onClose: dismissApproveModal, title: "Approve erasure request", children: [_jsxs("p", { className: "text-sm mb-3", children: ["Approving permanently erases the mailbox ", _jsx("strong", { className: "break-all", children: approveTarget?.mailboxUid }), " ", "and everything in it - mail, contacts, calendars, tasks, and notes."] }), _jsx("p", { className: "text-sm font-semibold text-danger mb-4", children: "This is irreversible and cannot be undone." }), approveError && _jsx(Alert, { children: approveError }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: approving, onClick: closeApproveModal, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", loading: approving, disabled: approving, onClick: handleApprove, children: "Erase mailbox" })] })] }), _jsxs(Modal, { open: denyTarget !== null, onClose: dismissDenyModal, title: "Deny erasure request", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm mb-4", children: [_jsx("span", { className: "font-semibold", children: "Reason" }), _jsx("input", { "aria-label": "Denial reason", className: INPUT_CLASS, value: denyReason, onChange: (e) => setDenyReason(e.target.value) })] }), denyError && _jsx(Alert, { children: denyError }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: denying, onClick: closeDenyModal, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", loading: denying, disabled: denying || !denyReason.trim(), onClick: handleDeny, children: "Deny" })] })] })] }));
|
|
177
177
|
}
|
|
@@ -4,6 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
6
|
import { useState } from "react";
|
|
7
|
+
import { useRouter } from "@rapidrest/react/client";
|
|
7
8
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
9
|
import { createDistributionList } from "@rapidmx/react-shared/admin/distributionListsApi.js";
|
|
9
10
|
import AdminShell from "../../../shared/components/admin/layout/AdminShell.js";
|
|
@@ -15,6 +16,7 @@ export default function NewDistributionListPage(props) {
|
|
|
15
16
|
return (_jsx(AdminShell, { ...props, active: "distributionLists", children: _jsx(NewDistributionListForm, {}) }));
|
|
16
17
|
}
|
|
17
18
|
function NewDistributionListForm() {
|
|
19
|
+
const { navigate } = useRouter();
|
|
18
20
|
const [primarySmtpAddress, setPrimarySmtpAddress] = useState("");
|
|
19
21
|
const [name, setName] = useState("");
|
|
20
22
|
const [description, setDescription] = useState("");
|
|
@@ -38,7 +40,7 @@ function NewDistributionListForm() {
|
|
|
38
40
|
name: name.trim(),
|
|
39
41
|
description: description.trim() || undefined,
|
|
40
42
|
});
|
|
41
|
-
|
|
43
|
+
void navigate(`/admin/distribution-lists/${encodeURIComponent(list.uid)}`);
|
|
42
44
|
}
|
|
43
45
|
catch (err) {
|
|
44
46
|
setError(err instanceof ApiRequestError ? err.message : "Could not create the distribution list.");
|
|
@@ -4,6 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
6
|
import { useState } from "react";
|
|
7
|
+
import { useRouter } from "@rapidrest/react/client";
|
|
7
8
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
9
|
import { deleteDomain, updateDomain } from "@rapidmx/react-shared/admin/domainsApi.js";
|
|
9
10
|
import AdminShell from "../../shared/components/admin/layout/AdminShell.js";
|
|
@@ -17,6 +18,7 @@ export default function DomainDetailPage(props) {
|
|
|
17
18
|
return (_jsx(AdminShell, { ...props, active: "domains", children: _jsx(DomainDetailContent, { uid: props.params.uid }) }));
|
|
18
19
|
}
|
|
19
20
|
function DomainDetailContent({ uid }) {
|
|
21
|
+
const { navigate } = useRouter();
|
|
20
22
|
const [domain, setDomain] = useState(null);
|
|
21
23
|
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
|
22
24
|
const [deleting, setDeleting] = useState(false);
|
|
@@ -39,7 +41,7 @@ function DomainDetailContent({ uid }) {
|
|
|
39
41
|
setDeleteError(null);
|
|
40
42
|
try {
|
|
41
43
|
await deleteDomain(domain.uid, domain.version);
|
|
42
|
-
|
|
44
|
+
void navigate("/admin/domains");
|
|
43
45
|
}
|
|
44
46
|
catch (err) {
|
|
45
47
|
setDeleteError(err instanceof ApiRequestError ? err.message : "Could not delete this domain.");
|
|
@@ -4,6 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
6
|
import { useEffect, useState } from "react";
|
|
7
|
+
import { useRouter } from "@rapidrest/react/client";
|
|
7
8
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
9
|
import { createDomain, listDomains } from "@rapidmx/react-shared/admin/domainsApi.js";
|
|
9
10
|
import AdminShell from "../../../shared/components/admin/layout/AdminShell.js";
|
|
@@ -15,6 +16,7 @@ export default function NewDomainPage(props) {
|
|
|
15
16
|
return (_jsx(AdminShell, { ...props, active: "domains", children: _jsx(NewDomainForm, {}) }));
|
|
16
17
|
}
|
|
17
18
|
function NewDomainForm() {
|
|
19
|
+
const { navigate } = useRouter();
|
|
18
20
|
const [name, setName] = useState("");
|
|
19
21
|
const [aliasOf, setAliasOf] = useState("");
|
|
20
22
|
// Only an existing, non-alias domain can be aliased (no chains) - see `Domain.aliasOf`'s own doc comment
|
|
@@ -37,7 +39,7 @@ function NewDomainForm() {
|
|
|
37
39
|
setSaving(true);
|
|
38
40
|
try {
|
|
39
41
|
const domain = await createDomain({ name: name.trim(), aliasOf: aliasOf || undefined });
|
|
40
|
-
|
|
42
|
+
void navigate(`/admin/domains/${encodeURIComponent(domain.uid)}`);
|
|
41
43
|
}
|
|
42
44
|
catch (err) {
|
|
43
45
|
setError(err instanceof ApiRequestError ? err.message : "Could not create the domain.");
|
|
@@ -4,6 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
6
|
import { useEffect, useState } from "react";
|
|
7
|
+
import { useRouter } from "@rapidrest/react/client";
|
|
7
8
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
9
|
import { toDatetimeLocal } from "@rapidmx/react-shared/util/dateInput.js";
|
|
9
10
|
import { deleteEscrowScope, getEscrowScope, updateEscrowScope, } from "@rapidmx/react-shared/admin/escrowScopesApi.js";
|
|
@@ -95,6 +96,7 @@ export default function EscrowScopeDetailPage(props) {
|
|
|
95
96
|
return (_jsx(AdminShell, { ...props, active: "escrowScopes", children: _jsx(EscrowScopeDetailContent, { uid: props.params.uid, adminUid: props.userUid }) }));
|
|
96
97
|
}
|
|
97
98
|
function EscrowScopeDetailContent({ uid, adminUid }) {
|
|
99
|
+
const { navigate } = useRouter();
|
|
98
100
|
const [original, setOriginal] = useState(null);
|
|
99
101
|
const [name, setName] = useState("");
|
|
100
102
|
const [description, setDescription] = useState("");
|
|
@@ -198,7 +200,7 @@ function EscrowScopeDetailContent({ uid, adminUid }) {
|
|
|
198
200
|
setDeleteError(null);
|
|
199
201
|
try {
|
|
200
202
|
await deleteEscrowScope(original.uid, original.version);
|
|
201
|
-
|
|
203
|
+
void navigate("/admin/escrow-scopes");
|
|
202
204
|
}
|
|
203
205
|
catch (err) {
|
|
204
206
|
// Most commonly a 409 ("referenced by an existing Matter") — see `deleteEscrowScope()`'s own
|
|
@@ -4,6 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
6
|
import { useState } from "react";
|
|
7
|
+
import { useRouter } from "@rapidrest/react/client";
|
|
7
8
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
9
|
import { createEscrowScope } from "@rapidmx/react-shared/admin/escrowScopesApi.js";
|
|
9
10
|
import AdminShell from "../../../shared/components/admin/layout/AdminShell.js";
|
|
@@ -16,6 +17,7 @@ export default function NewEscrowScopePage(props) {
|
|
|
16
17
|
return (_jsx(AdminShell, { ...props, active: "escrowScopes", children: _jsx(NewEscrowScopeForm, { adminUid: props.userUid }) }));
|
|
17
18
|
}
|
|
18
19
|
function NewEscrowScopeForm({ adminUid }) {
|
|
20
|
+
const { navigate } = useRouter();
|
|
19
21
|
const [name, setName] = useState("");
|
|
20
22
|
const [description, setDescription] = useState("");
|
|
21
23
|
const [keyAndHolders, setKeyAndHolders] = useState(emptyEscrowScopeKeyAndHoldersValue());
|
|
@@ -61,7 +63,7 @@ function NewEscrowScopeForm({ adminUid }) {
|
|
|
61
63
|
requiredHolders: keyAndHolders.requiredHolders,
|
|
62
64
|
notifySubjectOnAccess: keyAndHolders.notifySubjectOnAccess,
|
|
63
65
|
});
|
|
64
|
-
|
|
66
|
+
void navigate(`/admin/escrow-scopes/${encodeURIComponent(created.uid)}`);
|
|
65
67
|
}
|
|
66
68
|
catch (err) {
|
|
67
69
|
setError(err instanceof ApiRequestError ? err.message : "Could not create the escrow scope.");
|
package/dist/apps/admin/index.js
CHANGED
|
@@ -4,10 +4,12 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
6
|
import { useEffect, useState } from "react";
|
|
7
|
+
import { useRouter } from "@rapidrest/react/client";
|
|
7
8
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
9
|
import { listMailboxes } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
9
10
|
import { reopenSetup } from "@rapidmx/react-shared/admin/setupApi.js";
|
|
10
11
|
import AdminShell from "../shared/components/admin/layout/AdminShell.js";
|
|
12
|
+
import LeftoverMailboxesSection from "../shared/components/admin/mailboxes/LeftoverMailboxesSection.js";
|
|
11
13
|
import MailboxTable from "../shared/components/admin/mailboxes/MailboxTable.js";
|
|
12
14
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
13
15
|
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
@@ -17,6 +19,7 @@ export default function MailboxesListPage(props) {
|
|
|
17
19
|
return (_jsx(AdminShell, { ...props, active: "mailboxes", children: _jsx(MailboxesListContent, {}) }));
|
|
18
20
|
}
|
|
19
21
|
function MailboxesListContent() {
|
|
22
|
+
const { navigate } = useRouter();
|
|
20
23
|
const [page, setPage] = useState(0);
|
|
21
24
|
const [mailboxes, setMailboxes] = useState([]);
|
|
22
25
|
const [loading, setLoading] = useState(true);
|
|
@@ -37,7 +40,7 @@ function MailboxesListContent() {
|
|
|
37
40
|
setReopening(true);
|
|
38
41
|
try {
|
|
39
42
|
await reopenSetup();
|
|
40
|
-
|
|
43
|
+
void navigate("/admin/setup");
|
|
41
44
|
}
|
|
42
45
|
catch (err) {
|
|
43
46
|
setError(err instanceof ApiRequestError ? err.message : "Could not reopen setup.");
|
|
@@ -45,5 +48,5 @@ function MailboxesListContent() {
|
|
|
45
48
|
setConfirmingSetup(false);
|
|
46
49
|
}
|
|
47
50
|
}
|
|
48
|
-
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex items-center justify-between mb-5", children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide", children: "Mailboxes" }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: reopening, onClick: () => setConfirmingSetup(true), children: "Run setup again" }), _jsx("a", { href: "/admin/mailboxes/new", children: _jsx(Button, { type: "button", className: "!w-auto", children: "+ New mailbox" }) })] })] }), _jsx("p", { className: "text-sm text-text-muted mb-4", children: "Administrative details only (addresses, owners, quota). The console never shows a mailbox\u2019s mail: to see someone\u2019s account, impersonate them from their mailbox page." }), error && _jsx(Alert, { children: error }), _jsxs(Modal, { open: confirmingSetup, onClose: () => setConfirmingSetup(false), title: "Run setup again?", children: [_jsx("p", { className: "text-sm mb-4", children: "Setup starts again from its first step, and the admin console sends administrators to it until it's finished. Your existing settings, domains and mailboxes are kept." }), _jsxs("div", { className: "flex gap-2 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setConfirmingSetup(false), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", loading: reopening, disabled: reopening, onClick: () => void handleRunSetup(), children: "Run setup" })] })] }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : (_jsx(MailboxTable, { mailboxes: mailboxes })), _jsxs("div", { className: "flex gap-3 items-center mt-4", children: [_jsx(Button, { variant: "secondary", type: "button", className: "!w-auto", disabled: page === 0 || loading, onClick: () => setPage((p) => Math.max(0, p - 1)), children: "Previous" }), _jsxs("span", { className: "text-sm text-text-muted", children: ["Page ", page + 1] }), _jsx(Button, { variant: "secondary", type: "button", className: "!w-auto", disabled: !hasNextPage || loading, onClick: () => setPage((p) => p + 1), children: "Next" })] })] }));
|
|
51
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex items-center justify-between mb-5", children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide", children: "Mailboxes" }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: reopening, onClick: () => setConfirmingSetup(true), children: "Run setup again" }), _jsx("a", { href: "/admin/mailboxes/new", children: _jsx(Button, { type: "button", className: "!w-auto", children: "+ New mailbox" }) })] })] }), _jsx("p", { className: "text-sm text-text-muted mb-4", children: "Administrative details only (addresses, owners, quota). The console never shows a mailbox\u2019s mail: to see someone\u2019s account, impersonate them from their mailbox page." }), error && _jsx(Alert, { children: error }), _jsxs(Modal, { open: confirmingSetup, onClose: () => setConfirmingSetup(false), title: "Run setup again?", children: [_jsx("p", { className: "text-sm mb-4", children: "Setup starts again from its first step, and the admin console sends administrators to it until it's finished. Your existing settings, domains and mailboxes are kept." }), _jsxs("div", { className: "flex gap-2 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setConfirmingSetup(false), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", loading: reopening, disabled: reopening, onClick: () => void handleRunSetup(), children: "Run setup" })] })] }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : (_jsx(MailboxTable, { mailboxes: mailboxes })), _jsxs("div", { className: "flex gap-3 items-center mt-4", children: [_jsx(Button, { variant: "secondary", type: "button", className: "!w-auto", disabled: page === 0 || loading, onClick: () => setPage((p) => Math.max(0, p - 1)), children: "Previous" }), _jsxs("span", { className: "text-sm text-text-muted", children: ["Page ", page + 1] }), _jsx(Button, { variant: "secondary", type: "button", className: "!w-auto", disabled: !hasNextPage || loading, onClick: () => setPage((p) => p + 1), children: "Next" })] }), _jsx(LeftoverMailboxesSection, {})] }));
|
|
49
52
|
}
|
|
@@ -4,6 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
6
|
import { useEffect, useState } from "react";
|
|
7
|
+
import { useRouter } from "@rapidrest/react/client";
|
|
7
8
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
9
|
import { deleteMailbox, getMailbox, impersonateUser } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
9
10
|
import AdminShell from "../../shared/components/admin/layout/AdminShell.js";
|
|
@@ -27,6 +28,7 @@ export default function MailboxDetailPage(props) {
|
|
|
27
28
|
}
|
|
28
29
|
function MailboxDetailContent({ uid, impersonationBaseUrl, currentUserUid, }) {
|
|
29
30
|
const [mailbox, setMailbox] = useState(null);
|
|
31
|
+
const { navigate } = useRouter();
|
|
30
32
|
const [loading, setLoading] = useState(true);
|
|
31
33
|
const [error, setError] = useState(null);
|
|
32
34
|
const [confirmingAccess, setConfirmingAccess] = useState(false);
|
|
@@ -35,6 +37,9 @@ function MailboxDetailContent({ uid, impersonationBaseUrl, currentUserUid, }) {
|
|
|
35
37
|
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
|
36
38
|
const [deleting, setDeleting] = useState(false);
|
|
37
39
|
const [deleteError, setDeleteError] = useState(null);
|
|
40
|
+
// Deleting keeps the mailbox's data; this also erases it in the same step, after the address is typed to confirm.
|
|
41
|
+
const [eraseData, setEraseData] = useState(false);
|
|
42
|
+
const [typedAddress, setTypedAddress] = useState("");
|
|
38
43
|
// Ignored while the action is under way (Escape/Close; Cancel is disabled then too), so its result or error
|
|
39
44
|
// can't land on a modal reopened afterwards.
|
|
40
45
|
function closeAccessModal() {
|
|
@@ -61,6 +66,9 @@ function MailboxDetailContent({ uid, impersonationBaseUrl, currentUserUid, }) {
|
|
|
61
66
|
if (deleting)
|
|
62
67
|
return;
|
|
63
68
|
setConfirmingDelete(false);
|
|
69
|
+
setEraseData(false);
|
|
70
|
+
setTypedAddress("");
|
|
71
|
+
setDeleteError(null);
|
|
64
72
|
}
|
|
65
73
|
// Only ever invoked from the delete-confirmation modal below, which itself only renders once
|
|
66
74
|
// `mailbox` is resolved (the `error || !mailbox` branch below returns before this content mounts).
|
|
@@ -68,8 +76,9 @@ function MailboxDetailContent({ uid, impersonationBaseUrl, currentUserUid, }) {
|
|
|
68
76
|
setDeleting(true);
|
|
69
77
|
setDeleteError(null);
|
|
70
78
|
try {
|
|
71
|
-
await deleteMailbox(mailbox.uid, mailbox.version);
|
|
72
|
-
|
|
79
|
+
await deleteMailbox(mailbox.uid, mailbox.version, { erase: eraseData });
|
|
80
|
+
// The Mailboxes page it goes to lists what a delete without `erase` kept, and how far an erasure has got.
|
|
81
|
+
void navigate("/admin");
|
|
73
82
|
}
|
|
74
83
|
catch (err) {
|
|
75
84
|
// Most commonly a 409 if this mailbox is a custodian on an open legal hold (restapi's own
|
|
@@ -98,5 +107,8 @@ function MailboxDetailContent({ uid, impersonationBaseUrl, currentUserUid, }) {
|
|
|
98
107
|
const ingestQueueHref = `/admin/ingest-queue?mailboxUid=${encodeURIComponent(mailbox.uid)}`;
|
|
99
108
|
return (_jsxs("div", { className: "max-w-3xl flex flex-col gap-5", children: [_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { children: [_jsx("a", { href: "/admin", className: "text-sm text-primary-dark hover:underline", children: "\u2190 All mailboxes" }), _jsx("h1", { className: "text-xl font-bold tracking-tight mt-1", children: mailbox.primarySmtpAddress })] }), _jsxs("div", { className: "flex gap-3 shrink-0", children: [mailbox.ownerUserUid && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setConfirmingAccess(true), children: "Impersonate this user" })), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto !border-danger !text-danger hover:!border-danger hover:!text-danger", onClick: () => setConfirmingDelete(true), children: "Delete mailbox" })] })] }), _jsxs("p", { className: "text-sm text-text-muted", children: ["The console shows administrative details only - never a mailbox\u2019s mail or settings.", mailbox.ownerUserUid
|
|
100
109
|
? " To see this mailbox as its owner sees it, impersonate them: that is recorded, and you can stop at any time."
|
|
101
|
-
: " This shared mailbox has no owner: add yourself under Shared access to open it in the mail client."] }), _jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsxs("dl", { className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3 text-sm", children: [_jsx("dt", { className: "text-text-muted", children: "Display name" }), _jsx("dd", { children: mailbox.displayName }), _jsx("dt", { className: "text-text-muted", children: "Owner" }), _jsx("dd", { children: mailbox.ownerUserUid ?? "None (shared mailbox)" }), _jsx("dt", { className: "text-text-muted", children: "Timezone" }), _jsx("dd", { children: mailbox.timezone }), _jsx("dt", { className: "text-text-muted", children: "Quota" }), _jsxs("dd", { children: [formatBytes(mailbox.usedBytes), " / ", formatBytes(mailbox.quotaBytes)] }), _jsx("dt", { className: "text-text-muted", children: "Alias addresses" }), _jsx("dd", { children: mailbox.aliasAddresses.length > 0 ? mailbox.aliasAddresses.join(", ") : "None" }), mailbox.isResource && (_jsxs(_Fragment, { children: [_jsx("dt", { className: "text-text-muted", children: "Resource type" }), _jsx("dd", { className: "capitalize", children: mailbox.resourceType ?? "room" })] })), _jsx("dt", { className: "text-text-muted", children: "Created" }), _jsx("dd", { children: new Date(mailbox.dateCreated).toLocaleString() })] }), _jsxs("div", { className: "flex gap-4 mt-5 pt-5 border-t border-border text-sm font-medium", children: [_jsx("a", { href: quarantineHref, className: "text-primary-dark hover:underline", children: "View quarantine" }), _jsx("a", { href: ingestQueueHref, className: "text-primary-dark hover:underline", children: "View ingest queue" })] })] }), _jsx(ShareAccessCard, { mailboxUid: mailbox.uid, ownerUserUid: mailbox.ownerUserUid, currentUserUid: currentUserUid }), _jsx(EscrowScopeCard, { mailbox: mailbox, onUpdate: setMailbox }), mailbox.isResource && _jsx(ResourceSettingsCard, { mailbox: mailbox, onUpdate: setMailbox }), _jsxs(Modal, { open: confirmingAccess, onClose: closeAccessModal, title: "Impersonate this user", children: [_jsxs("p", { className: "text-sm mb-3", children: ["You\u2019ll be signed in as ", _jsx("strong", { className: "break-all", children: mailbox.ownerUserUid }), ", the owner of ", _jsx("strong", { className: "break-all", children: mailbox.primarySmtpAddress }), ", and see everything they can - and act as them, until you stop impersonating. This is the only way an administrator sees another user\u2019s mail."] }), accessError && _jsx(Alert, { children: accessError }), _jsxs("div", { className: "flex gap-3 justify-end mt-5", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: impersonating, onClick: closeAccessModal, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", loading: impersonating, disabled: impersonating, onClick: handleAccessMailbox, children: "Impersonate" })] })] }), _jsxs(Modal, { open: confirmingDelete, onClose: closeDeleteModal, title: "Delete mailbox", children: [_jsxs("p", { className: "text-sm mb-
|
|
110
|
+
: " This shared mailbox has no owner: add yourself under Shared access to open it in the mail client."] }), _jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsxs("dl", { className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3 text-sm", children: [_jsx("dt", { className: "text-text-muted", children: "Display name" }), _jsx("dd", { children: mailbox.displayName }), _jsx("dt", { className: "text-text-muted", children: "Owner" }), _jsx("dd", { children: mailbox.ownerUserUid ?? "None (shared mailbox)" }), _jsx("dt", { className: "text-text-muted", children: "Timezone" }), _jsx("dd", { children: mailbox.timezone }), _jsx("dt", { className: "text-text-muted", children: "Quota" }), _jsxs("dd", { children: [formatBytes(mailbox.usedBytes), " / ", formatBytes(mailbox.quotaBytes)] }), _jsx("dt", { className: "text-text-muted", children: "Alias addresses" }), _jsx("dd", { children: mailbox.aliasAddresses.length > 0 ? mailbox.aliasAddresses.join(", ") : "None" }), mailbox.isResource && (_jsxs(_Fragment, { children: [_jsx("dt", { className: "text-text-muted", children: "Resource type" }), _jsx("dd", { className: "capitalize", children: mailbox.resourceType ?? "room" })] })), _jsx("dt", { className: "text-text-muted", children: "Created" }), _jsx("dd", { children: new Date(mailbox.dateCreated).toLocaleString() })] }), _jsxs("div", { className: "flex gap-4 mt-5 pt-5 border-t border-border text-sm font-medium", children: [_jsx("a", { href: quarantineHref, className: "text-primary-dark hover:underline", children: "View quarantine" }), _jsx("a", { href: ingestQueueHref, className: "text-primary-dark hover:underline", children: "View ingest queue" })] })] }), _jsx(ShareAccessCard, { mailboxUid: mailbox.uid, ownerUserUid: mailbox.ownerUserUid, currentUserUid: currentUserUid }), _jsx(EscrowScopeCard, { mailbox: mailbox, onUpdate: setMailbox }), mailbox.isResource && _jsx(ResourceSettingsCard, { mailbox: mailbox, onUpdate: setMailbox }), _jsxs(Modal, { open: confirmingAccess, onClose: closeAccessModal, title: "Impersonate this user", children: [_jsxs("p", { className: "text-sm mb-3", children: ["You\u2019ll be signed in as ", _jsx("strong", { className: "break-all", children: mailbox.ownerUserUid }), ", the owner of ", _jsx("strong", { className: "break-all", children: mailbox.primarySmtpAddress }), ", and see everything they can - and act as them, until you stop impersonating. This is the only way an administrator sees another user\u2019s mail."] }), accessError && _jsx(Alert, { children: accessError }), _jsxs("div", { className: "flex gap-3 justify-end mt-5", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: impersonating, onClick: closeAccessModal, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", loading: impersonating, disabled: impersonating, onClick: handleAccessMailbox, children: "Impersonate" })] })] }), _jsxs(Modal, { open: confirmingDelete, onClose: closeDeleteModal, title: "Delete mailbox", children: [_jsxs("p", { className: "text-sm mb-3", children: ["Are you sure you want to delete ", _jsx("strong", { className: "break-all", children: mailbox.primarySmtpAddress }), "? This cannot be undone. It fails if this mailbox is a custodian on an active legal hold."] }), _jsxs("p", { className: "text-sm mb-3", children: ["Deleting removes the mailbox itself. Its mail, contacts, calendars and everything else in it are", " ", _jsx("strong", { children: "kept" }), " until they are erased, and until then the address can\u2019t be used for a new mailbox. You can erase them later from the Mailboxes page."] }), _jsxs("label", { className: "flex items-start gap-2 text-sm mb-3", children: [_jsx("input", { type: "checkbox", className: "mt-0.5", checked: eraseData, disabled: deleting, onChange: (e) => {
|
|
111
|
+
setEraseData(e.target.checked);
|
|
112
|
+
setTypedAddress("");
|
|
113
|
+
} }), _jsx("span", { children: "Also erase all of its data now (permanent)" })] }), eraseData && (_jsxs("div", { className: "mb-3", children: [_jsx("p", { className: "text-sm font-semibold text-danger mb-2", children: "Everything in this mailbox will be permanently erased, and this cannot be undone." }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsxs("span", { className: "font-semibold", children: ["Type ", _jsx("span", { className: "break-all", children: mailbox.primarySmtpAddress }), " to confirm"] }), _jsx("input", { "aria-label": "Type the address to confirm", className: "w-full text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary", value: typedAddress, disabled: deleting, autoComplete: "off", spellCheck: false, onChange: (e) => setTypedAddress(e.target.value) })] })] })), deleteError && _jsx(Alert, { children: deleteError }), _jsxs("div", { className: "flex gap-3 justify-end mt-5", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: deleting, onClick: closeDeleteModal, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", loading: deleting, disabled: deleting || (eraseData && typedAddress.trim().toLowerCase() !== mailbox.primarySmtpAddress.trim().toLowerCase()), onClick: handleDelete, children: eraseData ? "Delete and erase all its data" : "Delete" })] })] })] }));
|
|
102
114
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRouter } from "@rapidrest/react/client";
|
|
2
3
|
import AdminShell from "../../../shared/components/admin/layout/AdminShell.js";
|
|
3
4
|
import MailboxCreateForm from "../../../shared/components/admin/settings/MailboxCreateForm.js";
|
|
4
5
|
export default function NewMailboxPage(props) {
|
|
6
|
+
const { navigate } = useRouter();
|
|
5
7
|
return (_jsx(AdminShell, { ...props, active: "mailboxes", children: _jsxs("div", { className: "max-w-xl", children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide mb-1", children: "New mailbox" }), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "Choose \"Shared mailbox\" to create a true ownerless mailbox (the Exchange \"shared mailbox\" concept) \u2014 access is then granted entirely to delegates afterward, not to a single owner. Choose \"Owned by a specific person\" and look them up to create a mailbox for a specific user instead." }), _jsx(MailboxCreateForm, { cancelHref: "/admin", onCreated: (mailbox) => {
|
|
6
|
-
|
|
8
|
+
void navigate(`/admin/mailboxes/${encodeURIComponent(mailbox.uid)}`);
|
|
7
9
|
} })] }) }));
|
|
8
10
|
}
|
|
@@ -4,6 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
6
|
import { useState } from "react";
|
|
7
|
+
import { useRouter } from "@rapidrest/react/client";
|
|
7
8
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
9
|
import { createTransportRule, } from "@rapidmx/react-shared/admin/transportRulesApi.js";
|
|
9
10
|
import AdminShell from "../../../shared/components/admin/layout/AdminShell.js";
|
|
@@ -17,6 +18,7 @@ export default function NewTransportRulePage(props) {
|
|
|
17
18
|
return (_jsx(AdminShell, { ...props, active: "transportRules", children: _jsx(NewTransportRuleForm, {}) }));
|
|
18
19
|
}
|
|
19
20
|
function NewTransportRuleForm() {
|
|
21
|
+
const { navigate } = useRouter();
|
|
20
22
|
const [name, setName] = useState("");
|
|
21
23
|
const [rule, setRule] = useState({
|
|
22
24
|
enabled: true,
|
|
@@ -53,7 +55,7 @@ function NewTransportRuleForm() {
|
|
|
53
55
|
setSaving(true);
|
|
54
56
|
try {
|
|
55
57
|
const created = await createTransportRule({ name: name.trim(), ...rule });
|
|
56
|
-
|
|
58
|
+
void navigate(`/admin/transport-rules/${encodeURIComponent(created.uid)}`);
|
|
57
59
|
}
|
|
58
60
|
catch (err) {
|
|
59
61
|
setError(err instanceof ApiRequestError ? err.message : "Could not create the transport rule.");
|
|
@@ -4,6 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
6
|
import { useState } from "react";
|
|
7
|
+
import { useRouter } from "@rapidrest/react/client";
|
|
7
8
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
9
|
import { toDatetimeLocal } from "@rapidmx/react-shared/util/dateInput.js";
|
|
9
10
|
import { createMatter } from "@rapidmx/react-shared/admin/mattersApi.js";
|
|
@@ -17,6 +18,7 @@ export default function NewMatterPage(props) {
|
|
|
17
18
|
return (_jsx(EscrowShell, { ...props, active: "matters", children: _jsx(NewMatterForm, {}) }));
|
|
18
19
|
}
|
|
19
20
|
function NewMatterForm() {
|
|
21
|
+
const { navigate } = useRouter();
|
|
20
22
|
const [name, setName] = useState("");
|
|
21
23
|
const [description, setDescription] = useState("");
|
|
22
24
|
// No dropdown here: `EscrowScope` (BaseEscrowScopeRoute) is trusted-admin-only end to end, so this
|
|
@@ -62,7 +64,7 @@ function NewMatterForm() {
|
|
|
62
64
|
dateRangeStart: new Date(dateRangeStart).toISOString(),
|
|
63
65
|
dateRangeEnd: new Date(dateRangeEnd).toISOString(),
|
|
64
66
|
});
|
|
65
|
-
|
|
67
|
+
void navigate(`/escrow/matters/${encodeURIComponent(created.uid)}`);
|
|
66
68
|
}
|
|
67
69
|
catch (err) {
|
|
68
70
|
setError(err instanceof ApiRequestError ? err.message : "Could not create the matter.");
|
|
@@ -5,6 +5,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
6
|
import "../../../styles/app.css";
|
|
7
7
|
import { useEffect, useState } from "react";
|
|
8
|
+
import { useRouter } from "@rapidrest/react/client";
|
|
8
9
|
import { HiOutlineClipboardDocumentList, HiOutlineClock, HiOutlineDocumentArrowDown, HiOutlineDocumentCheck, HiOutlineGlobeAlt, HiOutlineInboxStack, HiOutlineKey, HiOutlineLockClosed, HiOutlinePaintBrush, HiOutlinePuzzlePiece, HiOutlineQueueList, HiOutlineRocketLaunch, HiOutlineShieldCheck, HiOutlineShieldExclamation, HiOutlineUserGroup, HiOutlineWrenchScrewdriver, HiOutlineBars3, } from "react-icons/hi2";
|
|
9
10
|
import { apiFetch, ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
10
11
|
import { getSetupStatus } from "@rapidmx/react-shared/admin/setupApi.js";
|
|
@@ -108,6 +109,7 @@ export function adminNavItems(pluginNav) {
|
|
|
108
109
|
* 403 `api-103` (elevated, but not an administrator), any other 403, and 401 mean "no administrator access".
|
|
109
110
|
*/
|
|
110
111
|
export default function AdminShell({ active, userUid, authServerUrl, pluginNav, children }) {
|
|
112
|
+
const { navigate } = useRouter();
|
|
111
113
|
const [status, setStatus] = useState("checking");
|
|
112
114
|
const [error, setError] = useState(null);
|
|
113
115
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
@@ -129,7 +131,8 @@ export default function AdminShell({ active, userUid, authServerUrl, pluginNav,
|
|
|
129
131
|
if (active !== "setup") {
|
|
130
132
|
try {
|
|
131
133
|
if ((await getSetupStatus()).required) {
|
|
132
|
-
|
|
134
|
+
// Replacing this page in the history, which would otherwise send the back button round again.
|
|
135
|
+
void navigate(SETUP_ITEM.href, { replace: true });
|
|
133
136
|
return;
|
|
134
137
|
}
|
|
135
138
|
}
|
|
@@ -199,10 +202,10 @@ export default function AdminShell({ active, userUid, authServerUrl, pluginNav,
|
|
|
199
202
|
id === active
|
|
200
203
|
? "bg-primary/10 text-primary-dark"
|
|
201
204
|
: "text-text-muted hover:bg-surface-alt hover:text-text",
|
|
202
|
-
].join(" "), children: _jsx(Icon, { size: 20, "aria-hidden": "true" }) }, id)))] }), _jsx(Drawer, { open: menuOpen, onClose: () => setMenuOpen(false), title: "Admin", children: _jsx("nav", { "aria-label": "Admin menu", className: "flex flex-col gap-1 -mx-2", children: navItems.map(({ id, href, label, icon: Icon }) => (_jsxs("a", { href: href, "aria-current": id === active ? "page" : undefined, onClick: () => setMenuOpen(false), className: [
|
|
205
|
+
].join(" "), children: _jsx(Icon, { size: 20, "aria-hidden": "true" }) }, id)))] }), _jsx(Drawer, { open: menuOpen, onClose: () => setMenuOpen(false), title: "Admin", fullScreen: true, children: _jsx("nav", { "aria-label": "Admin menu", className: "flex flex-col gap-1 -mx-2", children: navItems.map(({ id, href, label, icon: Icon }) => (_jsxs("a", { href: href, "aria-current": id === active ? "page" : undefined, onClick: () => setMenuOpen(false), className: [
|
|
203
206
|
"flex items-center gap-3 px-3 py-2.5 rounded-sm text-sm font-medium",
|
|
204
207
|
id === active ? "bg-primary/10 text-primary-dark" : "text-text hover:bg-surface-alt",
|
|
205
|
-
].join(" "), children: [_jsx(Icon, { size: 20, "aria-hidden": "true" }), label] }, id))) }) }), _jsxs("div", { className: "flex-1 flex flex-col min-w-0", children: [_jsxs("header", { className: "rr-solid sticky top-0 z-30 h-16 shrink-0 bg-surface border-b border-border flex items-center justify-between gap-4 px-4 md:px-6", children: [_jsxs("div", { className: "flex items-center gap-2 min-w-0", children: [_jsx("button", { type: "button", className: "md:hidden shrink-0 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open menu", onClick: () => setMenuOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx("span", { className: "font-display font-bold text-lg uppercase tracking-wide truncate", children: activeItem?.label })] }), _jsx(UserMenu, { userUid: userUid, authServerUrl: authServerUrl, onSignOut: handleSignOut })] }), _jsx("div", { id: "app-content", className: "flex-1", children: _jsx("main", { className: "max-w-6xl mx-auto px-6 py-8", children: children }) })] })] }) }));
|
|
208
|
+
].join(" "), children: [_jsx(Icon, { size: 20, "aria-hidden": "true" }), label] }, id))) }) }), _jsxs("div", { className: "flex-1 flex flex-col min-w-0", children: [_jsxs("header", { className: "rr-solid sticky top-0 z-30 h-16 shrink-0 bg-surface border-b border-border flex items-center justify-between gap-4 px-4 md:px-6", children: [_jsxs("div", { className: "flex items-center gap-2 min-w-0", children: [_jsx("button", { type: "button", className: "md:hidden shrink-0 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open menu", onClick: () => setMenuOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx("span", { className: "font-display font-bold text-lg uppercase tracking-wide truncate", children: activeItem?.label })] }), _jsx(UserMenu, { userUid: userUid, authServerUrl: authServerUrl, onSignOut: handleSignOut, showMailLink: true })] }), _jsx("div", { id: "app-content", className: "flex-1", children: _jsx("main", { className: "max-w-6xl mx-auto px-6 py-8", children: children }) })] })] }) }));
|
|
206
209
|
}
|
|
207
210
|
// The user's own colours and background (`AppearanceProvider`), from what the last webmail page cached in this browser: the console never asks the server.
|
|
208
211
|
return (_jsxs(AppearanceProvider, { userUid: userUid, lookUp: false, children: [content, _jsx(FrameBrandingFooter, { parsed: footer, appTitle: [...adminNavItems(pluginNav), ...OFF_RAIL_ITEMS].find((item) => item.id === active)?.label })] }));
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DataSubjectErasureStatus } from "@rapidmx/react-shared/mail/erasureRequestApi.js";
|
|
3
|
+
/** How often the request is asked how far the erasure has got. */
|
|
4
|
+
export declare const ERASE_POLL_INTERVAL_MS = 2000;
|
|
5
|
+
export interface EraseLeftoverDataDialogProps {
|
|
6
|
+
/** The deleted mailbox's address. */
|
|
7
|
+
address: string;
|
|
8
|
+
/** What is left, when the caller knows it (the list of deleted mailboxes does): named in the confirmation. */
|
|
9
|
+
counts?: {
|
|
10
|
+
folders: number;
|
|
11
|
+
messages: number;
|
|
12
|
+
};
|
|
13
|
+
/** An erasure already filed for the address (approved or running): the dialog then watches it instead of asking first. */
|
|
14
|
+
resume?: {
|
|
15
|
+
uid: string;
|
|
16
|
+
status: DataSubjectErasureStatus;
|
|
17
|
+
};
|
|
18
|
+
/** The button shown once the data is erased (`onDone` runs when it is pressed - the dialog closes itself first). Default "Done". */
|
|
19
|
+
doneLabel?: string;
|
|
20
|
+
onDone?: () => void;
|
|
21
|
+
/** Called once when the data is erased - whether or not the dialog is still open by then. */
|
|
22
|
+
onErased?: (address: string) => void;
|
|
23
|
+
onClose: () => void;
|
|
24
|
+
/** How often to ask how far the erasure has got. */
|
|
25
|
+
pollIntervalMs?: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Erases the data a deleted mailbox left behind - permanently - after the administrator types its address to confirm, then shows
|
|
29
|
+
* how far it has got: the erasure runs on the server in the background, so this asks about it every few seconds until it has
|
|
30
|
+
* finished or been refused. Mount it (with the address as its `key`) only while it is wanted; it holds its own state.
|
|
31
|
+
*
|
|
32
|
+
* The request that files the erasure can't be interrupted (closing is ignored while it is under way); once it is filed, closing
|
|
33
|
+
* only stops watching - the erasure carries on, and the deleted-mailbox list shows it.
|
|
34
|
+
*/
|
|
35
|
+
export default function EraseLeftoverDataDialog({ address, counts, resume, doneLabel, onDone, onErased, onClose, pollIntervalMs, }: EraseLeftoverDataDialogProps): React.JSX.Element;
|
|
@@ -0,0 +1,115 @@
|
|
|
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, useRef, useState } from "react";
|
|
7
|
+
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
|
+
import { eraseLeftoverMailbox } from "@rapidmx/react-shared/admin/leftoverMailboxApi.js";
|
|
9
|
+
import { getErasureRequest } from "@rapidmx/react-shared/mail/erasureRequestApi.js";
|
|
10
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
11
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
12
|
+
import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
|
|
13
|
+
import { notify } from "../../../notifications/store.js";
|
|
14
|
+
const INPUT_CLASS = "w-full text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
15
|
+
const DANGER_BUTTON_CLASS = "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger";
|
|
16
|
+
/** How often the request is asked how far the erasure has got. */
|
|
17
|
+
export const ERASE_POLL_INTERVAL_MS = 2000;
|
|
18
|
+
/** Where an erasure is, in words. */
|
|
19
|
+
function progressLabel(status) {
|
|
20
|
+
return status === "in_progress" ? "Erasing the data…" : "Waiting for the erasure to start…";
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Erases the data a deleted mailbox left behind - permanently - after the administrator types its address to confirm, then shows
|
|
24
|
+
* how far it has got: the erasure runs on the server in the background, so this asks about it every few seconds until it has
|
|
25
|
+
* finished or been refused. Mount it (with the address as its `key`) only while it is wanted; it holds its own state.
|
|
26
|
+
*
|
|
27
|
+
* The request that files the erasure can't be interrupted (closing is ignored while it is under way); once it is filed, closing
|
|
28
|
+
* only stops watching - the erasure carries on, and the deleted-mailbox list shows it.
|
|
29
|
+
*/
|
|
30
|
+
export default function EraseLeftoverDataDialog({ address, counts, resume, doneLabel = "Done", onDone, onErased, onClose, pollIntervalMs = ERASE_POLL_INTERVAL_MS, }) {
|
|
31
|
+
const [phase, setPhase] = useState(resume ? "watching" : "confirm");
|
|
32
|
+
const [typed, setTyped] = useState("");
|
|
33
|
+
const [requestUid, setRequestUid] = useState(resume?.uid);
|
|
34
|
+
const [status, setStatus] = useState(resume?.status);
|
|
35
|
+
const [error, setError] = useState(null);
|
|
36
|
+
// A check on the erasure failed (offline, a dropped connection): it is tried again, and the admin is told meanwhile.
|
|
37
|
+
const [checkFailed, setCheckFailed] = useState(false);
|
|
38
|
+
const onErasedRef = useRef(onErased);
|
|
39
|
+
onErasedRef.current = onErased;
|
|
40
|
+
const confirmed = typed.trim().toLowerCase() === address.trim().toLowerCase();
|
|
41
|
+
async function handleErase(e) {
|
|
42
|
+
e.preventDefault();
|
|
43
|
+
if (!confirmed) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
setError(null);
|
|
47
|
+
setPhase("filing");
|
|
48
|
+
try {
|
|
49
|
+
const request = await eraseLeftoverMailbox(address);
|
|
50
|
+
setRequestUid(request.uid);
|
|
51
|
+
setStatus(request.status);
|
|
52
|
+
setPhase("watching");
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
// The server's own words: a legal hold names its matter, a mailbox that exists again says so.
|
|
56
|
+
setError(err instanceof ApiRequestError ? err.message : "Could not start the erasure.");
|
|
57
|
+
setPhase("confirm");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Watches the request until it has finished or been refused.
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (phase !== "watching" || !requestUid) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
let cancelled = false;
|
|
66
|
+
let timer;
|
|
67
|
+
const check = async () => {
|
|
68
|
+
try {
|
|
69
|
+
const request = await getErasureRequest(requestUid);
|
|
70
|
+
if (cancelled) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
setCheckFailed(false);
|
|
74
|
+
setStatus(request.status);
|
|
75
|
+
if (request.status === "completed") {
|
|
76
|
+
setPhase("done");
|
|
77
|
+
notify({ kind: "success", title: "Leftover data erased", message: `Everything left over from ${address} was erased.` });
|
|
78
|
+
onErasedRef.current?.(address);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (request.status === "denied") {
|
|
82
|
+
setError(request.reason || "The erasure was refused.");
|
|
83
|
+
setPhase("refused");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
if (cancelled) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
setCheckFailed(true);
|
|
92
|
+
}
|
|
93
|
+
timer = setTimeout(() => void check(), pollIntervalMs);
|
|
94
|
+
};
|
|
95
|
+
void check();
|
|
96
|
+
return () => {
|
|
97
|
+
cancelled = true;
|
|
98
|
+
clearTimeout(timer);
|
|
99
|
+
};
|
|
100
|
+
}, [phase, requestUid, address, pollIntervalMs]);
|
|
101
|
+
// Closing is ignored while the request that files the erasure is under way, so its answer can't land on a dialog that was dismissed.
|
|
102
|
+
function dismiss() {
|
|
103
|
+
if (phase !== "filing") {
|
|
104
|
+
onClose();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function handleDone() {
|
|
108
|
+
onClose();
|
|
109
|
+
onDone?.();
|
|
110
|
+
}
|
|
111
|
+
const summary = counts
|
|
112
|
+
? `It still has ${counts.folders} ${counts.folders === 1 ? "folder" : "folders"} and ${counts.messages} ${counts.messages === 1 ? "message" : "messages"}, and everything else stored with them.`
|
|
113
|
+
: "It still has its folders and everything stored in them.";
|
|
114
|
+
return (_jsxs(Modal, { open: true, onClose: dismiss, title: "Erase leftover data", children: [(phase === "confirm" || phase === "filing") && (_jsxs("form", { onSubmit: handleErase, children: [_jsxs("p", { className: "text-sm mb-3", children: [_jsx("strong", { className: "break-all", children: address }), " was deleted, but its data was kept. ", summary] }), _jsx("p", { className: "text-sm mb-3", children: "Erasing permanently deletes all of it - the mail, drafts and attachments, contacts, calendar events, tasks, notes, labels, filter rules and signatures, encryption keys, share links and access lists - and frees the address for a new mailbox." }), _jsx("p", { className: "text-sm font-semibold text-danger mb-4", children: "This is irreversible and cannot be undone." }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm mb-4", children: [_jsxs("span", { className: "font-semibold", children: ["Type ", _jsx("span", { className: "break-all", children: address }), " to confirm"] }), _jsx("input", { "aria-label": "Type the address to confirm", className: INPUT_CLASS, value: typed, disabled: phase === "filing", autoComplete: "off", spellCheck: false, onChange: (e) => setTyped(e.target.value) })] }), error && _jsx(Alert, { children: error }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: phase === "filing", onClick: dismiss, children: "Cancel" }), _jsx(Button, { type: "submit", className: DANGER_BUTTON_CLASS, loading: phase === "filing", disabled: phase === "filing" || !confirmed, children: "Erase data" })] })] })), phase === "watching" && (_jsxs("div", { children: [_jsxs("p", { className: "text-sm mb-3", children: ["Erasing the data left over from ", _jsx("strong", { className: "break-all", children: address }), "."] }), _jsxs("p", { role: "status", className: "flex items-center gap-2 text-sm font-semibold mb-3", children: [_jsx("span", { className: "w-4 h-4 rounded-full border-2 border-current/30 border-t-current animate-spin", "aria-hidden": "true" }), progressLabel(status)] }), checkFailed && (_jsx("p", { className: "text-sm text-danger mb-3", children: "Could not check how far the erasure has got. Trying again\u2026" })), _jsx("p", { className: "text-sm text-text-muted mb-4", children: "You can close this window: the erasure carries on in the background, and the Mailboxes page shows how it is going." }), _jsx("div", { className: "flex justify-end", children: _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: onClose, children: "Stop watching" }) })] })), phase === "done" && (_jsxs("div", { children: [_jsxs("p", { role: "status", className: "text-sm mb-4", children: ["Everything left over from ", _jsx("strong", { className: "break-all", children: address }), " was erased. The address is free to use again."] }), _jsx("div", { className: "flex justify-end", children: _jsx(Button, { type: "button", className: "!w-auto", onClick: handleDone, children: doneLabel }) })] })), phase === "refused" && (_jsxs("div", { children: [_jsx(Alert, { children: error }), _jsx("div", { className: "flex justify-end", children: _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: onClose, children: "OK" }) })] }))] }));
|
|
115
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
/** How often the list is refreshed while an erasure is under way. */
|
|
3
|
+
export declare const LEFTOVER_REFRESH_INTERVAL_MS = 5000;
|
|
4
|
+
export interface LeftoverMailboxesSectionProps {
|
|
5
|
+
/** How often the list is refreshed while an erasure is under way. */
|
|
6
|
+
refreshIntervalMs?: number;
|
|
7
|
+
/** How often an open erasure asks how far it has got. */
|
|
8
|
+
pollIntervalMs?: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* "Deleted mailboxes with remaining data": deleting a mailbox keeps everything in it, and the address can't be used again until
|
|
12
|
+
* that is erased, so this lists what is left (from `GET /mailboxes/leftover`) with an "Erase data" action for each. It shows nothing
|
|
13
|
+
* at all while there is nothing left - the section is only there when it is needed. An erasure that is under way shows as such, and
|
|
14
|
+
* the list refreshes itself until it has finished.
|
|
15
|
+
*/
|
|
16
|
+
export default function LeftoverMailboxesSection({ refreshIntervalMs, pollIntervalMs }: LeftoverMailboxesSectionProps): React.JSX.Element;
|