@rapidmx/web-client 0.3.0 → 0.4.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/branding/index.tsx +39 -404
- package/apps/admin/domains/[uid].tsx +92 -259
- package/apps/admin/encryption-policy/index.tsx +19 -0
- package/apps/admin/index.tsx +100 -82
- package/apps/admin/mailbox-policy/index.tsx +19 -0
- package/apps/admin/mailboxes/new/index.tsx +28 -291
- package/apps/admin/plugins/index.tsx +15 -0
- package/apps/admin/retention-policy/index.tsx +39 -132
- package/apps/admin/setup/index.tsx +15 -0
- package/apps/shared/components/admin/layout/AdminShell.tsx +249 -210
- package/apps/shared/components/admin/settings/BrandingForm.tsx +374 -0
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +176 -0
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +104 -0
- package/apps/shared/components/admin/settings/LoadedSettingsForm.tsx +34 -0
- package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +302 -0
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +119 -0
- package/apps/shared/components/admin/settings/PluginsManager.tsx +595 -0
- package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +98 -0
- package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +254 -0
- package/apps/shared/components/admin/setup/SetupWizard.tsx +300 -0
- package/apps/shared/components/calendar/CalendarListSidebar.tsx +120 -76
- package/apps/shared/components/calendar/EventModal.tsx +40 -4
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +80 -99
- package/apps/shared/components/contacts/ContactForm.tsx +38 -4
- package/apps/shared/components/layout/AppShell.tsx +198 -169
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +298 -265
- package/apps/shared/components/layout/UnlockPromptProvider.tsx +128 -0
- package/apps/shared/components/mail/MessageDetailPane.tsx +630 -595
- package/apps/shared/components/mail/compose/ComposeContext.tsx +8 -3
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +930 -765
- package/apps/shared/components/mail/layout/MailShell.tsx +408 -302
- package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -0
- package/apps/shared/mail/findWellKnownFolderUid.ts +13 -0
- package/apps/shared/search/LocalIndexLifecycle.tsx +89 -0
- package/apps/shared/search/localIndexBlockCipher.ts +83 -0
- package/apps/shared/search/localIndexBuilder.ts +179 -0
- package/apps/shared/search/localIndexKey.ts +27 -0
- package/apps/shared/search/localIndexRpcClient.ts +116 -0
- package/apps/shared/search/localIndexSchema.ts +227 -0
- package/apps/shared/search/localIndexSizePreference.ts +88 -0
- package/apps/shared/search/localIndexVFS.ts +235 -0
- package/apps/shared/search/localIndexWorker.ts +464 -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 +31 -21
- package/apps/www/contacts/index.tsx +33 -6
- package/apps/www/index.tsx +661 -106
- package/apps/www/messages/[uid].tsx +5 -1
- package/apps/www/settings/encryption/index.tsx +63 -1
- package/apps/www/settings/sharing/index.tsx +271 -0
- package/apps/www/tasks/index.tsx +53 -4
- package/dist/apps/admin/branding/index.js +4 -98
- package/dist/apps/admin/domains/[uid].js +5 -68
- package/dist/apps/admin/encryption-policy/index.js +8 -0
- package/dist/apps/admin/index.js +14 -1
- package/dist/apps/admin/mailbox-policy/index.js +8 -0
- package/dist/apps/admin/mailboxes/new/index.js +4 -89
- package/dist/apps/admin/plugins/index.js +6 -0
- package/dist/apps/admin/retention-policy/index.js +3 -36
- package/dist/apps/admin/setup/index.js +6 -0
- package/dist/apps/shared/components/admin/layout/AdminShell.js +34 -3
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +105 -0
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +77 -0
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +57 -0
- package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.js +25 -0
- package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +106 -0
- package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +52 -0
- package/dist/apps/shared/components/admin/settings/PluginsManager.js +260 -0
- package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +44 -0
- package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +131 -0
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +142 -0
- package/dist/apps/shared/components/calendar/CalendarListSidebar.js +24 -13
- package/dist/apps/shared/components/calendar/EventModal.js +16 -4
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +47 -39
- package/dist/apps/shared/components/contacts/ContactForm.js +16 -5
- package/dist/apps/shared/components/layout/AppShell.js +30 -7
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +15 -3
- package/dist/apps/shared/components/layout/UnlockPromptProvider.js +77 -0
- package/dist/apps/shared/components/mail/MessageDetailPane.js +27 -2
- package/dist/apps/shared/components/mail/compose/ComposeContext.js +2 -2
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +134 -14
- package/dist/apps/shared/components/mail/layout/MailShell.js +106 -40
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -0
- package/dist/apps/shared/mail/findWellKnownFolderUid.js +12 -0
- package/dist/apps/shared/search/LocalIndexLifecycle.js +76 -0
- package/dist/apps/shared/search/localIndexBlockCipher.js +63 -0
- package/dist/apps/shared/search/localIndexBuilder.js +153 -0
- package/dist/apps/shared/search/localIndexKey.js +25 -0
- package/dist/apps/shared/search/localIndexRpcClient.js +78 -0
- package/dist/apps/shared/search/localIndexSchema.js +179 -0
- package/dist/apps/shared/search/localIndexSizePreference.js +78 -0
- package/dist/apps/shared/search/localIndexVFS.js +230 -0
- package/dist/apps/shared/search/localIndexWorker.js +341 -0
- package/dist/apps/shared/search/searchTier2.js +38 -0
- package/dist/apps/www/calendar/index.js +14 -12
- package/dist/apps/www/contacts/index.js +15 -6
- package/dist/apps/www/index.js +496 -94
- package/dist/apps/www/messages/[uid].js +5 -1
- package/dist/apps/www/settings/encryption/index.js +30 -2
- package/dist/apps/www/settings/sharing/index.js +128 -0
- package/dist/apps/www/tasks/index.js +27 -5
- package/package.json +3 -2
|
@@ -1,291 +1,28 @@
|
|
|
1
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
-
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
-
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
-
import React
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const [domains, setDomains] = useState<string[]>([]);
|
|
30
|
-
const [domain, setDomain] = useState("");
|
|
31
|
-
const [displayName, setDisplayName] = useState("");
|
|
32
|
-
const [ownerUserUid, setOwnerUserUid] = useState("");
|
|
33
|
-
const [timezone, setTimezone] = useState("UTC");
|
|
34
|
-
const [quotaGb, setQuotaGb] = useState(5);
|
|
35
|
-
const [isResource, setIsResource] = useState(false);
|
|
36
|
-
const [resourceType, setResourceType] = useState<"room" | "equipment">("room");
|
|
37
|
-
const [resourceCapacity, setResourceCapacity] = useState("");
|
|
38
|
-
const [autoAcceptBookings, setAutoAcceptBookings] = useState(false);
|
|
39
|
-
const [allowConflicts, setAllowConflicts] = useState(false);
|
|
40
|
-
const [bookingWindowDays, setBookingWindowDays] = useState("");
|
|
41
|
-
const [maxDurationMinutes, setMaxDurationMinutes] = useState("");
|
|
42
|
-
const [error, setError] = useState<string | null>(null);
|
|
43
|
-
const [saving, setSaving] = useState(false);
|
|
44
|
-
|
|
45
|
-
// An empty `mail:domains` list (the default) means this server enforces no domain restriction —
|
|
46
|
-
// the address field stays free text, exactly as before. Once an admin configures at least one
|
|
47
|
-
// domain, every mailbox this server creates must be on one of them (enforced server-side too, in
|
|
48
|
-
// `BaseMailboxRoute.create()`), so the form switches to a local-part input + domain dropdown to
|
|
49
|
-
// make that restriction visible rather than let an admin discover it only via a rejected submit.
|
|
50
|
-
useEffect(() => {
|
|
51
|
-
listMailboxDomains()
|
|
52
|
-
.then((list) => {
|
|
53
|
-
setDomains(list);
|
|
54
|
-
setDomain(list[0] ?? "");
|
|
55
|
-
})
|
|
56
|
-
.catch(() => undefined);
|
|
57
|
-
}, []);
|
|
58
|
-
|
|
59
|
-
const constrained = domains.length > 0;
|
|
60
|
-
|
|
61
|
-
async function handleSubmit(e: FormEvent) {
|
|
62
|
-
e.preventDefault();
|
|
63
|
-
setError(null);
|
|
64
|
-
|
|
65
|
-
const address = constrained ? `${localPart.trim()}@${domain}` : primarySmtpAddress.trim();
|
|
66
|
-
if (constrained ? !localPart.trim() : !primarySmtpAddress.trim()) {
|
|
67
|
-
setError("A primary SMTP address is required.");
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
if (!displayName.trim()) {
|
|
71
|
-
setError("A display name is required.");
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
setSaving(true);
|
|
76
|
-
try {
|
|
77
|
-
// ownerUserUid left blank creates a true ownerless shared mailbox (e.g. support@example.com);
|
|
78
|
-
// delegates are then granted access from the mailbox's detail page. A resource mailbox is
|
|
79
|
-
// ownerless the same way — leave "Owner user uid" blank for one too.
|
|
80
|
-
const mailbox = await createMailbox({
|
|
81
|
-
primarySmtpAddress: address,
|
|
82
|
-
displayName: displayName.trim(),
|
|
83
|
-
ownerUserUid: ownerUserUid.trim() || undefined,
|
|
84
|
-
timezone,
|
|
85
|
-
quotaBytes: Math.round(quotaGb * 1_000_000_000),
|
|
86
|
-
...(isResource && {
|
|
87
|
-
isResource: true,
|
|
88
|
-
resourceType,
|
|
89
|
-
resourceCapacity: resourceCapacity ? Number(resourceCapacity) : undefined,
|
|
90
|
-
autoAcceptBookings,
|
|
91
|
-
allowConflicts,
|
|
92
|
-
bookingWindowDays: bookingWindowDays ? Number(bookingWindowDays) : undefined,
|
|
93
|
-
maxDurationMinutes: maxDurationMinutes ? Number(maxDurationMinutes) : undefined,
|
|
94
|
-
}),
|
|
95
|
-
});
|
|
96
|
-
window.location.href = `/admin/mailboxes/${encodeURIComponent(mailbox.uid)}`;
|
|
97
|
-
} catch (err) {
|
|
98
|
-
setError(err instanceof ApiRequestError ? err.message : "Could not create the mailbox.");
|
|
99
|
-
} finally {
|
|
100
|
-
setSaving(false);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return (
|
|
105
|
-
<div className="max-w-xl">
|
|
106
|
-
<h1 className="text-xl font-bold uppercase tracking-wide mb-1">New mailbox</h1>
|
|
107
|
-
<p className="text-sm text-text-muted mb-5">
|
|
108
|
-
Leave "Owner user uid" blank to create a true ownerless shared mailbox (the Exchange "shared
|
|
109
|
-
mailbox" concept) — access is then granted entirely to delegates afterward, not to a single
|
|
110
|
-
owner. Set an owner to create a mailbox for a specific user instead.
|
|
111
|
-
</p>
|
|
112
|
-
|
|
113
|
-
{error && <Alert>{error}</Alert>}
|
|
114
|
-
|
|
115
|
-
<form onSubmit={handleSubmit} className="bg-surface border border-border rounded-md p-6">
|
|
116
|
-
{constrained ? (
|
|
117
|
-
<FormField label="Local part" htmlFor="localPart">
|
|
118
|
-
<div className="flex gap-2 items-center">
|
|
119
|
-
<input
|
|
120
|
-
id="localPart"
|
|
121
|
-
type="text"
|
|
122
|
-
className={INPUT_CLASS}
|
|
123
|
-
value={localPart}
|
|
124
|
-
onChange={(e) => setLocalPart(e.target.value)}
|
|
125
|
-
placeholder="support"
|
|
126
|
-
/>
|
|
127
|
-
<span className="text-text-muted shrink-0">@</span>
|
|
128
|
-
<select
|
|
129
|
-
aria-label="Domain"
|
|
130
|
-
className={SELECT_CLASS}
|
|
131
|
-
value={domain}
|
|
132
|
-
onChange={(e) => setDomain(e.target.value)}
|
|
133
|
-
>
|
|
134
|
-
{domains.map((d) => (
|
|
135
|
-
<option key={d} value={d}>
|
|
136
|
-
{d}
|
|
137
|
-
</option>
|
|
138
|
-
))}
|
|
139
|
-
</select>
|
|
140
|
-
</div>
|
|
141
|
-
</FormField>
|
|
142
|
-
) : (
|
|
143
|
-
<FormField label="Primary SMTP address" htmlFor="primarySmtpAddress">
|
|
144
|
-
<input
|
|
145
|
-
id="primarySmtpAddress"
|
|
146
|
-
type="email"
|
|
147
|
-
className={INPUT_CLASS}
|
|
148
|
-
value={primarySmtpAddress}
|
|
149
|
-
onChange={(e) => setPrimarySmtpAddress(e.target.value)}
|
|
150
|
-
placeholder="support@example.com"
|
|
151
|
-
/>
|
|
152
|
-
</FormField>
|
|
153
|
-
)}
|
|
154
|
-
|
|
155
|
-
<FormField label="Display name" htmlFor="displayName">
|
|
156
|
-
<input
|
|
157
|
-
id="displayName"
|
|
158
|
-
type="text"
|
|
159
|
-
className={INPUT_CLASS}
|
|
160
|
-
value={displayName}
|
|
161
|
-
onChange={(e) => setDisplayName(e.target.value)}
|
|
162
|
-
placeholder="Support"
|
|
163
|
-
/>
|
|
164
|
-
</FormField>
|
|
165
|
-
|
|
166
|
-
<FormField label="Owner user uid (optional)" htmlFor="ownerUserUid">
|
|
167
|
-
<input
|
|
168
|
-
id="ownerUserUid"
|
|
169
|
-
type="text"
|
|
170
|
-
className={INPUT_CLASS}
|
|
171
|
-
value={ownerUserUid}
|
|
172
|
-
onChange={(e) => setOwnerUserUid(e.target.value)}
|
|
173
|
-
placeholder="Leave blank for a shared mailbox"
|
|
174
|
-
/>
|
|
175
|
-
</FormField>
|
|
176
|
-
|
|
177
|
-
<FormField label="Timezone" htmlFor="timezone">
|
|
178
|
-
<input
|
|
179
|
-
id="timezone"
|
|
180
|
-
type="text"
|
|
181
|
-
className={INPUT_CLASS}
|
|
182
|
-
value={timezone}
|
|
183
|
-
onChange={(e) => setTimezone(e.target.value)}
|
|
184
|
-
placeholder="America/Los_Angeles"
|
|
185
|
-
/>
|
|
186
|
-
</FormField>
|
|
187
|
-
|
|
188
|
-
<FormField label="Quota (GB)" htmlFor="quotaGb">
|
|
189
|
-
<input
|
|
190
|
-
id="quotaGb"
|
|
191
|
-
type="number"
|
|
192
|
-
min={1}
|
|
193
|
-
step={1}
|
|
194
|
-
className={INPUT_CLASS}
|
|
195
|
-
value={quotaGb}
|
|
196
|
-
onChange={(e) => setQuotaGb(Number(e.target.value))}
|
|
197
|
-
/>
|
|
198
|
-
</FormField>
|
|
199
|
-
|
|
200
|
-
<label className="flex items-center gap-2 text-sm mb-4">
|
|
201
|
-
<input type="checkbox" checked={isResource} onChange={(e) => setIsResource(e.target.checked)} />
|
|
202
|
-
This is a resource mailbox (a bookable room or piece of equipment)
|
|
203
|
-
</label>
|
|
204
|
-
|
|
205
|
-
{isResource && (
|
|
206
|
-
<div className="border border-border rounded-sm p-4 mb-4 flex flex-col gap-4">
|
|
207
|
-
<FormField label="Resource type" htmlFor="resourceType">
|
|
208
|
-
<select
|
|
209
|
-
id="resourceType"
|
|
210
|
-
className={SELECT_CLASS}
|
|
211
|
-
value={resourceType}
|
|
212
|
-
onChange={(e) => setResourceType(e.target.value as "room" | "equipment")}
|
|
213
|
-
>
|
|
214
|
-
<option value="room">Room</option>
|
|
215
|
-
<option value="equipment">Equipment</option>
|
|
216
|
-
</select>
|
|
217
|
-
</FormField>
|
|
218
|
-
|
|
219
|
-
<FormField label="Capacity (optional)" htmlFor="resourceCapacity">
|
|
220
|
-
<input
|
|
221
|
-
id="resourceCapacity"
|
|
222
|
-
type="number"
|
|
223
|
-
min={0}
|
|
224
|
-
step={1}
|
|
225
|
-
className={INPUT_CLASS}
|
|
226
|
-
value={resourceCapacity}
|
|
227
|
-
onChange={(e) => setResourceCapacity(e.target.value)}
|
|
228
|
-
placeholder="Informational only"
|
|
229
|
-
/>
|
|
230
|
-
</FormField>
|
|
231
|
-
|
|
232
|
-
<label className="flex items-center gap-2 text-sm">
|
|
233
|
-
<input
|
|
234
|
-
type="checkbox"
|
|
235
|
-
checked={autoAcceptBookings}
|
|
236
|
-
onChange={(e) => setAutoAcceptBookings(e.target.checked)}
|
|
237
|
-
/>
|
|
238
|
-
Automatically accept booking requests
|
|
239
|
-
</label>
|
|
240
|
-
|
|
241
|
-
<label className="flex items-center gap-2 text-sm">
|
|
242
|
-
<input
|
|
243
|
-
type="checkbox"
|
|
244
|
-
checked={allowConflicts}
|
|
245
|
-
onChange={(e) => setAllowConflicts(e.target.checked)}
|
|
246
|
-
/>
|
|
247
|
-
Allow conflicting bookings (skip conflict checking entirely)
|
|
248
|
-
</label>
|
|
249
|
-
|
|
250
|
-
<FormField label="Booking window, in days (optional)" htmlFor="bookingWindowDays">
|
|
251
|
-
<input
|
|
252
|
-
id="bookingWindowDays"
|
|
253
|
-
type="number"
|
|
254
|
-
min={0}
|
|
255
|
-
step={1}
|
|
256
|
-
className={INPUT_CLASS}
|
|
257
|
-
value={bookingWindowDays}
|
|
258
|
-
onChange={(e) => setBookingWindowDays(e.target.value)}
|
|
259
|
-
placeholder="No limit"
|
|
260
|
-
/>
|
|
261
|
-
</FormField>
|
|
262
|
-
|
|
263
|
-
<FormField label="Maximum duration, in minutes (optional)" htmlFor="maxDurationMinutes">
|
|
264
|
-
<input
|
|
265
|
-
id="maxDurationMinutes"
|
|
266
|
-
type="number"
|
|
267
|
-
min={0}
|
|
268
|
-
step={1}
|
|
269
|
-
className={INPUT_CLASS}
|
|
270
|
-
value={maxDurationMinutes}
|
|
271
|
-
onChange={(e) => setMaxDurationMinutes(e.target.value)}
|
|
272
|
-
placeholder="No limit"
|
|
273
|
-
/>
|
|
274
|
-
</FormField>
|
|
275
|
-
</div>
|
|
276
|
-
)}
|
|
277
|
-
|
|
278
|
-
<div className="flex gap-3 mt-2">
|
|
279
|
-
<Button type="submit" loading={saving} disabled={saving} className="!w-auto">
|
|
280
|
-
Create mailbox
|
|
281
|
-
</Button>
|
|
282
|
-
<a href="/admin">
|
|
283
|
-
<Button type="button" variant="secondary" className="!w-auto">
|
|
284
|
-
Cancel
|
|
285
|
-
</Button>
|
|
286
|
-
</a>
|
|
287
|
-
</div>
|
|
288
|
-
</form>
|
|
289
|
-
</div>
|
|
290
|
-
);
|
|
291
|
-
}
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React from "react";
|
|
6
|
+
import AdminShell, { AdminShellProps } from "../../../shared/components/admin/layout/AdminShell.js";
|
|
7
|
+
import MailboxCreateForm from "../../../shared/components/admin/settings/MailboxCreateForm.js";
|
|
8
|
+
|
|
9
|
+
export default function NewMailboxPage(props: Omit<AdminShellProps, "active">) {
|
|
10
|
+
return (
|
|
11
|
+
<AdminShell {...props} active="mailboxes">
|
|
12
|
+
<div className="max-w-xl">
|
|
13
|
+
<h1 className="text-xl font-bold uppercase tracking-wide mb-1">New mailbox</h1>
|
|
14
|
+
<p className="text-sm text-text-muted mb-5">
|
|
15
|
+
Leave "Owner user uid" blank to create a true ownerless shared mailbox (the Exchange "shared
|
|
16
|
+
mailbox" concept) — access is then granted entirely to delegates afterward, not to a single
|
|
17
|
+
owner. Set an owner to create a mailbox for a specific user instead.
|
|
18
|
+
</p>
|
|
19
|
+
<MailboxCreateForm
|
|
20
|
+
cancelHref="/admin"
|
|
21
|
+
onCreated={(mailbox) => {
|
|
22
|
+
window.location.href = `/admin/mailboxes/${encodeURIComponent(mailbox.uid)}`;
|
|
23
|
+
}}
|
|
24
|
+
/>
|
|
25
|
+
</div>
|
|
26
|
+
</AdminShell>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React from "react";
|
|
6
|
+
import AdminShell, { AdminShellProps } from "../../shared/components/admin/layout/AdminShell.js";
|
|
7
|
+
import PluginsManager from "../../shared/components/admin/settings/PluginsManager.js";
|
|
8
|
+
|
|
9
|
+
export default function PluginsPage(props: Omit<AdminShellProps, "active">) {
|
|
10
|
+
return (
|
|
11
|
+
<AdminShell {...props} active="plugins">
|
|
12
|
+
<PluginsManager />
|
|
13
|
+
</AdminShell>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -1,132 +1,39 @@
|
|
|
1
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
-
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
-
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
-
import React, {
|
|
6
|
-
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (loading) {
|
|
41
|
-
return <p className="text-sm text-text-muted">Loading…</p>;
|
|
42
|
-
}
|
|
43
|
-
if (loadError || !policy) {
|
|
44
|
-
return <Alert>{loadError}</Alert>;
|
|
45
|
-
}
|
|
46
|
-
return <RetentionPolicyForm policy={policy} onChange={setPolicy} />;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function RetentionPolicyForm({ policy, onChange }: { policy: RetentionPolicy; onChange: (p: RetentionPolicy) => void }) {
|
|
50
|
-
const [messageRetentionDays, setMessageRetentionDays] = useState(policy.messageRetentionDays?.toString() ?? "");
|
|
51
|
-
const [auditLogRetentionDays, setAuditLogRetentionDays] = useState(policy.auditLogRetentionDays?.toString() ?? "");
|
|
52
|
-
const [error, setError] = useState<string | null>(null);
|
|
53
|
-
const [saving, setSaving] = useState(false);
|
|
54
|
-
const [saved, setSaved] = useState(false);
|
|
55
|
-
|
|
56
|
-
async function handleSubmit(e: FormEvent) {
|
|
57
|
-
e.preventDefault();
|
|
58
|
-
setError(null);
|
|
59
|
-
setSaved(false);
|
|
60
|
-
setSaving(true);
|
|
61
|
-
try {
|
|
62
|
-
const patch: Partial<RetentionPolicy> = {};
|
|
63
|
-
if (messageRetentionDays.trim() !== "") {
|
|
64
|
-
patch.messageRetentionDays = Number(messageRetentionDays);
|
|
65
|
-
}
|
|
66
|
-
if (auditLogRetentionDays.trim() !== "") {
|
|
67
|
-
patch.auditLogRetentionDays = Number(auditLogRetentionDays);
|
|
68
|
-
}
|
|
69
|
-
const updated = await updateRetentionPolicy(patch);
|
|
70
|
-
onChange(updated);
|
|
71
|
-
setSaved(true);
|
|
72
|
-
} catch (err) {
|
|
73
|
-
setError(err instanceof ApiRequestError ? err.message : "Could not save the retention policy.");
|
|
74
|
-
} finally {
|
|
75
|
-
setSaving(false);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return (
|
|
80
|
-
<div className="max-w-xl">
|
|
81
|
-
<h1 className="text-xl font-bold uppercase tracking-wide mb-1">Retention Policy</h1>
|
|
82
|
-
<p className="text-sm text-text-muted mb-5">
|
|
83
|
-
Automatically and permanently deletes mail/audit-log entries older than the configured age.
|
|
84
|
-
Leave a field blank for no automatic purge. This does not apply to a mailbox that is a
|
|
85
|
-
custodian on an active legal hold — a held message is skipped and retried on a later run once
|
|
86
|
-
the hold is lifted.
|
|
87
|
-
</p>
|
|
88
|
-
|
|
89
|
-
{error && <Alert>{error}</Alert>}
|
|
90
|
-
{saved && !error && <div className="mb-4 text-sm text-success font-medium">Saved.</div>}
|
|
91
|
-
|
|
92
|
-
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
|
93
|
-
<label className="flex flex-col gap-1.5 text-sm">
|
|
94
|
-
<span className="font-semibold">Message retention (days)</span>
|
|
95
|
-
<input
|
|
96
|
-
aria-label="Message retention (days)"
|
|
97
|
-
type="number"
|
|
98
|
-
min={1}
|
|
99
|
-
className={INPUT_CLASS}
|
|
100
|
-
value={messageRetentionDays}
|
|
101
|
-
onChange={(e) => setMessageRetentionDays(e.target.value)}
|
|
102
|
-
placeholder="No automatic purge"
|
|
103
|
-
/>
|
|
104
|
-
<span className="text-xs text-text-muted">
|
|
105
|
-
Applies to any message, in any folder, regardless of age.
|
|
106
|
-
</span>
|
|
107
|
-
</label>
|
|
108
|
-
<label className="flex flex-col gap-1.5 text-sm">
|
|
109
|
-
<span className="font-semibold">Audit log retention (days)</span>
|
|
110
|
-
<input
|
|
111
|
-
aria-label="Audit log retention (days)"
|
|
112
|
-
type="number"
|
|
113
|
-
min={MIN_AUDIT_LOG_RETENTION_DAYS}
|
|
114
|
-
className={INPUT_CLASS}
|
|
115
|
-
value={auditLogRetentionDays}
|
|
116
|
-
onChange={(e) => setAuditLogRetentionDays(e.target.value)}
|
|
117
|
-
placeholder="Keep forever"
|
|
118
|
-
/>
|
|
119
|
-
<span className="text-xs text-text-muted">
|
|
120
|
-
Cannot be set below {MIN_AUDIT_LOG_RETENTION_DAYS} days. Never applies to the
|
|
121
|
-
tamper-evident escrow audit chain.
|
|
122
|
-
</span>
|
|
123
|
-
</label>
|
|
124
|
-
<div>
|
|
125
|
-
<Button type="submit" loading={saving} disabled={saving} className="!w-auto">
|
|
126
|
-
Save
|
|
127
|
-
</Button>
|
|
128
|
-
</div>
|
|
129
|
-
</form>
|
|
130
|
-
</div>
|
|
131
|
-
);
|
|
132
|
-
}
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React, { useEffect, useState } from "react";
|
|
6
|
+
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
7
|
+
import { RetentionPolicy, getRetentionPolicy } from "@rapidmx/react-shared/admin/retentionPolicyApi.js";
|
|
8
|
+
import AdminShell, { AdminShellProps } from "../../shared/components/admin/layout/AdminShell.js";
|
|
9
|
+
import RetentionPolicyForm from "../../shared/components/admin/settings/RetentionPolicyForm.js";
|
|
10
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
11
|
+
|
|
12
|
+
export default function RetentionPolicyPage(props: Omit<AdminShellProps, "active">) {
|
|
13
|
+
return (
|
|
14
|
+
<AdminShell {...props} active="retentionPolicy">
|
|
15
|
+
<RetentionPolicyContent />
|
|
16
|
+
</AdminShell>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function RetentionPolicyContent() {
|
|
21
|
+
const [policy, setPolicy] = useState<RetentionPolicy | null>(null);
|
|
22
|
+
const [loading, setLoading] = useState(true);
|
|
23
|
+
const [loadError, setLoadError] = useState<string | null>(null);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
getRetentionPolicy()
|
|
27
|
+
.then(setPolicy)
|
|
28
|
+
.catch((err) => setLoadError(err instanceof ApiRequestError ? err.message : "Could not load the retention policy."))
|
|
29
|
+
.finally(() => setLoading(false));
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
if (loading) {
|
|
33
|
+
return <p className="text-sm text-text-muted">Loading…</p>;
|
|
34
|
+
}
|
|
35
|
+
if (loadError || !policy) {
|
|
36
|
+
return <Alert>{loadError}</Alert>;
|
|
37
|
+
}
|
|
38
|
+
return <RetentionPolicyForm policy={policy} onChange={setPolicy} />;
|
|
39
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React from "react";
|
|
6
|
+
import AdminShell, { AdminShellProps } from "../../shared/components/admin/layout/AdminShell.js";
|
|
7
|
+
import SetupWizard from "../../shared/components/admin/setup/SetupWizard.js";
|
|
8
|
+
|
|
9
|
+
export default function SetupPage(props: Omit<AdminShellProps, "active">) {
|
|
10
|
+
return (
|
|
11
|
+
<AdminShell {...props} active="setup">
|
|
12
|
+
<SetupWizard userUid={props.userUid ?? ""} />
|
|
13
|
+
</AdminShell>
|
|
14
|
+
);
|
|
15
|
+
}
|