@rapidmx/web-client 0.3.1 → 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/branding/index.tsx +39 -404
- package/apps/admin/data-requests/index.tsx +480 -485
- package/apps/admin/distribution-lists/[uid].tsx +19 -1
- package/apps/admin/domains/[uid].tsx +92 -259
- package/apps/admin/encryption-policy/index.tsx +19 -0
- package/apps/admin/escrow-scopes/[uid].tsx +348 -237
- package/apps/admin/escrow-scopes/new/index.tsx +8 -2
- package/apps/admin/index.tsx +118 -82
- package/apps/admin/ingest-queue/index.tsx +44 -5
- package/apps/admin/mailbox-policy/index.tsx +19 -0
- package/apps/admin/mailboxes/[uid].tsx +211 -184
- package/apps/admin/mailboxes/new/index.tsx +28 -291
- package/apps/admin/plugins/index.tsx +15 -0
- package/apps/admin/quarantine/index.tsx +46 -14
- package/apps/admin/retention-policy/index.tsx +39 -132
- package/apps/admin/setup/index.tsx +15 -0
- 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 +251 -210
- 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 +385 -0
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +187 -0
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +112 -0
- package/apps/shared/components/admin/settings/LoadedSettingsForm.tsx +34 -0
- package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +308 -0
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +152 -0
- package/apps/shared/components/admin/settings/PluginsManager.tsx +1235 -0
- package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +174 -0
- package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +290 -0
- package/apps/shared/components/admin/setup/SetupWizard.tsx +416 -0
- package/apps/shared/components/admin/signOut.ts +79 -0
- package/apps/shared/components/admin/usePagedList.tsx +129 -0
- package/apps/shared/components/calendar/CalendarListSidebar.tsx +120 -76
- package/apps/shared/components/calendar/EventModal.tsx +717 -513
- 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 +80 -99
- package/apps/shared/components/contacts/ContactDetailPane.tsx +100 -21
- package/apps/shared/components/contacts/ContactForm.tsx +377 -314
- 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 +270 -169
- package/apps/shared/components/layout/BrandingChrome.tsx +48 -10
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +150 -26
- 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 -0
- package/apps/shared/components/mail/ConversationThreadPane.tsx +216 -181
- package/apps/shared/components/mail/MessageDetailPane.tsx +799 -76
- package/apps/shared/components/mail/compose/ComposeContext.tsx +26 -16
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +1652 -765
- package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -0
- package/apps/shared/components/mail/layout/MailShell.tsx +418 -302
- 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/components/settings/layout/SettingsShell.tsx +1 -0
- package/apps/shared/mail/findWellKnownFolderUid.ts +13 -0
- package/apps/shared/mail/listAllPages.ts +39 -0
- package/apps/shared/search/LocalIndexLifecycle.tsx +98 -0
- package/apps/shared/search/localIndexBlockCipher.ts +83 -0
- package/apps/shared/search/localIndexBuilder.ts +481 -0
- package/apps/shared/search/localIndexKey.ts +27 -0
- package/apps/shared/search/localIndexRpcClient.ts +316 -0
- package/apps/shared/search/localIndexSchema.ts +236 -0
- package/apps/shared/search/localIndexSizePreference.ts +88 -0
- package/apps/shared/search/localIndexStorage.ts +98 -0
- package/apps/shared/search/localIndexVFS.ts +266 -0
- package/apps/shared/search/localIndexWorker.ts +926 -0
- package/apps/shared/search/searchTier2.ts +79 -0
- package/apps/shared/search/wa-sqlite-shims.d.ts +44 -0
- package/apps/www/calendar/index.tsx +438 -407
- package/apps/www/contacts/[uid].tsx +21 -2
- package/apps/www/contacts/index.tsx +148 -17
- package/apps/www/index.tsx +1314 -486
- package/apps/www/messages/[uid].tsx +23 -7
- 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 +800 -215
- 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 +274 -0
- package/apps/www/tasks/index.tsx +638 -530
- package/dist/apps/admin/audit-log/index.js +38 -8
- package/dist/apps/admin/branding/index.js +4 -98
- package/dist/apps/admin/data-requests/index.js +52 -72
- package/dist/apps/admin/distribution-lists/[uid].js +1 -1
- package/dist/apps/admin/domains/[uid].js +5 -68
- package/dist/apps/admin/encryption-policy/index.js +8 -0
- 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 +17 -1
- package/dist/apps/admin/ingest-queue/index.js +24 -6
- package/dist/apps/admin/mailbox-policy/index.js +8 -0
- package/dist/apps/admin/mailboxes/[uid].js +20 -4
- package/dist/apps/admin/mailboxes/new/index.js +4 -89
- package/dist/apps/admin/plugins/index.js +6 -0
- package/dist/apps/admin/quarantine/index.js +20 -11
- package/dist/apps/admin/retention-policy/index.js +3 -36
- package/dist/apps/admin/setup/index.js +6 -0
- 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 +37 -4
- 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 +116 -0
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +83 -0
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +62 -0
- package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.js +25 -0
- package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +112 -0
- package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +74 -0
- package/dist/apps/shared/components/admin/settings/PluginsManager.js +572 -0
- package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +73 -0
- package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +147 -0
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +208 -0
- 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/CalendarListSidebar.js +24 -13
- package/dist/apps/shared/components/calendar/EventModal.js +154 -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 +47 -39
- package/dist/apps/shared/components/contacts/ContactDetailPane.js +43 -5
- package/dist/apps/shared/components/contacts/ContactForm.js +45 -12
- 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 +100 -8
- package/dist/apps/shared/components/layout/BrandingChrome.js +45 -10
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +63 -14
- 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 +136 -0
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +50 -12
- package/dist/apps/shared/components/mail/MessageDetailPane.js +464 -34
- package/dist/apps/shared/components/mail/compose/ComposeContext.js +9 -6
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +807 -116
- package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +50 -0
- package/dist/apps/shared/components/mail/layout/MailShell.js +116 -41
- 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/components/settings/layout/SettingsShell.js +1 -0
- package/dist/apps/shared/mail/findWellKnownFolderUid.js +12 -0
- package/dist/apps/shared/mail/listAllPages.js +25 -0
- package/dist/apps/shared/search/LocalIndexLifecycle.js +80 -0
- package/dist/apps/shared/search/localIndexBlockCipher.js +63 -0
- package/dist/apps/shared/search/localIndexBuilder.js +408 -0
- package/dist/apps/shared/search/localIndexKey.js +25 -0
- package/dist/apps/shared/search/localIndexRpcClient.js +241 -0
- package/dist/apps/shared/search/localIndexSchema.js +185 -0
- package/dist/apps/shared/search/localIndexSizePreference.js +78 -0
- package/dist/apps/shared/search/localIndexStorage.js +86 -0
- package/dist/apps/shared/search/localIndexVFS.js +258 -0
- package/dist/apps/shared/search/localIndexWorker.js +687 -0
- package/dist/apps/shared/search/searchTier2.js +38 -0
- package/dist/apps/www/calendar/index.js +38 -18
- package/dist/apps/www/contacts/[uid].js +13 -2
- package/dist/apps/www/contacts/index.js +94 -15
- package/dist/apps/www/index.js +770 -137
- package/dist/apps/www/messages/[uid].js +23 -7
- 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 +541 -96
- 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 +124 -0
- package/dist/apps/www/tasks/index.js +76 -14
- package/package.json +3 -2
|
@@ -8,7 +8,7 @@ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
|
8
8
|
import { toDatetimeLocal } from "@rapidmx/react-shared/util/dateInput.js";
|
|
9
9
|
import { deleteEscrowScope, getEscrowScope, updateEscrowScope, } from "@rapidmx/react-shared/admin/escrowScopesApi.js";
|
|
10
10
|
import AdminShell from "../../shared/components/admin/layout/AdminShell.js";
|
|
11
|
-
import EscrowScopeKeyAndHoldersFields from "../../shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js";
|
|
11
|
+
import EscrowScopeKeyAndHoldersFields, { selfAsHolderError, } from "../../shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js";
|
|
12
12
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
13
13
|
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
14
14
|
import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
|
|
@@ -26,10 +26,75 @@ function toFieldsValue(scope) {
|
|
|
26
26
|
notifySubjectOnAccess: scope.notifySubjectOnAccess,
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Describes every change to the security-relevant parts of a scope - who holds it, how many holders must approve,
|
|
31
|
+
* and its public key - so the administrator confirms exactly what they're changing before it's saved.
|
|
32
|
+
*/
|
|
33
|
+
export function describeSecurityChanges(original, next) {
|
|
34
|
+
const changes = [];
|
|
35
|
+
const added = next.holderUserUids.filter((holder) => !original.holderUserUids.includes(holder));
|
|
36
|
+
const removed = original.holderUserUids.filter((holder) => !next.holderUserUids.includes(holder));
|
|
37
|
+
if (added.length > 0) {
|
|
38
|
+
changes.push(`Add holders: ${added.join(", ")}`);
|
|
39
|
+
}
|
|
40
|
+
if (removed.length > 0) {
|
|
41
|
+
changes.push(`Remove holders: ${removed.join(", ")}`);
|
|
42
|
+
}
|
|
43
|
+
if (next.requiredHolders !== original.requiredHolders) {
|
|
44
|
+
changes.push(`Required approvals: ${original.requiredHolders} → ${next.requiredHolders}`);
|
|
45
|
+
}
|
|
46
|
+
if (next.notifySubjectOnAccess !== original.notifySubjectOnAccess) {
|
|
47
|
+
const onOff = (value) => (value ? "on" : "off");
|
|
48
|
+
changes.push(`Notify subject on access: ${onOff(original.notifySubjectOnAccess)} → ${onOff(next.notifySubjectOnAccess)}`);
|
|
49
|
+
}
|
|
50
|
+
const originalKey = toFieldsValue(original);
|
|
51
|
+
if (next.publicKey.trim() !== originalKey.publicKey || next.keyType.trim() !== originalKey.keyType) {
|
|
52
|
+
changes.push("Replace the public key");
|
|
53
|
+
}
|
|
54
|
+
if (next.fingerprint.trim() !== originalKey.fingerprint) {
|
|
55
|
+
changes.push(`Fingerprint: ${originalKey.fingerprint} → ${next.fingerprint.trim()}`);
|
|
56
|
+
}
|
|
57
|
+
if (validityChanged(originalKey, next)) {
|
|
58
|
+
changes.push(`Key validity: ${originalKey.notBefore} – ${originalKey.notAfter} → ${next.notBefore} – ${next.notAfter}`);
|
|
59
|
+
}
|
|
60
|
+
return changes;
|
|
61
|
+
}
|
|
62
|
+
function keyMaterialChanged(originalKey, next) {
|
|
63
|
+
return (next.publicKey.trim() !== originalKey.publicKey ||
|
|
64
|
+
next.keyType.trim() !== originalKey.keyType ||
|
|
65
|
+
next.fingerprint.trim() !== originalKey.fingerprint);
|
|
66
|
+
}
|
|
67
|
+
function validityChanged(originalKey, next) {
|
|
68
|
+
return next.notBefore !== originalKey.notBefore || next.notAfter !== originalKey.notAfter;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The `publicKey` to send with an update, or `undefined` when the admin didn't touch any key field - the form only
|
|
72
|
+
* shows dates to the minute, so re-sending a rebuilt key would silently truncate the stored validity window. A date
|
|
73
|
+
* field left as loaded keeps its original epoch-ms value. `revokedAt` isn't editable here: it's carried over when
|
|
74
|
+
* only the validity window changed, and dropped when the key material itself is replaced (a new key isn't revoked).
|
|
75
|
+
*/
|
|
76
|
+
export function buildPublicKeyUpdate(original, next) {
|
|
77
|
+
const originalKey = toFieldsValue(original);
|
|
78
|
+
const materialChanged = keyMaterialChanged(originalKey, next);
|
|
79
|
+
if (!materialChanged && !validityChanged(originalKey, next)) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
const key = {
|
|
83
|
+
publicKey: next.publicKey.trim(),
|
|
84
|
+
type: next.keyType.trim(),
|
|
85
|
+
fingerprint: next.fingerprint.trim(),
|
|
86
|
+
notBefore: next.notBefore === originalKey.notBefore ? original.publicKey.notBefore : new Date(next.notBefore).getTime(),
|
|
87
|
+
notAfter: next.notAfter === originalKey.notAfter ? original.publicKey.notAfter : new Date(next.notAfter).getTime(),
|
|
88
|
+
};
|
|
89
|
+
if (!materialChanged && original.publicKey.revokedAt !== undefined) {
|
|
90
|
+
key.revokedAt = original.publicKey.revokedAt;
|
|
91
|
+
}
|
|
92
|
+
return key;
|
|
93
|
+
}
|
|
29
94
|
export default function EscrowScopeDetailPage(props) {
|
|
30
|
-
return (_jsx(AdminShell, { ...props, active: "escrowScopes", children: _jsx(EscrowScopeDetailContent, { uid: props.params.uid }) }));
|
|
95
|
+
return (_jsx(AdminShell, { ...props, active: "escrowScopes", children: _jsx(EscrowScopeDetailContent, { uid: props.params.uid, adminUid: props.userUid }) }));
|
|
31
96
|
}
|
|
32
|
-
function EscrowScopeDetailContent({ uid }) {
|
|
97
|
+
function EscrowScopeDetailContent({ uid, adminUid }) {
|
|
33
98
|
const [original, setOriginal] = useState(null);
|
|
34
99
|
const [name, setName] = useState("");
|
|
35
100
|
const [description, setDescription] = useState("");
|
|
@@ -41,6 +106,7 @@ function EscrowScopeDetailContent({ uid }) {
|
|
|
41
106
|
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
|
42
107
|
const [deleting, setDeleting] = useState(false);
|
|
43
108
|
const [deleteError, setDeleteError] = useState(null);
|
|
109
|
+
const [pendingChanges, setPendingChanges] = useState(null);
|
|
44
110
|
useEffect(() => {
|
|
45
111
|
setLoading(true);
|
|
46
112
|
setError(null);
|
|
@@ -79,6 +145,20 @@ function EscrowScopeDetailContent({ uid }) {
|
|
|
79
145
|
setError("Required holders must be between 1 and the number of holders.");
|
|
80
146
|
return;
|
|
81
147
|
}
|
|
148
|
+
const selfError = selfAsHolderError(fields.holderUserUids, adminUid, original.holderUserUids);
|
|
149
|
+
if (selfError) {
|
|
150
|
+
setError(selfError);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const changes = describeSecurityChanges(original, fields);
|
|
154
|
+
if (changes.length > 0) {
|
|
155
|
+
setPendingChanges(changes);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
await save();
|
|
159
|
+
}
|
|
160
|
+
async function save() {
|
|
161
|
+
setPendingChanges(null);
|
|
82
162
|
setSaving(true);
|
|
83
163
|
setSaved(false);
|
|
84
164
|
try {
|
|
@@ -86,14 +166,10 @@ function EscrowScopeDetailContent({ uid }) {
|
|
|
86
166
|
uid: original.uid,
|
|
87
167
|
version: original.version,
|
|
88
168
|
name: name.trim(),
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
fingerprint: fields.fingerprint.trim(),
|
|
94
|
-
notBefore: new Date(fields.notBefore).getTime(),
|
|
95
|
-
notAfter: new Date(fields.notAfter).getTime(),
|
|
96
|
-
},
|
|
169
|
+
// `null` (not omitted) so clearing the field actually clears the stored description - an update is
|
|
170
|
+
// a merge, and an absent field is left as it was.
|
|
171
|
+
description: (description.trim() || null),
|
|
172
|
+
publicKey: buildPublicKeyUpdate(original, fields),
|
|
97
173
|
holderUserUids: fields.holderUserUids,
|
|
98
174
|
requiredHolders: fields.requiredHolders,
|
|
99
175
|
notifySubjectOnAccess: fields.notifySubjectOnAccess,
|
|
@@ -109,7 +185,10 @@ function EscrowScopeDetailContent({ uid }) {
|
|
|
109
185
|
setSaving(false);
|
|
110
186
|
}
|
|
111
187
|
}
|
|
188
|
+
// Ignored while deleting (Escape/Close; Cancel is disabled then too).
|
|
112
189
|
function closeDeleteModal() {
|
|
190
|
+
if (deleting)
|
|
191
|
+
return;
|
|
113
192
|
setConfirmingDelete(false);
|
|
114
193
|
}
|
|
115
194
|
// Only ever invoked from the delete-confirmation modal below, which itself only renders once
|
|
@@ -137,5 +216,5 @@ function EscrowScopeDetailContent({ uid }) {
|
|
|
137
216
|
if (!original || !fields) {
|
|
138
217
|
return _jsx(Alert, { children: "Escrow scope not found." });
|
|
139
218
|
}
|
|
140
|
-
return (_jsxs("div", { className: "max-w-3xl", children: [_jsxs("div", { className: "flex items-start justify-between gap-4 mb-1", children: [_jsxs("div", { children: [_jsx("a", { href: "/admin/escrow-scopes", className: "text-sm text-primary-dark hover:underline", children: "\u2190 All escrow scopes" }), _jsx("h1", { className: "text-xl font-bold tracking-tight mt-1", children: original.name })] }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto shrink-0 !border-danger !text-danger hover:!border-danger hover:!text-danger", onClick: () => setConfirmingDelete(true), children: "Delete scope" })] }), error && _jsx(Alert, { children: error }), saved && !error && _jsx("div", { className: "mb-4 text-sm text-success font-medium", children: "Saved." }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-5 mt-4", children: [_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx(FormField, { label: "Name", htmlFor: "name", children: _jsx("input", { id: "name", type: "text", className: INPUT_CLASS, value: name, onChange: (e) => setName(e.target.value) }) }), _jsx(FormField, { label: "Description (optional)", htmlFor: "description", children: _jsx("input", { id: "description", type: "text", className: INPUT_CLASS, value: description, onChange: (e) => setDescription(e.target.value) }) })] }), _jsx(EscrowScopeKeyAndHoldersFields, { value: fields, onChange: setFields }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save changes" }) })] }), _jsxs(Modal, { open: confirmingDelete, onClose: closeDeleteModal, title: "Delete escrow scope", children: [_jsxs("p", { className: "text-sm mb-5", children: ["Are you sure you want to delete ", _jsx("strong", { children: original.name }), "? This cannot be undone, and fails if any Matter still references this scope."] }), 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, onClick: handleDelete, children: "Delete" })] })] })] }));
|
|
219
|
+
return (_jsxs("div", { className: "max-w-3xl", children: [_jsxs("div", { className: "flex items-start justify-between gap-4 mb-1", children: [_jsxs("div", { children: [_jsx("a", { href: "/admin/escrow-scopes", className: "text-sm text-primary-dark hover:underline", children: "\u2190 All escrow scopes" }), _jsx("h1", { className: "text-xl font-bold tracking-tight mt-1", children: original.name })] }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto shrink-0 !border-danger !text-danger hover:!border-danger hover:!text-danger", onClick: () => setConfirmingDelete(true), children: "Delete scope" })] }), error && _jsx(Alert, { children: error }), saved && !error && _jsx("div", { className: "mb-4 text-sm text-success font-medium", children: "Saved." }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-5 mt-4", children: [_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx(FormField, { label: "Name", htmlFor: "name", children: _jsx("input", { id: "name", type: "text", className: INPUT_CLASS, value: name, onChange: (e) => setName(e.target.value) }) }), _jsx(FormField, { label: "Description (optional)", htmlFor: "description", children: _jsx("input", { id: "description", type: "text", className: INPUT_CLASS, value: description, onChange: (e) => setDescription(e.target.value) }) })] }), _jsx(EscrowScopeKeyAndHoldersFields, { value: fields, onChange: setFields }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save changes" }) })] }), _jsxs(Modal, { open: pendingChanges !== null, onClose: () => setPendingChanges(null), title: "Confirm escrow scope changes", children: [_jsxs("p", { className: "text-sm mb-3", children: ["These changes decide who can recover escrowed mail under ", _jsx("strong", { children: original.name }), ":"] }), _jsx("ul", { className: "list-disc pl-5 text-sm flex flex-col gap-1 mb-3", children: pendingChanges?.map((change) => (_jsx("li", { className: "break-all", children: change }, change))) }), _jsx("p", { className: "text-xs text-text-muted", children: "The server refuses changes while an access request under this scope is still awaiting approval." }), _jsxs("div", { className: "flex gap-3 justify-end mt-5", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setPendingChanges(null), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", onClick: () => void save(), children: "Confirm and save" })] })] }), _jsxs(Modal, { open: confirmingDelete, onClose: closeDeleteModal, title: "Delete escrow scope", children: [_jsxs("p", { className: "text-sm mb-5", children: ["Are you sure you want to delete ", _jsx("strong", { children: original.name }), "? This cannot be undone, and fails if any Matter still references this scope."] }), 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, onClick: handleDelete, children: "Delete" })] })] })] }));
|
|
141
220
|
}
|
|
@@ -7,15 +7,15 @@ import { useState } from "react";
|
|
|
7
7
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
8
|
import { createEscrowScope } from "@rapidmx/react-shared/admin/escrowScopesApi.js";
|
|
9
9
|
import AdminShell from "../../../shared/components/admin/layout/AdminShell.js";
|
|
10
|
-
import EscrowScopeKeyAndHoldersFields, { emptyEscrowScopeKeyAndHoldersValue, } from "../../../shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js";
|
|
10
|
+
import EscrowScopeKeyAndHoldersFields, { emptyEscrowScopeKeyAndHoldersValue, selfAsHolderError, } from "../../../shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js";
|
|
11
11
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
12
12
|
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
13
13
|
import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
|
|
14
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
15
|
export default function NewEscrowScopePage(props) {
|
|
16
|
-
return (_jsx(AdminShell, { ...props, active: "escrowScopes", children: _jsx(NewEscrowScopeForm, {}) }));
|
|
16
|
+
return (_jsx(AdminShell, { ...props, active: "escrowScopes", children: _jsx(NewEscrowScopeForm, { adminUid: props.userUid }) }));
|
|
17
17
|
}
|
|
18
|
-
function NewEscrowScopeForm() {
|
|
18
|
+
function NewEscrowScopeForm({ adminUid }) {
|
|
19
19
|
const [name, setName] = useState("");
|
|
20
20
|
const [description, setDescription] = useState("");
|
|
21
21
|
const [keyAndHolders, setKeyAndHolders] = useState(emptyEscrowScopeKeyAndHoldersValue());
|
|
@@ -40,6 +40,11 @@ function NewEscrowScopeForm() {
|
|
|
40
40
|
setError("Required holders must be between 1 and the number of holders.");
|
|
41
41
|
return;
|
|
42
42
|
}
|
|
43
|
+
const selfError = selfAsHolderError(keyAndHolders.holderUserUids, adminUid);
|
|
44
|
+
if (selfError) {
|
|
45
|
+
setError(selfError);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
43
48
|
setSaving(true);
|
|
44
49
|
try {
|
|
45
50
|
const created = await createEscrowScope({
|
package/dist/apps/admin/index.js
CHANGED
|
@@ -6,10 +6,12 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
6
6
|
import { useEffect, useState } from "react";
|
|
7
7
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
8
|
import { listMailboxes } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
9
|
+
import { reopenSetup } from "@rapidmx/react-shared/admin/setupApi.js";
|
|
9
10
|
import AdminShell from "../shared/components/admin/layout/AdminShell.js";
|
|
10
11
|
import MailboxTable from "../shared/components/admin/mailboxes/MailboxTable.js";
|
|
11
12
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
12
13
|
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
14
|
+
import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
|
|
13
15
|
const PAGE_SIZE = 25;
|
|
14
16
|
export default function MailboxesListPage(props) {
|
|
15
17
|
return (_jsx(AdminShell, { ...props, active: "mailboxes", children: _jsx(MailboxesListContent, {}) }));
|
|
@@ -28,5 +30,19 @@ function MailboxesListContent() {
|
|
|
28
30
|
.finally(() => setLoading(false));
|
|
29
31
|
}, [page]);
|
|
30
32
|
const hasNextPage = mailboxes.length === PAGE_SIZE;
|
|
31
|
-
|
|
33
|
+
const [reopening, setReopening] = useState(false);
|
|
34
|
+
const [confirmingSetup, setConfirmingSetup] = useState(false);
|
|
35
|
+
async function handleRunSetup() {
|
|
36
|
+
setReopening(true);
|
|
37
|
+
try {
|
|
38
|
+
await reopenSetup();
|
|
39
|
+
window.location.href = "/admin/setup";
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
setError(err instanceof ApiRequestError ? err.message : "Could not reopen setup.");
|
|
43
|
+
setReopening(false);
|
|
44
|
+
setConfirmingSetup(false);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
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" }) })] })] }), 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" })] })] }));
|
|
32
48
|
}
|
|
@@ -8,6 +8,8 @@ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
|
8
8
|
import { listIngestQueue } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
9
9
|
import AdminShell from "../../shared/components/admin/layout/AdminShell.js";
|
|
10
10
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
11
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
12
|
+
const PAGE_SIZE = 25;
|
|
11
13
|
export function readMailboxUid() {
|
|
12
14
|
if (typeof window === "undefined")
|
|
13
15
|
return null;
|
|
@@ -24,6 +26,7 @@ export default function IngestQueuePage(props) {
|
|
|
24
26
|
}
|
|
25
27
|
function IngestQueueContent() {
|
|
26
28
|
const [mailboxUid, setMailboxUid] = useState(null);
|
|
29
|
+
const [page, setPage] = useState(0);
|
|
27
30
|
const [entries, setEntries] = useState([]);
|
|
28
31
|
const [loading, setLoading] = useState(true);
|
|
29
32
|
const [error, setError] = useState(null);
|
|
@@ -34,15 +37,30 @@ function IngestQueueContent() {
|
|
|
34
37
|
if (!mailboxUid) {
|
|
35
38
|
return;
|
|
36
39
|
}
|
|
40
|
+
// Paging quickly can leave an older page's response landing last - only the latest request's is applied.
|
|
41
|
+
let cancelled = false;
|
|
37
42
|
setLoading(true);
|
|
38
43
|
setError(null);
|
|
39
|
-
listIngestQueue(mailboxUid)
|
|
40
|
-
.then(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
listIngestQueue(mailboxUid, { page, limit: PAGE_SIZE })
|
|
45
|
+
.then((data) => {
|
|
46
|
+
if (!cancelled)
|
|
47
|
+
setEntries(data);
|
|
48
|
+
})
|
|
49
|
+
.catch((err) => {
|
|
50
|
+
if (!cancelled)
|
|
51
|
+
setError(err instanceof ApiRequestError ? err.message : "Could not load the ingest queue.");
|
|
52
|
+
})
|
|
53
|
+
.finally(() => {
|
|
54
|
+
if (!cancelled)
|
|
55
|
+
setLoading(false);
|
|
56
|
+
});
|
|
57
|
+
return () => {
|
|
58
|
+
cancelled = true;
|
|
59
|
+
};
|
|
60
|
+
}, [mailboxUid, page]);
|
|
61
|
+
const hasNextPage = entries.length === PAGE_SIZE;
|
|
44
62
|
if (!mailboxUid) {
|
|
45
63
|
return _jsx(Alert, { children: "No mailbox specified. Open a mailbox's detail page and choose \"View ingest queue\"." });
|
|
46
64
|
}
|
|
47
|
-
return (_jsxs("div", { className: "max-w-4xl flex flex-col gap-4", children: [_jsxs("div", { children: [_jsx("a", { href: `/admin/mailboxes/${encodeURIComponent(mailboxUid)}`, className: "text-sm text-primary-dark hover:underline", children: "\u2190 Back to mailbox" }), _jsx("h1", { className: "text-xl font-bold uppercase tracking-wide mt-1", children: "Ingest queue" })] }), error && _jsx(Alert, { children: error }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : entries.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "Nothing pending or failed for this mailbox." })) : (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm border-collapse", children: [_jsx("thead", { children: _jsx("tr", { children: ["From", "To", "Received", "Status", "Error"].map((h) => (_jsx("th", { className: "text-left text-xs uppercase tracking-wide text-text-muted py-2 px-2.5 border-b border-border", children: h }, h))) }) }), _jsx("tbody", { children: entries.map((entry) => (_jsxs("tr", { children: [_jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: entry.envelopeFrom }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: entry.envelopeTo.join(", ") }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: new Date(entry.dateCreated).toLocaleString() }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: _jsx("span", { className: `inline-block text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill ${STATUS_STYLES[entry.status]}`, children: entry.status }) }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border text-text-muted", children: entry.errorMessage ?? "" })] }, entry.uid))) })] }) }))] }));
|
|
65
|
+
return (_jsxs("div", { className: "max-w-4xl flex flex-col gap-4", children: [_jsxs("div", { children: [_jsx("a", { href: `/admin/mailboxes/${encodeURIComponent(mailboxUid)}`, className: "text-sm text-primary-dark hover:underline", children: "\u2190 Back to mailbox" }), _jsx("h1", { className: "text-xl font-bold uppercase tracking-wide mt-1", children: "Ingest queue" })] }), error && _jsx(Alert, { children: error }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : entries.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "Nothing pending or failed for this mailbox." })) : (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm border-collapse", children: [_jsx("thead", { children: _jsx("tr", { children: ["From", "To", "Received", "Status", "Error"].map((h) => (_jsx("th", { className: "text-left text-xs uppercase tracking-wide text-text-muted py-2 px-2.5 border-b border-border", children: h }, h))) }) }), _jsx("tbody", { children: entries.map((entry) => (_jsxs("tr", { children: [_jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: entry.envelopeFrom }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: entry.envelopeTo.join(", ") }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: new Date(entry.dateCreated).toLocaleString() }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: _jsx("span", { className: `inline-block text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill ${STATUS_STYLES[entry.status]}`, children: entry.status }) }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border text-text-muted", children: entry.errorMessage ?? "" })] }, entry.uid))) })] }) })), _jsxs("div", { className: "flex gap-3 items-center", 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" })] })] }));
|
|
48
66
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { getMailboxPolicy } from "@rapidmx/react-shared/admin/mailboxPolicyApi.js";
|
|
3
|
+
import AdminShell from "../../shared/components/admin/layout/AdminShell.js";
|
|
4
|
+
import LoadedSettingsForm from "../../shared/components/admin/settings/LoadedSettingsForm.js";
|
|
5
|
+
import MailboxPolicyForm from "../../shared/components/admin/settings/MailboxPolicyForm.js";
|
|
6
|
+
export default function MailboxPolicyPage(props) {
|
|
7
|
+
return (_jsx(AdminShell, { ...props, active: "mailboxPolicy", children: _jsx(LoadedSettingsForm, { load: getMailboxPolicy, loadErrorMessage: "Could not load the mailbox policy.", children: (policy, onChange) => _jsx(MailboxPolicyForm, { policy: policy, onChange: onChange }) }) }));
|
|
8
|
+
}
|
|
@@ -9,6 +9,7 @@ import { deleteMailbox, getMailbox, impersonateUser } from "@rapidmx/react-share
|
|
|
9
9
|
import AdminShell from "../../shared/components/admin/layout/AdminShell.js";
|
|
10
10
|
import ShareAccessCard from "../../shared/components/admin/mailboxes/ShareAccessCard.js";
|
|
11
11
|
import ResourceSettingsCard from "../../shared/components/admin/mailboxes/ResourceSettingsCard.js";
|
|
12
|
+
import EscrowScopeCard from "../../shared/components/admin/mailboxes/EscrowScopeCard.js";
|
|
12
13
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
13
14
|
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
14
15
|
import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
|
|
@@ -28,22 +29,37 @@ function MailboxDetailContent({ uid, impersonationBaseUrl }) {
|
|
|
28
29
|
const [mailbox, setMailbox] = useState(null);
|
|
29
30
|
const [loading, setLoading] = useState(true);
|
|
30
31
|
const [error, setError] = useState(null);
|
|
32
|
+
const [confirmingAccess, setConfirmingAccess] = useState(false);
|
|
31
33
|
const [impersonating, setImpersonating] = useState(false);
|
|
34
|
+
const [accessError, setAccessError] = useState(null);
|
|
32
35
|
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
|
33
36
|
const [deleting, setDeleting] = useState(false);
|
|
34
37
|
const [deleteError, setDeleteError] = useState(null);
|
|
35
|
-
|
|
38
|
+
// Ignored while the action is under way (Escape/Close; Cancel is disabled then too), so its result or error
|
|
39
|
+
// can't land on a modal reopened afterwards.
|
|
40
|
+
function closeAccessModal() {
|
|
41
|
+
if (impersonating)
|
|
42
|
+
return;
|
|
43
|
+
setConfirmingAccess(false);
|
|
44
|
+
setAccessError(null);
|
|
45
|
+
}
|
|
46
|
+
// Only ever invoked from the access-confirmation modal below, which only renders for a loaded mailbox that has
|
|
47
|
+
// an owner. A failure stays in the modal rather than replacing the whole page.
|
|
48
|
+
async function handleAccessMailbox() {
|
|
36
49
|
setImpersonating(true);
|
|
50
|
+
setAccessError(null);
|
|
37
51
|
try {
|
|
38
|
-
await impersonateUser(impersonationBaseUrl ?? "", ownerUserUid);
|
|
52
|
+
await impersonateUser(impersonationBaseUrl ?? "", mailbox.ownerUserUid);
|
|
39
53
|
window.location.href = "/";
|
|
40
54
|
}
|
|
41
55
|
catch (err) {
|
|
42
|
-
|
|
56
|
+
setAccessError(err instanceof ApiRequestError ? err.message : "Could not access this mailbox.");
|
|
43
57
|
setImpersonating(false);
|
|
44
58
|
}
|
|
45
59
|
}
|
|
46
60
|
function closeDeleteModal() {
|
|
61
|
+
if (deleting)
|
|
62
|
+
return;
|
|
47
63
|
setConfirmingDelete(false);
|
|
48
64
|
}
|
|
49
65
|
// Only ever invoked from the delete-confirmation modal below, which itself only renders once
|
|
@@ -79,5 +95,5 @@ function MailboxDetailContent({ uid, impersonationBaseUrl }) {
|
|
|
79
95
|
}
|
|
80
96
|
const quarantineHref = `/admin/quarantine?mailboxUid=${encodeURIComponent(mailbox.uid)}`;
|
|
81
97
|
const ingestQueueHref = `/admin/ingest-queue?mailboxUid=${encodeURIComponent(mailbox.uid)}`;
|
|
82
|
-
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",
|
|
98
|
+
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: "Access this mailbox" })), _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("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 }), _jsx(EscrowScopeCard, { mailbox: mailbox, onUpdate: setMailbox }), mailbox.isResource && _jsx(ResourceSettingsCard, { mailbox: mailbox, onUpdate: setMailbox }), _jsxs(Modal, { open: confirmingAccess, onClose: closeAccessModal, title: "Access this mailbox", 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."] }), 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: "Access mailbox" })] })] }), _jsxs(Modal, { open: confirmingDelete, onClose: closeDeleteModal, title: "Delete mailbox", children: [_jsxs("p", { className: "text-sm mb-5", children: ["Are you sure you want to delete ", _jsx("strong", { children: mailbox.primarySmtpAddress }), "? This permanently deletes the mailbox and everything in it, and cannot be undone. It fails if this mailbox is a custodian on an active legal hold."] }), 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, onClick: handleDelete, children: "Delete" })] })] })] }));
|
|
83
99
|
}
|
|
@@ -1,93 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
-
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
-
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
-
import { useEffect, useState } from "react";
|
|
7
|
-
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
|
-
import { createMailbox, listMailboxDomains } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
9
2
|
import AdminShell from "../../../shared/components/admin/layout/AdminShell.js";
|
|
10
|
-
import
|
|
11
|
-
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
12
|
-
import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
|
|
13
|
-
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";
|
|
14
|
-
const SELECT_CLASS = "text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
3
|
+
import MailboxCreateForm from "../../../shared/components/admin/settings/MailboxCreateForm.js";
|
|
15
4
|
export default function NewMailboxPage(props) {
|
|
16
|
-
return (_jsx(AdminShell, { ...props, active: "mailboxes", children: _jsx(
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const [primarySmtpAddress, setPrimarySmtpAddress] = useState("");
|
|
20
|
-
const [localPart, setLocalPart] = useState("");
|
|
21
|
-
const [domains, setDomains] = useState([]);
|
|
22
|
-
const [domain, setDomain] = useState("");
|
|
23
|
-
const [displayName, setDisplayName] = useState("");
|
|
24
|
-
const [ownerUserUid, setOwnerUserUid] = useState("");
|
|
25
|
-
const [timezone, setTimezone] = useState("UTC");
|
|
26
|
-
const [quotaGb, setQuotaGb] = useState(5);
|
|
27
|
-
const [isResource, setIsResource] = useState(false);
|
|
28
|
-
const [resourceType, setResourceType] = useState("room");
|
|
29
|
-
const [resourceCapacity, setResourceCapacity] = useState("");
|
|
30
|
-
const [autoAcceptBookings, setAutoAcceptBookings] = useState(false);
|
|
31
|
-
const [allowConflicts, setAllowConflicts] = useState(false);
|
|
32
|
-
const [bookingWindowDays, setBookingWindowDays] = useState("");
|
|
33
|
-
const [maxDurationMinutes, setMaxDurationMinutes] = useState("");
|
|
34
|
-
const [error, setError] = useState(null);
|
|
35
|
-
const [saving, setSaving] = useState(false);
|
|
36
|
-
// An empty `mail:domains` list (the default) means this server enforces no domain restriction —
|
|
37
|
-
// the address field stays free text, exactly as before. Once an admin configures at least one
|
|
38
|
-
// domain, every mailbox this server creates must be on one of them (enforced server-side too, in
|
|
39
|
-
// `BaseMailboxRoute.create()`), so the form switches to a local-part input + domain dropdown to
|
|
40
|
-
// make that restriction visible rather than let an admin discover it only via a rejected submit.
|
|
41
|
-
useEffect(() => {
|
|
42
|
-
listMailboxDomains()
|
|
43
|
-
.then((list) => {
|
|
44
|
-
setDomains(list);
|
|
45
|
-
setDomain(list[0] ?? "");
|
|
46
|
-
})
|
|
47
|
-
.catch(() => undefined);
|
|
48
|
-
}, []);
|
|
49
|
-
const constrained = domains.length > 0;
|
|
50
|
-
async function handleSubmit(e) {
|
|
51
|
-
e.preventDefault();
|
|
52
|
-
setError(null);
|
|
53
|
-
const address = constrained ? `${localPart.trim()}@${domain}` : primarySmtpAddress.trim();
|
|
54
|
-
if (constrained ? !localPart.trim() : !primarySmtpAddress.trim()) {
|
|
55
|
-
setError("A primary SMTP address is required.");
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
if (!displayName.trim()) {
|
|
59
|
-
setError("A display name is required.");
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
setSaving(true);
|
|
63
|
-
try {
|
|
64
|
-
// ownerUserUid left blank creates a true ownerless shared mailbox (e.g. support@example.com);
|
|
65
|
-
// delegates are then granted access from the mailbox's detail page. A resource mailbox is
|
|
66
|
-
// ownerless the same way — leave "Owner user uid" blank for one too.
|
|
67
|
-
const mailbox = await createMailbox({
|
|
68
|
-
primarySmtpAddress: address,
|
|
69
|
-
displayName: displayName.trim(),
|
|
70
|
-
ownerUserUid: ownerUserUid.trim() || undefined,
|
|
71
|
-
timezone,
|
|
72
|
-
quotaBytes: Math.round(quotaGb * 1000000000),
|
|
73
|
-
...(isResource && {
|
|
74
|
-
isResource: true,
|
|
75
|
-
resourceType,
|
|
76
|
-
resourceCapacity: resourceCapacity ? Number(resourceCapacity) : undefined,
|
|
77
|
-
autoAcceptBookings,
|
|
78
|
-
allowConflicts,
|
|
79
|
-
bookingWindowDays: bookingWindowDays ? Number(bookingWindowDays) : undefined,
|
|
80
|
-
maxDurationMinutes: maxDurationMinutes ? Number(maxDurationMinutes) : undefined,
|
|
81
|
-
}),
|
|
82
|
-
});
|
|
83
|
-
window.location.href = `/admin/mailboxes/${encodeURIComponent(mailbox.uid)}`;
|
|
84
|
-
}
|
|
85
|
-
catch (err) {
|
|
86
|
-
setError(err instanceof ApiRequestError ? err.message : "Could not create the mailbox.");
|
|
87
|
-
}
|
|
88
|
-
finally {
|
|
89
|
-
setSaving(false);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
return (_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: "Leave \"Owner user uid\" blank to create a true ownerless shared mailbox (the Exchange \"shared mailbox\" concept) \u2014 access is then granted entirely to delegates afterward, not to a single owner. Set an owner to create a mailbox for a specific user instead." }), error && _jsx(Alert, { children: error }), _jsxs("form", { onSubmit: handleSubmit, className: "bg-surface border border-border rounded-md p-6", children: [constrained ? (_jsx(FormField, { label: "Local part", htmlFor: "localPart", children: _jsxs("div", { className: "flex gap-2 items-center", children: [_jsx("input", { id: "localPart", type: "text", className: INPUT_CLASS, value: localPart, onChange: (e) => setLocalPart(e.target.value), placeholder: "support" }), _jsx("span", { className: "text-text-muted shrink-0", children: "@" }), _jsx("select", { "aria-label": "Domain", className: SELECT_CLASS, value: domain, onChange: (e) => setDomain(e.target.value), children: domains.map((d) => (_jsx("option", { value: d, children: d }, d))) })] }) })) : (_jsx(FormField, { label: "Primary SMTP address", htmlFor: "primarySmtpAddress", children: _jsx("input", { id: "primarySmtpAddress", type: "email", className: INPUT_CLASS, value: primarySmtpAddress, onChange: (e) => setPrimarySmtpAddress(e.target.value), placeholder: "support@example.com" }) })), _jsx(FormField, { label: "Display name", htmlFor: "displayName", children: _jsx("input", { id: "displayName", type: "text", className: INPUT_CLASS, value: displayName, onChange: (e) => setDisplayName(e.target.value), placeholder: "Support" }) }), _jsx(FormField, { label: "Owner user uid (optional)", htmlFor: "ownerUserUid", children: _jsx("input", { id: "ownerUserUid", type: "text", className: INPUT_CLASS, value: ownerUserUid, onChange: (e) => setOwnerUserUid(e.target.value), placeholder: "Leave blank for a shared mailbox" }) }), _jsx(FormField, { label: "Timezone", htmlFor: "timezone", children: _jsx("input", { id: "timezone", type: "text", className: INPUT_CLASS, value: timezone, onChange: (e) => setTimezone(e.target.value), placeholder: "America/Los_Angeles" }) }), _jsx(FormField, { label: "Quota (GB)", htmlFor: "quotaGb", children: _jsx("input", { id: "quotaGb", type: "number", min: 1, step: 1, className: INPUT_CLASS, value: quotaGb, onChange: (e) => setQuotaGb(Number(e.target.value)) }) }), _jsxs("label", { className: "flex items-center gap-2 text-sm mb-4", children: [_jsx("input", { type: "checkbox", checked: isResource, onChange: (e) => setIsResource(e.target.checked) }), "This is a resource mailbox (a bookable room or piece of equipment)"] }), isResource && (_jsxs("div", { className: "border border-border rounded-sm p-4 mb-4 flex flex-col gap-4", children: [_jsx(FormField, { label: "Resource type", htmlFor: "resourceType", children: _jsxs("select", { id: "resourceType", className: SELECT_CLASS, value: resourceType, onChange: (e) => setResourceType(e.target.value), children: [_jsx("option", { value: "room", children: "Room" }), _jsx("option", { value: "equipment", children: "Equipment" })] }) }), _jsx(FormField, { label: "Capacity (optional)", htmlFor: "resourceCapacity", children: _jsx("input", { id: "resourceCapacity", type: "number", min: 0, step: 1, className: INPUT_CLASS, value: resourceCapacity, onChange: (e) => setResourceCapacity(e.target.value), placeholder: "Informational only" }) }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: autoAcceptBookings, onChange: (e) => setAutoAcceptBookings(e.target.checked) }), "Automatically accept booking requests"] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: allowConflicts, onChange: (e) => setAllowConflicts(e.target.checked) }), "Allow conflicting bookings (skip conflict checking entirely)"] }), _jsx(FormField, { label: "Booking window, in days (optional)", htmlFor: "bookingWindowDays", children: _jsx("input", { id: "bookingWindowDays", type: "number", min: 0, step: 1, className: INPUT_CLASS, value: bookingWindowDays, onChange: (e) => setBookingWindowDays(e.target.value), placeholder: "No limit" }) }), _jsx(FormField, { label: "Maximum duration, in minutes (optional)", htmlFor: "maxDurationMinutes", children: _jsx("input", { id: "maxDurationMinutes", type: "number", min: 0, step: 1, className: INPUT_CLASS, value: maxDurationMinutes, onChange: (e) => setMaxDurationMinutes(e.target.value), placeholder: "No limit" }) })] })), _jsxs("div", { className: "flex gap-3 mt-2", children: [_jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Create mailbox" }), _jsx("a", { href: "/admin", children: _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", children: "Cancel" }) })] })] })] }));
|
|
5
|
+
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: "Leave \"Owner user uid\" blank to create a true ownerless shared mailbox (the Exchange \"shared mailbox\" concept) \u2014 access is then granted entirely to delegates afterward, not to a single owner. Set an owner to create a mailbox for a specific user instead." }), _jsx(MailboxCreateForm, { cancelHref: "/admin", onCreated: (mailbox) => {
|
|
6
|
+
window.location.href = `/admin/mailboxes/${encodeURIComponent(mailbox.uid)}`;
|
|
7
|
+
} })] }) }));
|
|
93
8
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import AdminShell from "../../shared/components/admin/layout/AdminShell.js";
|
|
3
|
+
import PluginsManager from "../../shared/components/admin/settings/PluginsManager.js";
|
|
4
|
+
export default function PluginsPage(props) {
|
|
5
|
+
return (_jsx(AdminShell, { ...props, active: "plugins", children: _jsx(PluginsManager, {}) }));
|
|
6
|
+
}
|
|
@@ -9,6 +9,7 @@ import { listQuarantine, releaseQuarantineEntry } from "@rapidmx/react-shared/ma
|
|
|
9
9
|
import AdminShell from "../../shared/components/admin/layout/AdminShell.js";
|
|
10
10
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
11
11
|
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
12
|
+
import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
|
|
12
13
|
const PAGE_SIZE = 25;
|
|
13
14
|
export function readMailboxUid() {
|
|
14
15
|
if (typeof window === "undefined")
|
|
@@ -24,7 +25,9 @@ function QuarantineContent({ userUid }) {
|
|
|
24
25
|
const [entries, setEntries] = useState([]);
|
|
25
26
|
const [loading, setLoading] = useState(true);
|
|
26
27
|
const [error, setError] = useState(null);
|
|
27
|
-
const [
|
|
28
|
+
const [releaseTarget, setReleaseTarget] = useState(null);
|
|
29
|
+
const [releasing, setReleasing] = useState(false);
|
|
30
|
+
const [releaseError, setReleaseError] = useState(null);
|
|
28
31
|
useEffect(() => {
|
|
29
32
|
setMailboxUid(readMailboxUid());
|
|
30
33
|
}, []);
|
|
@@ -42,25 +45,31 @@ function QuarantineContent({ userUid }) {
|
|
|
42
45
|
}
|
|
43
46
|
}, [mailboxUid, page]);
|
|
44
47
|
const hasNextPage = entries.length === PAGE_SIZE;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
function closeReleaseModal() {
|
|
49
|
+
setReleaseTarget(null);
|
|
50
|
+
setReleaseError(null);
|
|
51
|
+
}
|
|
52
|
+
// Only ever invoked from the confirmation modal below, which only renders once `releaseTarget` is set, once
|
|
53
|
+
// `mailboxUid` is known, and once `AdminShell` has confirmed `userUid` (children only render once authorized).
|
|
54
|
+
// `releaseQuarantineEntry()` still sends its own `releasedAt`/`releasedByUserUid`; the server now stamps both
|
|
55
|
+
// itself and ignores the client's values, so what's displayed afterwards comes from the reloaded list.
|
|
56
|
+
async function handleRelease() {
|
|
57
|
+
setReleasing(true);
|
|
58
|
+
setReleaseError(null);
|
|
51
59
|
try {
|
|
52
|
-
await releaseQuarantineEntry(
|
|
60
|
+
await releaseQuarantineEntry(releaseTarget.uid, releaseTarget.version, userUid);
|
|
61
|
+
closeReleaseModal();
|
|
53
62
|
reload(mailboxUid);
|
|
54
63
|
}
|
|
55
64
|
catch (err) {
|
|
56
|
-
|
|
65
|
+
setReleaseError(err instanceof ApiRequestError ? err.message : "Could not mark this message released.");
|
|
57
66
|
}
|
|
58
67
|
finally {
|
|
59
|
-
setReleasing(
|
|
68
|
+
setReleasing(false);
|
|
60
69
|
}
|
|
61
70
|
}
|
|
62
71
|
if (!mailboxUid) {
|
|
63
72
|
return _jsx(Alert, { children: "No mailbox specified. Open a mailbox's detail page and choose \"View quarantine\"." });
|
|
64
73
|
}
|
|
65
|
-
return (_jsxs("div", { className: "max-w-4xl flex flex-col gap-4", children: [_jsxs("div", { children: [_jsx("a", { href: `/admin/mailboxes/${encodeURIComponent(mailboxUid)}`, className: "text-sm text-primary-dark hover:underline", children: "\u2190 Back to mailbox" }), _jsx("h1", { className: "text-xl font-bold uppercase tracking-wide mt-1", children: "Quarantine" })] }), error && _jsx(Alert, { children: error }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : entries.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "Nothing quarantined for this mailbox." })) : (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm border-collapse", children: [_jsx("thead", { children: _jsx("tr", { children: ["Reason", "Quarantined", "Status", ""].map((h) => (_jsx("th", { className: "text-left text-xs uppercase tracking-wide text-text-muted py-2 px-2.5 border-b border-border", children: h }, h))) }) }), _jsx("tbody", { children: entries.map((entry) => (_jsxs("tr", { children: [_jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: entry.reason }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: new Date(entry.dateCreated).toLocaleString() }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: entry.releasedAt ? (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-success text-white", children: "Released" })) : (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-surface-alt text-text-muted", children: "Held" })) }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border text-right", children: !entry.releasedAt && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto",
|
|
74
|
+
return (_jsxs("div", { className: "max-w-4xl flex flex-col gap-4", children: [_jsxs("div", { children: [_jsx("a", { href: `/admin/mailboxes/${encodeURIComponent(mailboxUid)}`, className: "text-sm text-primary-dark hover:underline", children: "\u2190 Back to mailbox" }), _jsx("h1", { className: "text-xl font-bold uppercase tracking-wide mt-1", children: "Quarantine" })] }), error && _jsx(Alert, { children: error }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : entries.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "Nothing quarantined for this mailbox." })) : (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm border-collapse", children: [_jsx("thead", { children: _jsx("tr", { children: ["Reason", "Quarantined", "Status", ""].map((h) => (_jsx("th", { className: "text-left text-xs uppercase tracking-wide text-text-muted py-2 px-2.5 border-b border-border", children: h }, h))) }) }), _jsx("tbody", { children: entries.map((entry) => (_jsxs("tr", { children: [_jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: entry.reason }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: new Date(entry.dateCreated).toLocaleString() }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: entry.releasedAt ? (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-success text-white", children: "Released" })) : (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-surface-alt text-text-muted", children: "Held" })) }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border text-right", children: !entry.releasedAt && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setReleaseTarget(entry), children: "Mark released" })) })] }, entry.uid))) })] }) })), _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" })] }), _jsxs(Modal, { open: releaseTarget !== null, onClose: closeReleaseModal, title: "Mark message released", children: [_jsxs("dl", { className: "grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-sm mb-4", children: [_jsx("dt", { className: "text-text-muted", children: "Reason" }), _jsx("dd", { children: releaseTarget?.reason }), _jsx("dt", { className: "text-text-muted", children: "Quarantined" }), _jsx("dd", { children: releaseTarget && new Date(releaseTarget.dateCreated).toLocaleString() }), _jsx("dt", { className: "text-text-muted", children: "Message" }), _jsx("dd", { className: "break-all", children: releaseTarget?.originalMessageUid ?? "Not delivered to the mailbox" })] }), _jsx("p", { className: "text-sm mb-4", children: "This only records the entry as released (with you as the releaser). It does not deliver the message - if it\u2019s infected or unwanted, leave it held." }), releaseError && _jsx(Alert, { children: releaseError }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: releasing, onClick: closeReleaseModal, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", loading: releasing, disabled: releasing, onClick: handleRelease, children: "Mark released" })] })] })] }));
|
|
66
75
|
}
|