@rapidmx/web-client 0.6.0 → 0.7.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/shared/components/admin/settings/BrandingForm.tsx +398 -385
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +119 -112
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +159 -152
- package/apps/shared/components/admin/settings/PluginsManager.tsx +1288 -1235
- package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +9 -1
- package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +288 -290
- package/apps/shared/components/admin/setup/SetupWizard.tsx +443 -416
- package/apps/shared/components/layout/AppShell.tsx +288 -288
- package/apps/shared/components/mail/ConversationList.tsx +225 -33
- package/apps/shared/components/mail/ConversationThreadPane.tsx +434 -216
- package/apps/shared/components/mail/MailListToolbar.tsx +265 -0
- package/apps/shared/components/mail/MailSelectionBar.tsx +222 -0
- package/apps/shared/components/mail/MenuButton.tsx +384 -0
- package/apps/shared/components/mail/MessageDetailPane.tsx +280 -215
- package/apps/shared/components/mail/MoveToFolderDialog.tsx +263 -0
- package/apps/shared/components/mail/compose/ComposeContext.tsx +29 -2
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +1700 -1652
- package/apps/shared/components/mail/compose/RecipientInput.tsx +356 -0
- package/apps/shared/components/mail/compose/RichTextEditor.tsx +38 -3
- package/apps/shared/components/mail/compose/quotedBody.ts +100 -0
- package/apps/shared/components/mail/compose/recipients.ts +104 -0
- package/apps/shared/components/mail/labelMenu.tsx +278 -0
- package/apps/shared/components/mail/layout/MailShell.tsx +445 -420
- package/apps/shared/components/mail/listPreferences.ts +228 -0
- package/apps/shared/components/settings/layout/SettingsShell.tsx +0 -1
- package/apps/shared/styles/app.css +28 -5
- package/apps/www/index.tsx +1854 -1314
- package/apps/www/messages/[uid].tsx +5 -4
- package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +7 -3
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +6 -5
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.d.ts +4 -1
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +2 -2
- package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.d.ts +4 -1
- package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +2 -2
- package/dist/apps/shared/components/admin/settings/PluginsManager.d.ts +8 -1
- package/dist/apps/shared/components/admin/settings/PluginsManager.js +33 -14
- package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.d.ts +4 -1
- package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +2 -2
- package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +1 -1
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +14 -10
- package/dist/apps/shared/components/mail/ConversationList.d.ts +42 -6
- package/dist/apps/shared/components/mail/ConversationList.js +84 -10
- package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +52 -13
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +217 -60
- package/dist/apps/shared/components/mail/MailListToolbar.d.ts +55 -0
- package/dist/apps/shared/components/mail/MailListToolbar.js +149 -0
- package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +59 -0
- package/dist/apps/shared/components/mail/MailSelectionBar.js +48 -0
- package/dist/apps/shared/components/mail/MenuButton.d.ts +71 -0
- package/dist/apps/shared/components/mail/MenuButton.js +211 -0
- package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +27 -19
- package/dist/apps/shared/components/mail/MessageDetailPane.js +126 -129
- package/dist/apps/shared/components/mail/MoveToFolderDialog.d.ts +57 -0
- package/dist/apps/shared/components/mail/MoveToFolderDialog.js +138 -0
- package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +16 -1
- package/dist/apps/shared/components/mail/compose/ComposeContext.js +3 -1
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +52 -20
- package/dist/apps/shared/components/mail/compose/RecipientInput.d.ts +36 -0
- package/dist/apps/shared/components/mail/compose/RecipientInput.js +203 -0
- package/dist/apps/shared/components/mail/compose/RichTextEditor.d.ts +10 -1
- package/dist/apps/shared/components/mail/compose/RichTextEditor.js +21 -3
- package/dist/apps/shared/components/mail/compose/quotedBody.d.ts +37 -0
- package/dist/apps/shared/components/mail/compose/quotedBody.js +85 -0
- package/dist/apps/shared/components/mail/compose/recipients.d.ts +24 -0
- package/dist/apps/shared/components/mail/compose/recipients.js +87 -0
- package/dist/apps/shared/components/mail/labelMenu.d.ts +97 -0
- package/dist/apps/shared/components/mail/labelMenu.js +134 -0
- package/dist/apps/shared/components/mail/layout/MailShell.d.ts +8 -0
- package/dist/apps/shared/components/mail/layout/MailShell.js +15 -3
- package/dist/apps/shared/components/mail/listPreferences.d.ts +101 -0
- package/dist/apps/shared/components/mail/listPreferences.js +186 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +0 -1
- package/dist/apps/shared/styles/app.css +28 -5
- package/dist/apps/www/index.js +506 -112
- package/dist/apps/www/messages/[uid].js +2 -3
- package/package.json +2 -2
- package/apps/www/settings/focused-inbox/index.tsx +0 -150
- package/dist/apps/www/settings/focused-inbox/index.d.ts +0 -4
- package/dist/apps/www/settings/focused-inbox/index.js +0 -71
|
@@ -24,7 +24,7 @@ export default function MessageDetailPage(props: MailShellProps & { params: { ui
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
function MessageDetailContent({ uid }: { uid: string }) {
|
|
27
|
-
const { mailboxFolders, mailboxUid } = useMailShell();
|
|
27
|
+
const { mailboxFolders, mailboxUid, onFolderCreated } = useMailShell();
|
|
28
28
|
const [message, setMessage] = useState<Message | null>(null);
|
|
29
29
|
const [loading, setLoading] = useState(true);
|
|
30
30
|
const [error, setError] = useState<string | null>(null);
|
|
@@ -77,7 +77,6 @@ function MessageDetailContent({ uid }: { uid: string }) {
|
|
|
77
77
|
const backHref = `/?mailboxUid=${encodeURIComponent(message.mailboxUid)}&folderUid=${encodeURIComponent(message.folderUid)}`;
|
|
78
78
|
const isSentItems = folders.find((f) => f.uid === message.folderUid)?.type === "sent_items";
|
|
79
79
|
const isOutbox = folders.find((f) => f.uid === message.folderUid)?.type === "outbox";
|
|
80
|
-
const isInbox = folders.find((f) => f.uid === message.folderUid)?.type === "inbox";
|
|
81
80
|
const draftsFolderUid = folders.find((f) => f.type === "drafts")?.uid;
|
|
82
81
|
return (
|
|
83
82
|
<MessageDetailPane
|
|
@@ -87,14 +86,16 @@ function MessageDetailContent({ uid }: { uid: string }) {
|
|
|
87
86
|
isSentItems={isSentItems}
|
|
88
87
|
onRecalled={setMessage}
|
|
89
88
|
isOutbox={isOutbox}
|
|
90
|
-
isInbox={isInbox}
|
|
91
|
-
onClassified={setMessage}
|
|
92
89
|
onReceiptHandled={setMessage}
|
|
93
90
|
draftsFolderUid={draftsFolderUid}
|
|
91
|
+
folders={folders}
|
|
92
|
+
onMoved={setMessage}
|
|
93
|
+
onFolderCreated={onFolderCreated}
|
|
94
94
|
onScheduledSendCanceled={setMessage}
|
|
95
95
|
onArchived={setMessage}
|
|
96
96
|
labels={labels}
|
|
97
97
|
onLabelsChanged={setMessage}
|
|
98
|
+
onLabelCreated={(label) => setLabels((prev) => [...prev, label])}
|
|
98
99
|
/>
|
|
99
100
|
);
|
|
100
101
|
}
|
|
@@ -2,8 +2,12 @@ import React from "react";
|
|
|
2
2
|
import { Branding } from "@rapidmx/react-shared/branding/brandingApi.js";
|
|
3
3
|
/** SVG logo/icon uploads are refused (the server refuses them too): an SVG served from this origin can carry script. */
|
|
4
4
|
export declare function isSvgFile(file: File): boolean;
|
|
5
|
-
|
|
6
|
-
export default function BrandingForm({ branding, onChange }: {
|
|
5
|
+
export interface BrandingFormProps {
|
|
7
6
|
branding: Branding;
|
|
8
7
|
onChange: (b: Branding) => void;
|
|
9
|
-
|
|
8
|
+
/** Set when shown in a page that already introduces branding (the setup wizard): the title and introduction are
|
|
9
|
+
* left out, and the section headings sit one level lower. */
|
|
10
|
+
embedded?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/** The branding editor, shared by the Branding page and the setup wizard. */
|
|
13
|
+
export default function BrandingForm({ branding, onChange, embedded }: BrandingFormProps): React.JSX.Element;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
///////////////////////////////////////////////////////////////////////////////
|
|
3
3
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
@@ -16,7 +16,8 @@ export function isSvgFile(file) {
|
|
|
16
16
|
}
|
|
17
17
|
const RASTER_IMAGE_ACCEPT = "image/png,image/jpeg,image/gif,image/webp,image/x-icon,image/vnd.microsoft.icon";
|
|
18
18
|
/** The branding editor, shared by the Branding page and the setup wizard. */
|
|
19
|
-
export default function BrandingForm({ branding, onChange }) {
|
|
19
|
+
export default function BrandingForm({ branding, onChange, embedded = false }) {
|
|
20
|
+
const SectionHeading = embedded ? "h3" : "h2";
|
|
20
21
|
const [companyName, setCompanyName] = useState(branding.companyName);
|
|
21
22
|
const [title, setTitle] = useState(branding.title);
|
|
22
23
|
const [headerHtml, setHeaderHtml] = useState(branding.headerHtml ?? "");
|
|
@@ -103,13 +104,13 @@ export default function BrandingForm({ branding, onChange }) {
|
|
|
103
104
|
setSaving(false);
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
|
-
return (_jsxs("div", { className: "max-w-2xl", children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide mb-1", children: "Branding" }), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "Customize the logo, nav-header icon, product name, and chrome shown to every visitor of the webmail and admin console \u2014 including anonymous booking-page visitors." }), error && _jsx(Alert, { children: error }), saved && !error && _jsx("div", { className: "mb-4 text-sm text-success font-medium", children: "Saved." }), _jsxs("div", { className: "flex flex-col gap-6", children: [_jsxs("section", { className: "flex flex-col gap-3", children: [_jsx(
|
|
107
|
+
return (_jsxs("div", { className: "max-w-2xl", children: [!embedded && (_jsxs(_Fragment, { children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide mb-1", children: "Branding" }), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "Customize the logo, nav-header icon, product name, and chrome shown to every visitor of the webmail and admin console \u2014 including anonymous booking-page visitors." })] })), error && _jsx(Alert, { children: error }), saved && !error && _jsx("div", { className: "mb-4 text-sm text-success font-medium", children: "Saved." }), _jsxs("div", { className: "flex flex-col gap-6", children: [_jsxs("section", { className: "flex flex-col gap-3", children: [_jsx(SectionHeading, { className: "text-sm font-bold uppercase tracking-wide text-text-muted", children: "Logo" }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsx("img", { src: branding.logoUrl || "/images/logo.svg", alt: "Current logo", width: "64", height: "64", className: "border border-border rounded-sm bg-surface-alt p-1" }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: assetBusy === "logo-upload", disabled: assetBusy !== null, onClick: () => logoFileRef.current?.click(), children: "Upload logo" }), _jsx("input", { ref: logoFileRef, type: "file", accept: RASTER_IMAGE_ACCEPT, "aria-label": "Upload logo", className: "hidden", onChange: handleLogoFileChange }), branding.logoUrl && (_jsx(Button, { type: "button", variant: "text", disabled: assetBusy !== null, loading: assetBusy === "logo-delete", onClick: () => void runAsset("logo-delete", async () => {
|
|
107
108
|
await deleteBrandingLogo();
|
|
108
109
|
return { ...branding, logoUrl: undefined };
|
|
109
|
-
}), children: "Remove logo" }))] })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Or use an external image URL" }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { "aria-label": "Logo URL", className: INPUT_CLASS, value: logoUrlInput, onChange: (e) => setLogoUrlInput(e.target.value) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: assetBusy !== null || logoUrlInput === (branding.logoUrl ?? ""), loading: assetBusy === "logo-url", onClick: () => void runAsset("logo-url", () => updateBranding({ logoUrl: logoUrlInput })), children: "Set" })] })] })] }), _jsxs("section", { className: "flex flex-col gap-3", children: [_jsx(
|
|
110
|
+
}), children: "Remove logo" }))] })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Or use an external image URL" }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { "aria-label": "Logo URL", className: INPUT_CLASS, value: logoUrlInput, onChange: (e) => setLogoUrlInput(e.target.value) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: assetBusy !== null || logoUrlInput === (branding.logoUrl ?? ""), loading: assetBusy === "logo-url", onClick: () => void runAsset("logo-url", () => updateBranding({ logoUrl: logoUrlInput })), children: "Set" })] })] })] }), _jsxs("section", { className: "flex flex-col gap-3", children: [_jsx(SectionHeading, { className: "text-sm font-bold uppercase tracking-wide text-text-muted", children: "Icon" }), _jsx("p", { className: "text-xs text-text-muted -mt-1", children: "A compact mark for navigation headers, independent of the full logo above. Falls back to the logo, then a default asset, when not set." }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsx("img", { src: branding.iconUrl || branding.logoUrl || "/images/logo.svg", alt: "Current icon", width: "64", height: "64", className: "border border-border rounded-sm bg-surface-alt p-1" }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: assetBusy === "icon-upload", disabled: assetBusy !== null, onClick: () => iconFileRef.current?.click(), children: "Upload icon" }), _jsx("input", { ref: iconFileRef, type: "file", accept: RASTER_IMAGE_ACCEPT, "aria-label": "Upload icon", className: "hidden", onChange: handleIconFileChange }), branding.iconUrl && (_jsx(Button, { type: "button", variant: "text", disabled: assetBusy !== null, loading: assetBusy === "icon-delete", onClick: () => void runAsset("icon-delete", async () => {
|
|
110
111
|
await deleteBrandingIcon();
|
|
111
112
|
return { ...branding, iconUrl: undefined };
|
|
112
|
-
}), children: "Remove icon" }))] })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Or use an external image URL" }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { "aria-label": "Icon URL", className: INPUT_CLASS, value: iconUrlInput, onChange: (e) => setIconUrlInput(e.target.value) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: assetBusy !== null || iconUrlInput === (branding.iconUrl ?? ""), loading: assetBusy === "icon-url", onClick: () => void runAsset("icon-url", () => updateBranding({ iconUrl: iconUrlInput })), children: "Set" })] })] })] }), _jsxs("section", { className: "flex flex-col gap-3", children: [_jsx(
|
|
113
|
+
}), children: "Remove icon" }))] })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Or use an external image URL" }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { "aria-label": "Icon URL", className: INPUT_CLASS, value: iconUrlInput, onChange: (e) => setIconUrlInput(e.target.value) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: assetBusy !== null || iconUrlInput === (branding.iconUrl ?? ""), loading: assetBusy === "icon-url", onClick: () => void runAsset("icon-url", () => updateBranding({ iconUrl: iconUrlInput })), children: "Set" })] })] })] }), _jsxs("section", { className: "flex flex-col gap-3", children: [_jsx(SectionHeading, { className: "text-sm font-bold uppercase tracking-wide text-text-muted", children: "Custom stylesheet" }), _jsx("div", { className: "flex items-center gap-3", children: _jsx("span", { className: "text-sm text-text-muted truncate", children: branding.stylesheetUrl || "None configured" }) }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: assetBusy === "stylesheet-upload", disabled: assetBusy !== null, onClick: () => stylesheetFileRef.current?.click(), children: "Upload CSS file" }), _jsx("input", { ref: stylesheetFileRef, type: "file", accept: "text/css,.css", "aria-label": "Upload stylesheet", className: "hidden", onChange: handleStylesheetFileChange }), branding.stylesheetUrl && (_jsx(Button, { type: "button", variant: "text", disabled: assetBusy !== null, loading: assetBusy === "stylesheet-delete", onClick: () => void runAsset("stylesheet-delete", async () => {
|
|
113
114
|
await deleteBrandingStylesheet();
|
|
114
115
|
return { ...branding, stylesheetUrl: undefined };
|
|
115
116
|
}), children: "Remove stylesheet" }))] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Or use an external stylesheet URL" }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { "aria-label": "Stylesheet URL", className: INPUT_CLASS, value: stylesheetUrlInput, onChange: (e) => setStylesheetUrlInput(e.target.value) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: assetBusy !== null || stylesheetUrlInput === (branding.stylesheetUrl ?? ""), loading: assetBusy === "stylesheet-url", onClick: () => void runAsset("stylesheet-url", () => updateBranding({ stylesheetUrl: stylesheetUrlInput })), children: "Set" })] })] })] }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-4 pt-2 border-t border-border", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Company name" }), _jsx("input", { "aria-label": "Company name", className: INPUT_CLASS, value: companyName, onChange: (e) => setCompanyName(e.target.value) })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Product title" }), _jsx("input", { "aria-label": "Product title", className: INPUT_CLASS, value: title, onChange: (e) => setTitle(e.target.value) }), _jsx("span", { className: "text-xs text-text-muted", children: "Shown as the browser tab title." })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Header HTML (optional)" }), _jsx("textarea", { "aria-label": "Header HTML", className: `${INPUT_CLASS} font-mono`, rows: 3, value: headerHtml, onChange: (e) => setHeaderHtml(e.target.value) })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Footer HTML (optional)" }), _jsx("textarea", { "aria-label": "Footer HTML", className: `${INPUT_CLASS} font-mono`, rows: 3, value: footerHtml, onChange: (e) => setFooterHtml(e.target.value) })] }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] })] })] }));
|
|
@@ -7,6 +7,9 @@ export interface EncryptionPolicyFormProps {
|
|
|
7
7
|
onChange: (policy: EncryptionPolicy) => void;
|
|
8
8
|
/** Told whether the form has edits that haven't been saved. */
|
|
9
9
|
onDirtyChange?: (dirty: boolean) => void;
|
|
10
|
+
/** Set when shown as one section of a page with its own heading (the setup wizard), so the title is a smaller,
|
|
11
|
+
* lower-level heading. */
|
|
12
|
+
embedded?: boolean;
|
|
10
13
|
}
|
|
11
14
|
/** The end-to-end encryption policy editor, shared by the Encryption Policy page and the setup wizard. */
|
|
12
|
-
export default function EncryptionPolicyForm({ policy, onChange, onDirtyChange }: EncryptionPolicyFormProps): React.JSX.Element;
|
|
15
|
+
export default function EncryptionPolicyForm({ policy, onChange, onDirtyChange, embedded }: EncryptionPolicyFormProps): React.JSX.Element;
|
|
@@ -28,7 +28,7 @@ export function isEncryptionEnabled(policy) {
|
|
|
28
28
|
return TIERS.some((tier) => policy[tier.key] !== "prohibited");
|
|
29
29
|
}
|
|
30
30
|
/** The end-to-end encryption policy editor, shared by the Encryption Policy page and the setup wizard. */
|
|
31
|
-
export default function EncryptionPolicyForm({ policy, onChange, onDirtyChange }) {
|
|
31
|
+
export default function EncryptionPolicyForm({ policy, onChange, onDirtyChange, embedded = false }) {
|
|
32
32
|
const [values, setValues] = useState(policy);
|
|
33
33
|
const [error, setError] = useState(null);
|
|
34
34
|
const [saving, setSaving] = useState(false);
|
|
@@ -55,7 +55,7 @@ export default function EncryptionPolicyForm({ policy, onChange, onDirtyChange }
|
|
|
55
55
|
setSaving(false);
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
-
return (_jsxs("div", { className: "max-w-xl", children: [_jsx("h2", { className: "text-lg font-bold uppercase tracking-wide mb-1", children: "End-to-end encryption" }), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "Encrypted messages can only be read by their senders and recipients - not by this server or its administrators. Choose how encryption applies to each kind of mail." }), 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-4", children: [TIERS.map((tier) => (_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: tier.label }), _jsx("select", { "aria-label": tier.label, className: SELECT_CLASS, value: values[tier.key], onChange: (e) => {
|
|
58
|
+
return (_jsxs("div", { className: "max-w-xl", children: [embedded ? (_jsx("h3", { className: "text-base font-bold uppercase tracking-wide mb-1", children: "End-to-end encryption" })) : (_jsx("h2", { className: "text-lg font-bold uppercase tracking-wide mb-1", children: "End-to-end encryption" })), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "Encrypted messages can only be read by their senders and recipients - not by this server or its administrators. Choose how encryption applies to each kind of mail." }), 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-4", children: [TIERS.map((tier) => (_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: tier.label }), _jsx("select", { "aria-label": tier.label, className: SELECT_CLASS, value: values[tier.key], onChange: (e) => {
|
|
59
59
|
setSaved(false);
|
|
60
60
|
setValues({ ...values, [tier.key]: e.target.value });
|
|
61
61
|
}, children: STATES.map((state) => (_jsx("option", { value: state.value, children: state.label }, state.value))) }), _jsx("span", { className: "text-xs text-text-muted", children: tier.help })] }, tier.key))), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] })] }));
|
|
@@ -5,6 +5,9 @@ export interface MailboxPolicyFormProps {
|
|
|
5
5
|
onChange: (policy: MailboxPolicy) => void;
|
|
6
6
|
/** Told whether the form has edits that haven't been saved. */
|
|
7
7
|
onDirtyChange?: (dirty: boolean) => void;
|
|
8
|
+
/** Set when shown as one section of a page with its own heading (the setup wizard), so the title is a smaller,
|
|
9
|
+
* lower-level heading. */
|
|
10
|
+
embedded?: boolean;
|
|
8
11
|
}
|
|
9
12
|
/** The mailbox defaults editor, shared by the Mailbox Policy page and the setup wizard. */
|
|
10
|
-
export default function MailboxPolicyForm({ policy, onChange, onDirtyChange }: MailboxPolicyFormProps): React.JSX.Element;
|
|
13
|
+
export default function MailboxPolicyForm({ policy, onChange, onDirtyChange, embedded }: MailboxPolicyFormProps): React.JSX.Element;
|
|
@@ -15,7 +15,7 @@ function toGb(bytes) {
|
|
|
15
15
|
return String(bytes / GB);
|
|
16
16
|
}
|
|
17
17
|
/** The mailbox defaults editor, shared by the Mailbox Policy page and the setup wizard. */
|
|
18
|
-
export default function MailboxPolicyForm({ policy, onChange, onDirtyChange }) {
|
|
18
|
+
export default function MailboxPolicyForm({ policy, onChange, onDirtyChange, embedded = false }) {
|
|
19
19
|
const [defaultQuotaGb, setDefaultQuotaGb] = useState(toGb(policy.defaultQuotaBytes));
|
|
20
20
|
const [autoProvisionEnabled, setAutoProvisionEnabled] = useState(policy.autoProvisionEnabled);
|
|
21
21
|
const [autoProvisionQuotaGb, setAutoProvisionQuotaGb] = useState(toGb(policy.autoProvisionQuotaBytes));
|
|
@@ -70,5 +70,5 @@ export default function MailboxPolicyForm({ policy, onChange, onDirtyChange }) {
|
|
|
70
70
|
setSaving(false);
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
-
return (_jsxs("div", { className: "max-w-xl", children: [_jsx("h2", { className: "text-lg font-bold uppercase tracking-wide mb-1", children: "Mailboxes" }), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "Defaults for new mailboxes on this server." }), 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-4", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Default quota (GB)" }), _jsx("input", { "aria-label": "Default quota (GB)", type: "number", min: 0, step: "any", className: INPUT_CLASS, value: defaultQuotaGb, onChange: (e) => setDefaultQuotaGb(e.target.value) }), _jsx("span", { className: "text-xs text-text-muted", children: "The storage a mailbox starts with when an administrator creates it." })] }), _jsxs("label", { className: "flex items-start gap-2 text-sm", children: [_jsx("input", { type: "checkbox", className: "mt-1", checked: autoProvisionEnabled, onChange: (e) => setAutoProvisionEnabled(e.target.checked) }), _jsxs("span", { children: [_jsx("span", { className: "font-semibold block", children: "Let people create their own mailbox" }), _jsx("span", { className: "text-xs text-text-muted", children: "Someone who signs in without a mailbox can create one, using their username on one of this server\u2019s verified domains." })] })] }), autoProvisionEnabled && (_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Quota for self-created mailboxes (GB)" }), _jsx("input", { "aria-label": "Quota for self-created mailboxes (GB)", type: "number", min: 0, step: "any", className: INPUT_CLASS, value: autoProvisionQuotaGb, onChange: (e) => setAutoProvisionQuotaGb(e.target.value) })] })), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] })] }));
|
|
73
|
+
return (_jsxs("div", { className: "max-w-xl", children: [embedded ? (_jsx("h3", { className: "text-base font-bold uppercase tracking-wide mb-1", children: "Mailboxes" })) : (_jsx("h2", { className: "text-lg font-bold uppercase tracking-wide mb-1", children: "Mailboxes" })), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "Defaults for new mailboxes on this server." }), 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-4", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Default quota (GB)" }), _jsx("input", { "aria-label": "Default quota (GB)", type: "number", min: 0, step: "any", className: INPUT_CLASS, value: defaultQuotaGb, onChange: (e) => setDefaultQuotaGb(e.target.value) }), _jsx("span", { className: "text-xs text-text-muted", children: "The storage a mailbox starts with when an administrator creates it." })] }), _jsxs("label", { className: "flex items-start gap-2 text-sm", children: [_jsx("input", { type: "checkbox", className: "mt-1", checked: autoProvisionEnabled, onChange: (e) => setAutoProvisionEnabled(e.target.checked) }), _jsxs("span", { children: [_jsx("span", { className: "font-semibold block", children: "Let people create their own mailbox" }), _jsx("span", { className: "text-xs text-text-muted", children: "Someone who signs in without a mailbox can create one, using their username on one of this server\u2019s verified domains." })] })] }), autoProvisionEnabled && (_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Quota for self-created mailboxes (GB)" }), _jsx("input", { "aria-label": "Quota for self-created mailboxes (GB)", type: "number", min: 0, step: "any", className: INPUT_CLASS, value: autoProvisionQuotaGb, onChange: (e) => setAutoProvisionQuotaGb(e.target.value) })] })), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] })] }));
|
|
74
74
|
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
export interface PluginsManagerProps {
|
|
3
|
+
/**
|
|
4
|
+
* Set when shown inside another page that already has its own heading and introduction (the setup wizard): the
|
|
5
|
+
* page heading and introduction are left out, and the section headings sit one level lower.
|
|
6
|
+
*/
|
|
7
|
+
embedded?: boolean;
|
|
8
|
+
}
|
|
2
9
|
/** Installed plugins with their rollout status, and every plugin action - shared by the Plugins page and the
|
|
3
10
|
* setup wizard. */
|
|
4
|
-
export default function PluginsManager(): React.JSX.Element;
|
|
11
|
+
export default function PluginsManager({ embedded }?: PluginsManagerProps): React.JSX.Element;
|
|
@@ -9,7 +9,7 @@ import { addPlugin, expectedPlanOf, getPluginStatus, getPluginUpdates, listPlugi
|
|
|
9
9
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
10
10
|
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
11
11
|
import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
|
|
12
|
-
const INPUT_CLASS = "w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
12
|
+
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";
|
|
13
13
|
/** How often status is refreshed while server copies are still applying a change. */
|
|
14
14
|
const PENDING_POLL_MS = 5000;
|
|
15
15
|
/** How long status keeps being polled after a saved change, since servers may not have noticed it yet. */
|
|
@@ -29,9 +29,21 @@ async function attempt(action, failure) {
|
|
|
29
29
|
return errorMessage(err, failure);
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
+
/** Classes for a plugin table's header and body cells. */
|
|
33
|
+
const TH_CLASS = "text-left text-xs uppercase tracking-wide text-text-muted py-2 px-2.5 border-b border-border";
|
|
34
|
+
const TD_CLASS = "py-3 px-2.5 border-b border-border align-top";
|
|
35
|
+
/**
|
|
36
|
+
* The installed plugins table stacks each row on narrow screens - name and description, then version and status side
|
|
37
|
+
* by side, then the actions - so the actions aren't scrolled off to the side. The explicit roles keep it a table for
|
|
38
|
+
* assistive technology while it isn't displayed as one.
|
|
39
|
+
*/
|
|
40
|
+
const INSTALLED_ROW_CLASS = "flex flex-wrap border-b border-border lg:table-row lg:border-b-0";
|
|
41
|
+
const INSTALLED_TD_CLASS = "block px-2.5 align-top lg:table-cell lg:pt-3 lg:pb-3 lg:border-b lg:border-border";
|
|
42
|
+
/** The small pill badge admin tables use for a state. */
|
|
43
|
+
const BADGE_CLASS = "inline-block text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill";
|
|
32
44
|
/** Installed plugins with their rollout status, and every plugin action - shared by the Plugins page and the
|
|
33
45
|
* setup wizard. */
|
|
34
|
-
export default function PluginsManager() {
|
|
46
|
+
export default function PluginsManager({ embedded = false } = {}) {
|
|
35
47
|
const [plugins, setPlugins] = useState([]);
|
|
36
48
|
const [status, setStatus] = useState(null);
|
|
37
49
|
/** Set when the last status refresh failed, so what's shown may be out of date. */
|
|
@@ -223,7 +235,9 @@ export default function PluginsManager() {
|
|
|
223
235
|
// Retried against the plugin as it's listed now (a newer version, or already enabled elsewhere, hides the retry).
|
|
224
236
|
const retryEnable = plugins.find((plugin) => plugin.uid === retryEnableUid && !plugin.enabled);
|
|
225
237
|
const displayNameOf = (name) => plugins.find((plugin) => plugin.name === name)?.manifest.displayName ?? name;
|
|
226
|
-
|
|
238
|
+
const SectionHeading = embedded ? "h3" : "h2";
|
|
239
|
+
const addButton = (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto shrink-0", onClick: () => setAdding(true), children: "Add by name" }));
|
|
240
|
+
return (_jsxs(_Fragment, { children: [embedded ? (_jsx("p", { className: "text-sm text-text-muted mb-5 max-w-3xl", children: "Changes are applied by restarting the servers one at a time, so mail keeps flowing. Plugins run with full access to the server, so only add ones you trust." })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex items-center justify-between gap-4 mb-2", children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide", children: "Plugins" }), addButton] }), _jsx("p", { className: "text-sm text-text-muted mb-6 max-w-3xl", children: "Plugins add protocols and features to every server. Changes are applied by restarting the servers one at a time, so mail keeps flowing. Plugins run with full access to the server, so only add ones you trust." })] })), error && (_jsxs(Alert, { children: [error, retryEnable && (_jsxs(_Fragment, { children: [" ", _jsx(Button, { type: "button", variant: "text", className: "!w-auto !p-0", onClick: () => void toggle(retryEnable), children: "Try again" })] }))] })), _jsx(RolloutBanner, { status: status }), statusStale && status && (_jsx("p", { className: "mb-4 text-xs text-text-muted", children: "Couldn't refresh server status. Showing the last status reported." })), listStale && (_jsxs("p", { className: "mb-4 text-xs text-text-muted", children: ["Couldn't re-read the plugin list, so it may be out of date.", " ", _jsx(Button, { type: "button", variant: "text", className: "!w-auto !p-0 !text-xs", onClick: () => void reload(), children: "Retry" })] })), _jsxs("div", { className: "flex items-center justify-between gap-4 mb-2", children: [_jsx(SectionHeading, { className: "text-sm font-bold uppercase tracking-wide", children: "Installed plugins" }), embedded && addButton] }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : plugins.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No plugins installed." })) : (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { role: "table", className: "w-full text-sm border-collapse block lg:table", children: [_jsx("thead", { role: "rowgroup", className: "hidden lg:table-header-group", children: _jsxs("tr", { role: "row", children: [_jsx("th", { role: "columnheader", className: TH_CLASS, children: "Plugin" }), _jsx("th", { role: "columnheader", className: TH_CLASS, children: "Version" }), _jsx("th", { role: "columnheader", className: TH_CLASS, children: "Status" }), _jsx("th", { role: "columnheader", className: TH_CLASS, children: _jsx("span", { className: "sr-only", children: "Actions" }) })] }) }), _jsx("tbody", { role: "rowgroup", className: "block lg:table-row-group", children: plugins.map((plugin) => {
|
|
227
241
|
const update = updates.get(plugin.uid);
|
|
228
242
|
const latest = update?.updateAvailable ? update.latestVersion : undefined;
|
|
229
243
|
const busy = busyUids.has(plugin.uid) || confirming?.uid === plugin.uid;
|
|
@@ -231,8 +245,8 @@ export default function PluginsManager() {
|
|
|
231
245
|
const requiredBy = plugins
|
|
232
246
|
.filter((other) => other.uid !== plugin.uid && other.manifest.requires?.[plugin.name] !== undefined)
|
|
233
247
|
.map((other) => other.manifest.displayName);
|
|
234
|
-
return (_jsxs("tr", { children: [_jsxs("td", {
|
|
235
|
-
}) })] }) })), _jsx(PluginBrowser, { plugins: plugins, onInstall: (result) => install(result.name, result.version, result.name),
|
|
248
|
+
return (_jsxs("tr", { role: "row", className: INSTALLED_ROW_CLASS, children: [_jsxs("td", { role: "cell", className: `${INSTALLED_TD_CLASS} basis-full pt-3 pb-2`, children: [_jsx("div", { className: "font-semibold", children: plugin.manifest.displayName }), _jsx("div", { className: "text-xs text-text-muted", children: plugin.name }), plugin.manifest.description && (_jsx("div", { className: "text-xs text-text-muted mt-1 max-w-md", children: plugin.manifest.description })), requires.length > 0 && _jsxs("div", { className: "text-xs text-text-muted mt-1", children: ["Requires: ", requires.join(", ")] }), requiredBy.length > 0 && (_jsxs("div", { className: "text-xs text-text-muted mt-1", children: ["Required by: ", requiredBy.join(", ")] }))] }), _jsxs("td", { role: "cell", className: `${INSTALLED_TD_CLASS} pt-1 pb-1 whitespace-nowrap`, children: [_jsx("div", { children: plugin.packageVersion }), latest && _jsxs("div", { className: "mt-1 text-xs font-semibold text-primary-dark", children: ["Update available: ", latest] })] }), _jsx("td", { role: "cell", className: `${INSTALLED_TD_CLASS} pt-1 pb-1`, children: _jsx(PluginStatusCell, { plugin: plugin, status: status }) }), _jsx("td", { role: "cell", className: `${INSTALLED_TD_CLASS} basis-full pt-2 pb-3`, children: _jsxs("div", { className: "flex flex-col items-start gap-1.5 lg:items-end lg:min-w-[11rem]", children: [_jsxs("div", { className: "flex flex-wrap gap-2 lg:justify-end", children: [latest && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", "aria-label": `Upgrade ${plugin.manifest.displayName} to ${latest}`, disabled: busy, onClick: () => void upgrade(plugin, latest), children: "Upgrade" })), (plugin.manifest.settings ?? []).length > 0 && (_jsx(Button, { variant: "secondary", type: "button", className: "!w-auto", onClick: () => setConfiguring(plugin), children: "Settings" })), _jsx(Button, { variant: plugin.enabled ? "secondary" : "primary", type: "button", className: "!w-auto min-w-[5.5rem]", "aria-label": `${plugin.enabled ? "Disable" : "Enable"} ${plugin.manifest.displayName}`, disabled: busy, onClick: () => void toggle(plugin), children: plugin.enabled ? "Disable" : "Enable" })] }), _jsxs("div", { className: "flex flex-wrap gap-x-4 lg:justify-end", children: [_jsx(Button, { variant: "text", type: "button", onClick: () => setUpgrading(plugin), children: "Change version" }), _jsx(Button, { variant: "text", type: "button", onClick: () => setRemoving(plugin), children: "Uninstall" })] })] }) })] }, plugin.uid));
|
|
249
|
+
}) })] }) })), _jsx(PluginBrowser, { headingLevel: SectionHeading, plugins: plugins, onInstall: (result) => install(result.name, result.version, result.name),
|
|
236
250
|
// The browser only offers an upgrade for a result it matched to one of these same `plugins`.
|
|
237
251
|
onUpgrade: (uid, packageVersion) => upgrade(plugins.find((p) => p.uid === uid), packageVersion) }), _jsx(AddPluginModal, { open: adding, onClose: () => setAdding(false), onAdd: async (name, packageVersion, displayName) => {
|
|
238
252
|
const problem = await install(name, packageVersion, displayName);
|
|
@@ -258,7 +272,7 @@ export default function PluginsManager() {
|
|
|
258
272
|
}
|
|
259
273
|
const ALL_NAMESPACES = "";
|
|
260
274
|
/** Searches the configured namespaces' registries for plugin packages, with install and upgrade actions. */
|
|
261
|
-
function PluginBrowser({ plugins, onInstall, onUpgrade, }) {
|
|
275
|
+
function PluginBrowser({ headingLevel: Heading, plugins, onInstall, onUpgrade, }) {
|
|
262
276
|
const [namespaces, setNamespaces] = useState([]);
|
|
263
277
|
const [namespace, setNamespace] = useState(ALL_NAMESPACES);
|
|
264
278
|
const [results, setResults] = useState(null);
|
|
@@ -310,14 +324,14 @@ function PluginBrowser({ plugins, onInstall, onUpgrade, }) {
|
|
|
310
324
|
setBusyName(null);
|
|
311
325
|
}
|
|
312
326
|
}
|
|
313
|
-
return (_jsxs("section", { "aria-labelledby": "plugin-browser-title", className: "mt-8", children: [_jsx(
|
|
314
|
-
(rows.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No plugins found." })) : (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm border-collapse", children: [_jsx("thead", { children:
|
|
327
|
+
return (_jsxs("section", { "aria-labelledby": "plugin-browser-title", className: "mt-8", children: [_jsx(Heading, { id: "plugin-browser-title", className: "text-sm font-bold uppercase tracking-wide mb-2", children: "Find plugins" }), _jsxs("form", { onSubmit: (e) => void search(e), className: "flex flex-wrap gap-2 items-end mb-3", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Namespace" }), _jsxs("select", { "aria-label": "Namespace", className: INPUT_CLASS, value: namespace, onChange: (e) => setNamespace(e.target.value), children: [_jsx("option", { value: ALL_NAMESPACES, children: "All namespaces" }), namespaces.map((ns) => (_jsx("option", { value: ns.name, children: ns.name }, ns.name)))] })] }), _jsx(Button, { type: "submit", className: "!w-auto", loading: searching, disabled: searching, children: "Search" })] }), error && _jsx(Alert, { children: error }), results !== null &&
|
|
328
|
+
(rows.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No plugins found." })) : (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm border-collapse", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { className: TH_CLASS, children: "Package" }), _jsx("th", { className: TH_CLASS, children: "Latest version" }), _jsx("th", { className: TH_CLASS, children: "Status" }), _jsx("th", { className: TH_CLASS, children: _jsx("span", { className: "sr-only", children: "Actions" }) })] }) }), _jsx("tbody", { children: rows.map((result) => (_jsxs("tr", { children: [_jsxs("td", { className: TD_CLASS, children: [_jsx("div", { className: "font-semibold", children: result.name }), result.description && _jsx("div", { className: "text-xs text-text-muted max-w-md", children: result.description })] }), _jsx("td", { className: TD_CLASS, children: result.version }), _jsx("td", { className: `${TD_CLASS} text-text-muted`, children: result.updateAvailable
|
|
315
329
|
? `Installed ${result.installedVersion} - update available`
|
|
316
330
|
: result.installedUid
|
|
317
331
|
? `Installed ${result.installedVersion}`
|
|
318
332
|
: result.allowed
|
|
319
333
|
? "Not installed"
|
|
320
|
-
: "Not allowed on this server" }), _jsx("td", { className:
|
|
334
|
+
: "Not allowed on this server" }), _jsx("td", { className: `${TD_CLASS} text-right`, children: result.updateAvailable ? (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", "aria-label": `Upgrade ${result.name} to ${result.version}`, disabled: busyName === result.name, onClick: () => void upgrade(result), children: "Upgrade" })) : (!result.installedUid &&
|
|
321
335
|
result.allowed && (_jsx(Button, { type: "button", className: "!w-auto", "aria-label": `Install ${result.name}`, loading: busyName === result.name, disabled: busyName === result.name, onClick: () => void install(result), children: "Install" }))) })] }, result.name))) })] }) })))] }));
|
|
322
336
|
}
|
|
323
337
|
/** Name a server reports an error under when it isn't about any single plugin (e.g. the plugin set as a whole failed). */
|
|
@@ -337,17 +351,21 @@ function RolloutBanner({ status }) {
|
|
|
337
351
|
.map((entry) => `${instance.instance}: ${entry.message}`));
|
|
338
352
|
return (_jsxs(_Fragment, { children: [serverWideErrors.length > 0 && (_jsxs(Alert, { children: ["Plugins couldn\u2019t be loaded:", _jsx("ul", { className: "list-disc pl-5 mt-1", children: serverWideErrors.map((message) => (_jsx("li", { className: "break-words", children: message }, message))) })] })), behind > 0 && (_jsxs("p", { role: "status", className: "mb-4 text-sm py-2 px-3 rounded-sm bg-surface-alt text-text", children: ["Applying changes: ", status.instances.length - behind, " of ", status.instances.length, " ", status.instances.length === 1 ? "server" : "servers", " updated. Servers restart one at a time."] })), safeMode.length > 0 && (_jsxs(Alert, { children: [safeMode.map((instance) => instance.instance).join(", "), " started without any plugins because recent starts failed. Fix or disable the failing plugin, and the server will pick up the change."] }))] }));
|
|
339
353
|
}
|
|
354
|
+
/** A plugin's enabled state and, for an enabled one, how many servers have loaded it and what went wrong. */
|
|
340
355
|
function PluginStatusCell({ plugin, status }) {
|
|
341
356
|
if (!plugin.enabled) {
|
|
342
|
-
return _jsx("span", { className:
|
|
357
|
+
return _jsx("span", { className: `${BADGE_CLASS} bg-surface-alt text-text-muted`, children: "Disabled" });
|
|
343
358
|
}
|
|
359
|
+
return (_jsxs("div", { className: "flex flex-col items-start gap-1", children: [_jsx("span", { className: `${BADGE_CLASS} bg-success text-white`, children: "Enabled" }), _jsx(PluginServers, { plugin: plugin, status: status })] }));
|
|
360
|
+
}
|
|
361
|
+
function PluginServers({ plugin, status }) {
|
|
344
362
|
if (!status || status.instances.length === 0) {
|
|
345
|
-
return _jsx("span", { className: "text-text-muted", children: "
|
|
363
|
+
return _jsx("span", { className: "text-xs text-text-muted", children: "Server status unknown" });
|
|
346
364
|
}
|
|
347
365
|
const current = status.instances.filter((instance) => instance.hash === status.hash);
|
|
348
366
|
const loaded = current.filter((instance) => instance.loaded.some((entry) => entry.name === plugin.name && entry.version === plugin.packageVersion)).length;
|
|
349
367
|
const errors = current.flatMap((instance) => instance.errors.filter((entry) => entry.name === plugin.name).map((entry) => `${instance.instance}: ${entry.message}`));
|
|
350
|
-
return (_jsxs("div", { children: [_jsxs("span", { className: loaded === status.instances.length ? "text-success font-medium" : "text-text-muted", children: ["Loaded on ", loaded, " of ", status.instances.length, " ", status.instances.length === 1 ? "server" : "servers"] }), errors.map((message) => (_jsx("div", { className: "text-
|
|
368
|
+
return (_jsxs("div", { className: "text-xs", children: [_jsxs("span", { className: loaded === status.instances.length ? "text-success font-medium" : "text-text-muted", children: ["Loaded on ", loaded, " of ", status.instances.length, " ", status.instances.length === 1 ? "server" : "servers"] }), errors.map((message) => (_jsx("div", { className: "text-danger mt-1 break-words", children: message }, message)))] }));
|
|
351
369
|
}
|
|
352
370
|
function AddPluginModal({ open, onClose, onAdd, }) {
|
|
353
371
|
const [name, setName] = useState("");
|
|
@@ -544,12 +562,13 @@ function SettingsModal({ plugin, onClose, onSaved }) {
|
|
|
544
562
|
setBusy(false);
|
|
545
563
|
}
|
|
546
564
|
}
|
|
547
|
-
return (_jsxs(Modal, { open: true, onClose: onClose, title: `${plugin.manifest.displayName} settings`, children: [error && _jsx(Alert, { children: error }), _jsxs("form", { onSubmit: save, className: "flex flex-col gap-
|
|
565
|
+
return (_jsxs(Modal, { open: true, onClose: onClose, title: `${plugin.manifest.displayName} settings`, children: [error && _jsx(Alert, { children: error }), _jsxs("form", { onSubmit: save, className: "flex flex-col gap-5", children: [definitions.map((definition) => (_jsx(SettingField, { definition: definition, value: values[definition.key], onChange: (value) => setValues((prev) => ({ ...prev, [definition.key]: value })) }, definition.key))), _jsxs("div", { className: "sticky -bottom-7 -mx-7 -mb-7 px-7 py-4 bg-surface border-t border-border flex flex-col gap-3", children: [_jsx("p", { className: "text-xs text-text-muted", children: "Saving restarts the servers one at a time to apply the new settings." }), _jsxs("div", { className: "flex gap-2 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "submit", className: "!w-auto", loading: busy, disabled: busy, children: "Save" })] })] })] })] }));
|
|
548
566
|
}
|
|
549
567
|
function SettingField({ definition, value, onChange, }) {
|
|
550
568
|
const help = definition.help && _jsx("span", { className: "text-xs text-text-muted", children: definition.help });
|
|
551
569
|
if (definition.type === "boolean") {
|
|
552
|
-
|
|
570
|
+
// Laid out like the admin policy forms' checkboxes: the help sits under the label, beside the box.
|
|
571
|
+
return (_jsxs("label", { className: "flex items-start gap-2 text-sm", children: [_jsx("input", { type: "checkbox", className: "mt-1", checked: value === true, onChange: (e) => onChange(e.target.checked) }), _jsxs("span", { className: "flex flex-col gap-0.5", children: [_jsx("span", { className: "font-semibold", children: definition.label }), help] })] }));
|
|
553
572
|
}
|
|
554
573
|
return (_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: definition.label }), definition.type === "select" ? (_jsxs("select", { "aria-label": definition.label, className: INPUT_CLASS, value: String(value), onChange: (e) => onChange(e.target.value), children: [!definition.required && _jsx("option", { value: "", children: "Default" }), (definition.options ?? []).map((option) => (_jsx("option", { value: option.value, children: option.label }, option.value)))] })) : (_jsx("input", { "aria-label": definition.label, className: INPUT_CLASS, type: definition.type === "number" ? "number" : "text", min: definition.min, max: definition.max, required: definition.required, value: String(value), onChange: (e) => onChange(e.target.value) })), help] }));
|
|
555
574
|
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { RetentionPolicy } from "@rapidmx/react-shared/admin/retentionPolicyApi.js";
|
|
3
3
|
/** The retention policy editor, shared by the Retention Policy page and the setup wizard. */
|
|
4
|
-
export default function RetentionPolicyForm({ policy, onChange, onDirtyChange, }: {
|
|
4
|
+
export default function RetentionPolicyForm({ policy, onChange, onDirtyChange, embedded, }: {
|
|
5
5
|
policy: RetentionPolicy;
|
|
6
6
|
onChange: (p: RetentionPolicy) => void;
|
|
7
7
|
/** Told whether the form has edits that haven't been saved. */
|
|
8
8
|
onDirtyChange?: (dirty: boolean) => void;
|
|
9
|
+
/** Set when shown as one section of a page with its own heading (the setup wizard), so the title is a smaller,
|
|
10
|
+
* lower-level heading. */
|
|
11
|
+
embedded?: boolean;
|
|
9
12
|
}): React.JSX.Element;
|
|
@@ -15,7 +15,7 @@ function shortensRetention(next, saved) {
|
|
|
15
15
|
return next != null && (saved == null || next < saved);
|
|
16
16
|
}
|
|
17
17
|
/** The retention policy editor, shared by the Retention Policy page and the setup wizard. */
|
|
18
|
-
export default function RetentionPolicyForm({ policy, onChange, onDirtyChange, }) {
|
|
18
|
+
export default function RetentionPolicyForm({ policy, onChange, onDirtyChange, embedded = false, }) {
|
|
19
19
|
const [messageRetentionDays, setMessageRetentionDays] = useState(policy.messageRetentionDays?.toString() ?? "");
|
|
20
20
|
const [auditLogRetentionDays, setAuditLogRetentionDays] = useState(policy.auditLogRetentionDays?.toString() ?? "");
|
|
21
21
|
const [error, setError] = useState(null);
|
|
@@ -65,7 +65,7 @@ export default function RetentionPolicyForm({ policy, onChange, onDirtyChange, }
|
|
|
65
65
|
setSaving(false);
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
-
return (_jsxs("div", { className: "max-w-xl", children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide mb-1", children: "Retention Policy" }), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "Automatically and permanently deletes mail/audit-log entries older than the configured age. Leave a field blank for no automatic purge. This does not apply to a mailbox that is a custodian on an active legal hold \u2014 a held message is skipped and retried on a later run once the hold is lifted." }), 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-4", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Message retention (days)" }), _jsx("input", { "aria-label": "Message retention (days)", type: "number", min: 1, className: INPUT_CLASS, value: messageRetentionDays, onChange: (e) => setMessageRetentionDays(e.target.value), placeholder: "No automatic purge" }), _jsx("span", { className: "text-xs text-text-muted", children: "Applies to any message, in any folder, regardless of age." })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Audit log retention (days)" }), _jsx("input", { "aria-label": "Audit log retention (days)", type: "number", min: MIN_AUDIT_LOG_RETENTION_DAYS, className: INPUT_CLASS, value: auditLogRetentionDays, onChange: (e) => setAuditLogRetentionDays(e.target.value), placeholder: "Keep forever" }), _jsxs("span", { className: "text-xs text-text-muted", children: ["Cannot be set below ", MIN_AUDIT_LOG_RETENTION_DAYS, " days. Never applies to the tamper-evident escrow audit chain."] })] }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] }), _jsxs(Modal, { open: pendingPatch !== null, onClose: () => setPendingPatch(null), title: "Delete older data?", children: [shortensRetention(pendingPatch?.messageRetentionDays, savedMessageDays) && (_jsxs("p", { className: "text-sm mb-3", children: [savedMessageDays == null
|
|
68
|
+
return (_jsxs("div", { className: "max-w-xl", children: [embedded ? (_jsx("h3", { className: "text-base font-bold uppercase tracking-wide mb-1", children: "Retention policy" })) : (_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide mb-1", children: "Retention Policy" })), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "Automatically and permanently deletes mail/audit-log entries older than the configured age. Leave a field blank for no automatic purge. This does not apply to a mailbox that is a custodian on an active legal hold \u2014 a held message is skipped and retried on a later run once the hold is lifted." }), 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-4", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Message retention (days)" }), _jsx("input", { "aria-label": "Message retention (days)", type: "number", min: 1, className: INPUT_CLASS, value: messageRetentionDays, onChange: (e) => setMessageRetentionDays(e.target.value), placeholder: "No automatic purge" }), _jsx("span", { className: "text-xs text-text-muted", children: "Applies to any message, in any folder, regardless of age." })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Audit log retention (days)" }), _jsx("input", { "aria-label": "Audit log retention (days)", type: "number", min: MIN_AUDIT_LOG_RETENTION_DAYS, className: INPUT_CLASS, value: auditLogRetentionDays, onChange: (e) => setAuditLogRetentionDays(e.target.value), placeholder: "Keep forever" }), _jsxs("span", { className: "text-xs text-text-muted", children: ["Cannot be set below ", MIN_AUDIT_LOG_RETENTION_DAYS, " days. Never applies to the tamper-evident escrow audit chain."] })] }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] }), _jsxs(Modal, { open: pendingPatch !== null, onClose: () => setPendingPatch(null), title: "Delete older data?", children: [shortensRetention(pendingPatch?.messageRetentionDays, savedMessageDays) && (_jsxs("p", { className: "text-sm mb-3", children: [savedMessageDays == null
|
|
69
69
|
? "Mail isn't deleted automatically today."
|
|
70
70
|
: `Mail is currently kept for ${savedMessageDays} days.`, " ", "With this change, every message older than ", _jsxs("strong", { children: [pendingPatch?.messageRetentionDays, " days"] }), ", in every mailbox and folder, is permanently deleted on the next purge run - and so on from then on. Mailboxes on an active legal hold are skipped."] })), shortensRetention(pendingPatch?.auditLogRetentionDays, savedAuditDays) && (_jsxs("p", { className: "text-sm mb-3", children: [savedAuditDays == null
|
|
71
71
|
? "Audit-log entries are kept forever today."
|
|
@@ -140,7 +140,7 @@ export default function EscrowSetupStep({ adminUid }) {
|
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
const scopeForm = (_jsxs("form", { onSubmit: create, className: "flex flex-col gap-5", children: [_jsx(EscrowScopeKeyAndHoldersFields, { value: keyAndHolders, onChange: setKeyAndHolders, disabled: mode === "generate" && busy, keyReadOnly: mode === "generate" }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: busy, disabled: busy, className: "!w-auto", children: "Create escrow scope" }) })] }));
|
|
143
|
-
return (_jsxs("div", { className: "flex flex-col gap-5 max-w-3xl", children: [
|
|
143
|
+
return (_jsxs("div", { className: "flex flex-col gap-5 max-w-3xl", children: [_jsx("p", { className: "text-sm text-text-muted", children: "Escrow lets a group of trusted holders - for example your legal or compliance team - recover a mailbox\u2019s encrypted mail when they all agree to. It\u2019s optional. Without it, mail that someone encrypts can\u2019t be recovered if they lose their keys, by anyone." }), scopes && scopes.length > 0 && (_jsxs("div", { role: "status", className: "text-sm py-2 px-3 rounded-sm bg-surface-alt text-text", children: ["Escrow scopes set up: ", scopes.map((scope) => scope.name).join(", "), "."] })), error && _jsx(Alert, { children: error }), _jsxs("fieldset", { className: "flex flex-col gap-2 text-sm", children: [_jsx("legend", { className: "font-semibold mb-1", children: "How do you want to set up escrow?" }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "radio", name: "escrow-mode", checked: mode === "generate", onChange: () => chooseMode("generate") }), "Generate new escrow keys in this browser"] }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "radio", name: "escrow-mode", checked: mode === "existing", onChange: () => chooseMode("existing") }), "Use a certificate I already have"] }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "radio", name: "escrow-mode", checked: mode === "none", onChange: () => chooseMode("none") }), scopes && scopes.length > 0 ? "Don't add another escrow scope" : "Don't use escrow"] })] }), mode !== "none" && (_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx(FormField, { label: "Escrow scope name", htmlFor: "escrowName", children: _jsx("input", { id: "escrowName", type: "text", className: INPUT_CLASS, value: name, disabled: !!generated, onChange: (e) => setName(e.target.value) }) }), mode === "generate" && !generated && (_jsxs(_Fragment, { children: [_jsx(FormField, { label: "Keys valid for", htmlFor: "escrowValidYears", children: _jsx("select", { id: "escrowValidYears", className: INPUT_CLASS, value: validYears, onChange: (e) => setValidYears(Number(e.target.value)), children: [1, 2, 5, 10].map((years) => (_jsxs("option", { value: years, children: [years, " ", years === 1 ? "year" : "years"] }, years))) }) }), _jsx(Button, { type: "button", className: "!w-auto", loading: busy, disabled: busy, onClick: () => void generate(), children: "Generate keys" })] })), mode === "generate" && generated && (_jsxs("div", { className: "flex flex-col gap-3 text-sm", children: [_jsx(Alert, { children: "Download the private key now and give it to your escrow holders. It isn\u2019t stored anywhere else - if it\u2019s lost, no escrowed mail can ever be recovered." }), _jsx(Alert, { children: "You\u2019re downloading this private key, so you must not also be one of its holders: pick holders other than yourself, hand the key over, and delete your copy once they have it. Otherwise one person could both configure escrow and recover mail with it." }), _jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsx(Button, { type: "button", className: "!w-auto", onClick: () => {
|
|
144
144
|
downloadTextFile(`${fileSafe(name)}-private-key.pem`, generated.privateKeyPem);
|
|
145
145
|
setDownloadedPrivateKey(true);
|
|
146
146
|
}, children: "Download private key" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => downloadTextFile(`${fileSafe(name)}-certificate.pem`, generated.certificatePem), children: "Download certificate" })] }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "checkbox", checked: savedPrivateKey, disabled: !downloadedPrivateKey, onChange: (e) => setSavedPrivateKey(e.target.checked) }), "I\u2019ve saved the private key somewhere safe"] }), !downloadedPrivateKey && (_jsx("p", { className: "text-xs text-text-muted", children: "Download the private key before continuing." }))] }))] })), mode === "existing" && scopeForm, mode === "generate" && generated && savedPrivateKey && scopeForm] }));
|
|
@@ -4,6 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
6
|
import { useEffect, useRef, useState } from "react";
|
|
7
|
+
import { HiCheck } from "react-icons/hi2";
|
|
7
8
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
9
|
import { getRetentionPolicy } from "@rapidmx/react-shared/admin/retentionPolicyApi.js";
|
|
9
10
|
import { getMailboxPolicy } from "@rapidmx/react-shared/admin/mailboxPolicyApi.js";
|
|
@@ -26,6 +27,8 @@ import PluginsManager from "../settings/PluginsManager.js";
|
|
|
26
27
|
import RetentionPolicyForm from "../settings/RetentionPolicyForm.js";
|
|
27
28
|
import EscrowSetupStep from "./EscrowSetupStep.js";
|
|
28
29
|
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";
|
|
30
|
+
/** The bordered panel the wizard's forms sit in. */
|
|
31
|
+
const CARD_CLASS = "bg-surface border border-border rounded-md p-6";
|
|
29
32
|
export const SETUP_STEPS = [
|
|
30
33
|
{ id: "plugins", title: "Plugins", intro: "Choose which protocols and features this server runs. The recommended plugins are already installed." },
|
|
31
34
|
{ id: "domain", title: "Domain", intro: "Add the domain this server receives mail for, then publish the DNS records below." },
|
|
@@ -147,20 +150,21 @@ export default function SetupWizard({ userUid }) {
|
|
|
147
150
|
}
|
|
148
151
|
const noDomain = domains.length === 0;
|
|
149
152
|
const blocked = (step === "domain" || last) && noDomain;
|
|
150
|
-
return (_jsxs("div", { className: "flex flex-col gap-6", children: [_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide mb-1", children: "Set up your server" }), _jsx("p", { className: "text-sm text-text-muted", children: "You can change any of these settings later from the admin console." })] }), status && !status.required && (_jsx("a", { href: "/admin", className: "text-sm text-primary-dark underline whitespace-nowrap", children: "Exit setup" }))] }), domainsError && (_jsx(Alert, { children: _jsxs("span", { className: "flex flex-wrap items-center gap-2", children: [domainsError, _jsx(Button, { type: "button", variant: "
|
|
153
|
+
return (_jsxs("div", { className: "flex flex-col gap-6", children: [_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide mb-1", children: "Set up your server" }), _jsx("p", { className: "text-sm text-text-muted", children: "You can change any of these settings later from the admin console." })] }), status && !status.required && (_jsx("a", { href: "/admin", className: "text-sm text-primary-dark font-medium hover:underline whitespace-nowrap", children: "Exit setup" }))] }), domainsError && (_jsx(Alert, { children: _jsxs("span", { className: "flex flex-wrap items-center gap-2", children: [domainsError, _jsx(Button, { type: "button", variant: "text", onClick: () => void loadDomains(), children: "Retry" })] }) })), _jsx("ol", { "aria-label": "Setup steps", className: "flex flex-wrap gap-2", children: SETUP_STEPS.map((s, i) => (_jsx("li", { children: _jsxs("button", { type: "button", "aria-current": s.id === step ? "step" : undefined,
|
|
151
154
|
// Steps after the domain need somewhere to put mail first.
|
|
152
155
|
disabled: i > 1 && noDomain, onClick: () => requestGoTo(s.id), className: [
|
|
153
|
-
"text-sm py-1.5 px-3 rounded-pill border
|
|
156
|
+
"inline-flex items-center gap-1.5 text-sm py-1.5 px-3 rounded-pill border transition-colors",
|
|
157
|
+
"disabled:opacity-50 disabled:cursor-not-allowed",
|
|
154
158
|
s.id === step
|
|
155
159
|
? "bg-primary/10 border-primary text-primary-dark font-semibold"
|
|
156
160
|
: i < index
|
|
157
|
-
? "border-border text-text"
|
|
158
|
-
: "border-border text-text-muted",
|
|
159
|
-
].join(" "), children: [i + 1, ". ", s.title, i < index && _jsx("span", { className: "sr-only", children: " (done)" })] }) }, s.id))) }), _jsxs("section", { "aria-labelledby": "setup-step-title", className: "flex flex-col gap-5", children: [_jsxs("div", { children: [_jsxs("h2", { id: "setup-step-title", className: "text-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
(encryption && !isEncryptionEnabled(encryption) ? (_jsx("p", { role: "status", className: "text-sm py-2 px-3 rounded-sm bg-surface-alt text-text", children: "End-to-end encryption is turned off, so escrow isn\u2019t needed. You can set it up later if you turn encryption on." })) : (_jsx(EscrowSetupStep, { adminUid: userUid }))), step === "branding" && (_jsx(LoadedSettingsForm, { load: getBranding, loadErrorMessage: "Could not load branding.", children: (branding, onChange) => _jsx(BrandingForm, { branding: branding, onChange: onChange }) })), step === "mailboxes" && _jsx(MailboxesStep, { userUid: userUid, domain: domains.find((d) => d.verified)?.name ?? domains[0]?.name })] }), error && _jsx(Alert, { children: error }), _jsxs("div", { className: "flex items-center gap-3 border-t border-border pt-5", children: [index > 0 && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => requestGoTo(SETUP_STEPS[index - 1].id), children: "Back" })), _jsx("div", { className: "flex-1" }), progressFailed && _jsx("span", { className: "text-xs text-text-muted", children: "Your place in setup couldn\u2019t be saved." }), blocked && _jsx("span", { className: "text-sm text-text-muted", children: last ? "Add a domain before finishing setup." : "Add a domain to continue." }), last ? (_jsx(Button, { type: "button", className: "!w-auto", loading: finishing, disabled: finishing || blocked, onClick: () => void finish(), children: "Finish setup" })) : (_jsx(Button, { type: "button", className: "!w-auto", disabled: blocked, onClick: () => requestGoTo(SETUP_STEPS[index + 1].id), children: "Continue" }))] }), leavingTo && (_jsxs(Modal, { open: true, onClose: () => setLeavingTo(null), title: "Discard unsaved changes?", children: [_jsx("p", { className: "text-sm mb-4", children: "Some server settings on this step have changes that haven\u2019t been saved." }), _jsxs("div", { className: "flex gap-2 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setLeavingTo(null), children: "Keep editing" }), _jsx(Button, { type: "button", className: "!w-auto", onClick: () => goTo(leavingTo), children: "Discard changes" })] })] }))] }));
|
|
161
|
+
? "bg-surface border-border text-text hover:not-disabled:border-primary"
|
|
162
|
+
: "bg-surface border-border text-text-muted hover:not-disabled:border-primary hover:not-disabled:text-text",
|
|
163
|
+
].join(" "), children: [i < index && _jsx(HiCheck, { "aria-hidden": "true", className: "w-4 h-4 text-success" }), i + 1, ". ", s.title, i < index && _jsx("span", { className: "sr-only", children: " (done)" })] }) }, s.id))) }), _jsxs("section", { "aria-labelledby": "setup-step-title", className: "flex flex-col gap-5", children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsxs("h2", { id: "setup-step-title", className: "flex flex-col gap-0.5", children: [_jsxs("span", { className: "text-xs font-bold uppercase tracking-wide text-text-muted", children: ["Step ", index + 1, " of ", SETUP_STEPS.length, _jsx("span", { className: "sr-only", children: ":" })] }), " ", _jsx("span", { className: "text-lg font-bold uppercase tracking-wide", children: current.title })] }), _jsx("p", { className: "text-sm text-text-muted max-w-3xl", children: current.intro })] }), step === "plugins" && (_jsx("div", { children: _jsx(PluginsManager, { embedded: true }) })), step === "domain" && _jsx(DomainStep, { domains: domains, onCreated: (domain) => setDomains((prev) => [...prev, domain]) }), step === "settings" && (_jsxs("div", { className: "flex flex-col gap-6 max-w-3xl", children: [_jsx("div", { className: CARD_CLASS, children: _jsx(LoadedSettingsForm, { load: getEncryptionPolicy, loadErrorMessage: "Could not load the encryption policy.", children: (policy, onChange) => (_jsx(EncryptionPolicyForm, { embedded: true, policy: policy, onDirtyChange: trackUnsaved("encryption"), onChange: (updated) => {
|
|
164
|
+
onChange(updated);
|
|
165
|
+
setEncryption(updated);
|
|
166
|
+
} })) }) }), _jsx("div", { className: CARD_CLASS, children: _jsx(LoadedSettingsForm, { load: getRetentionPolicy, loadErrorMessage: "Could not load the retention policy.", children: (policy, onChange) => (_jsx(RetentionPolicyForm, { embedded: true, policy: policy, onChange: onChange, onDirtyChange: trackUnsaved("retention") })) }) }), _jsx("div", { className: CARD_CLASS, children: _jsx(LoadedSettingsForm, { load: getMailboxPolicy, loadErrorMessage: "Could not load the mailbox policy.", children: (policy, onChange) => (_jsx(MailboxPolicyForm, { embedded: true, policy: policy, onChange: onChange, onDirtyChange: trackUnsaved("mailbox") })) }) })] })), step === "escrow" &&
|
|
167
|
+
(encryption && !isEncryptionEnabled(encryption) ? (_jsx("p", { role: "status", className: "text-sm py-2 px-3 rounded-sm bg-surface-alt text-text max-w-3xl", children: "End-to-end encryption is turned off, so escrow isn\u2019t needed. You can set it up later if you turn encryption on." })) : (_jsx(EscrowSetupStep, { adminUid: userUid }))), step === "branding" && (_jsx("div", { className: `${CARD_CLASS} max-w-3xl`, children: _jsx(LoadedSettingsForm, { load: getBranding, loadErrorMessage: "Could not load branding.", children: (branding, onChange) => _jsx(BrandingForm, { embedded: true, branding: branding, onChange: onChange }) }) })), step === "mailboxes" && _jsx(MailboxesStep, { userUid: userUid, domain: domains.find((d) => d.verified)?.name ?? domains[0]?.name })] }), error && _jsx(Alert, { children: error }), _jsxs("div", { className: "flex items-center gap-3 border-t border-border pt-5", children: [index > 0 && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => requestGoTo(SETUP_STEPS[index - 1].id), children: "Back" })), _jsx("div", { className: "flex-1" }), progressFailed && _jsx("span", { className: "text-xs text-text-muted", children: "Your place in setup couldn\u2019t be saved." }), blocked && _jsx("span", { className: "text-sm text-text-muted", children: last ? "Add a domain before finishing setup." : "Add a domain to continue." }), last ? (_jsx(Button, { type: "button", className: "!w-auto", loading: finishing, disabled: finishing || blocked, onClick: () => void finish(), children: "Finish setup" })) : (_jsx(Button, { type: "button", className: "!w-auto", disabled: blocked, onClick: () => requestGoTo(SETUP_STEPS[index + 1].id), children: "Continue" }))] }), leavingTo && (_jsxs(Modal, { open: true, onClose: () => setLeavingTo(null), title: "Discard unsaved changes?", children: [_jsx("p", { className: "text-sm mb-4", children: "Some server settings on this step have changes that haven\u2019t been saved." }), _jsxs("div", { className: "flex gap-2 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setLeavingTo(null), children: "Keep editing" }), _jsx(Button, { type: "button", className: "!w-auto", onClick: () => goTo(leavingTo), children: "Discard changes" })] })] }))] }));
|
|
164
168
|
}
|
|
165
169
|
function DomainStep({ domains, onCreated }) {
|
|
166
170
|
const [name, setName] = useState("");
|
|
@@ -185,7 +189,7 @@ function DomainStep({ domains, onCreated }) {
|
|
|
185
189
|
setSaving(false);
|
|
186
190
|
}
|
|
187
191
|
}
|
|
188
|
-
return (_jsxs("div", { className: "flex flex-col gap-5 max-w-3xl", children: [domains.map((domain) => (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("h3", { className: "text-base font-bold", children: domain.name }), _jsx(DomainDnsSetup, { uid: domain.uid })] }, domain.uid))), error && _jsx(Alert, { children: error }), _jsxs("form", { onSubmit: handleSubmit, className:
|
|
192
|
+
return (_jsxs("div", { className: "flex flex-col gap-5 max-w-3xl", children: [domains.map((domain) => (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("h3", { className: "text-base font-bold", children: domain.name }), _jsx(DomainDnsSetup, { uid: domain.uid })] }, domain.uid))), error && _jsx(Alert, { children: error }), _jsxs("form", { onSubmit: handleSubmit, className: CARD_CLASS, children: [_jsx(FormField, { label: domains.length === 0 ? "Domain name" : "Add another domain", htmlFor: "setupDomainName", children: _jsx("input", { id: "setupDomainName", type: "text", className: INPUT_CLASS, value: name, onChange: (e) => setName(e.target.value), placeholder: "example.com" }) }), _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Add domain" })] })] }));
|
|
189
193
|
}
|
|
190
194
|
function MailboxesStep({ userUid, domain }) {
|
|
191
195
|
const [mailboxes, setMailboxes] = useState([]);
|