Migrate Mail surfaces to interface patterns

This commit is contained in:
2026-08-03 10:06:13 +02:00
parent 5b5077cde7
commit 7844d9cab4
12 changed files with 460 additions and 44 deletions
+58 -15
View File
@@ -1,11 +1,14 @@
import { useEffect, useMemo, useState } from "react";
import { ArrowLeft, Plus, RefreshCw, RotateCw, Trash2 } from "lucide-react";
import {
ActionBlockerHint,
Button,
Card,
ConfirmDialog,
DataGrid,
Dialog,
DismissibleAlert,
DocumentationHelpLink,
FormField,
LoadingFrame,
PageScrollViewport,
@@ -31,6 +34,11 @@ import {
type MailServerProfile
} from "../../api/mail";
const MAIL_BOUNCE_DOCUMENTATION = {
topicId: "mail.bounce-processing",
documentationType: "admin"
} as const;
export default function MailBouncePage({ settings }: { settings: ApiSettings }) {
const navigate = useGuardedNavigate();
const [sources, setSources] = useState<MailBounceSource[]>([]);
@@ -50,6 +58,24 @@ export default function MailBouncePage({ settings }: { settings: ApiSettings })
() => new Map(profiles.map((profile) => [profile.id, profile.name])),
[profiles]
);
const imapProfiles = useMemo(
() => profiles.filter((profile) => profile.is_active && profile.imap),
[profiles]
);
const pageMutationBlocker = loading
? "Bounce evidence is already loading."
: busy
? "Wait for the current bounce-processing action to finish."
: "";
const addWatcherBlocker = pageMutationBlocker
|| (imapProfiles.length === 0 ? "Configure an active IMAP-enabled Mail profile before adding a watcher." : "");
const saveWatcherBlocker = busy
? "Wait for the current bounce-processing action to finish."
: !profileId
? "Select an active IMAP-enabled Mail profile."
: !folder.trim()
? "Enter the mailbox folder that contains delivery-status messages."
: "";
async function load() {
setLoading(true);
@@ -153,8 +179,8 @@ export default function MailBouncePage({ settings }: { settings: ApiSettings })
width: 92,
sticky: "end",
render: (source) => <TableActionGroup actions={[
{ id: "scan", label: "Scan now", icon: <RotateCw aria-hidden="true" />, disabled: Boolean(busy), onClick: () => void scan(source) },
{ id: "delete", label: "Remove watcher", icon: <Trash2 aria-hidden="true" />, variant: "danger", disabled: Boolean(busy), onClick: () => setDeleteSource(source) }
{ id: "scan", label: "Scan now", icon: <RotateCw aria-hidden="true" />, disabled: Boolean(busy), disabledReason: busy === `scan:${source.id}` ? "This mailbox scan is already running." : busy ? "Wait for the current bounce-processing action to finish." : "", onClick: () => void scan(source) },
{ id: "delete", label: "Remove watcher", icon: <Trash2 aria-hidden="true" />, variant: "danger", disabled: Boolean(busy), disabledReason: busy ? "Wait for the current bounce-processing action to finish." : "", onClick: () => setDeleteSource(source) }
]} />
}
];
@@ -174,8 +200,9 @@ export default function MailBouncePage({ settings }: { settings: ApiSettings })
<div><PageTitle loading={loading}>Bounce processing</PageTitle><p>Watch IMAP delivery-status folders and correlate recipient failures with Mail delivery commands.</p></div>
<div className="button-row compact-actions">
<Button onClick={() => navigate("/mail")}><ArrowLeft size={16} aria-hidden="true" /> Mailbox</Button>
<Button onClick={() => void load()} disabled={loading}><RefreshCw size={16} aria-hidden="true" /> Reload</Button>
<Button variant="primary" onClick={() => setAddOpen(true)} disabled={loading}><Plus size={16} aria-hidden="true" /> Add watcher</Button>
<DocumentationHelpLink reference={MAIL_BOUNCE_DOCUMENTATION} />
<Button onClick={() => void load()} disabled={Boolean(pageMutationBlocker)} disabledReason={pageMutationBlocker}><RefreshCw size={16} aria-hidden="true" /> Reload</Button>
<Button variant="primary" onClick={() => setAddOpen(true)} disabled={Boolean(addWatcherBlocker)} disabledReason={addWatcherBlocker}><Plus size={16} aria-hidden="true" /> Add watcher</Button>
</div>
</div>
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
@@ -192,25 +219,41 @@ export default function MailBouncePage({ settings }: { settings: ApiSettings })
</LoadingFrame>
</div>
<Dialog open={addOpen} title="Add bounce mailbox watcher" onClose={() => !busy && setAddOpen(false)} footer={<><Button onClick={() => setAddOpen(false)} disabled={Boolean(busy)}>Cancel</Button><Button variant="primary" onClick={() => void addSource()} disabled={Boolean(busy) || !profileId || !folder.trim()}>Add watcher</Button></>}>
<Dialog open={addOpen} title="Add bounce mailbox watcher" onClose={() => !busy && setAddOpen(false)} footer={<><Button onClick={() => setAddOpen(false)} disabled={Boolean(busy)} disabledReason={busy ? "Wait for the current bounce-processing action to finish." : undefined}>Cancel</Button><Button variant="primary" onClick={() => void addSource()} disabled={Boolean(saveWatcherBlocker)} disabledReason={saveWatcherBlocker}>Add watcher</Button></>}>
<div className="form-grid">
<FormField label="Mail profile">
<select value={profileId} onChange={(event) => setProfileId(event.target.value)}>
{imapProfiles.length === 0 &&
<ActionBlockerHint
reason={{
summary: "No active IMAP-enabled Mail profile can be watched.",
details: "Bounce processing reads a bounded authorized mailbox folder and cannot run without IMAP configuration.",
requiredAction: "Create or activate an IMAP server and credential first.",
actor: "Mail profile administrator",
target: "Settings or Administration > Mail profiles"
}}
documentation={MAIL_BOUNCE_DOCUMENTATION} />
}
<FormField label="Mail profile" documentation={MAIL_BOUNCE_DOCUMENTATION}>
<select value={profileId} disabled={Boolean(busy)} onChange={(event) => setProfileId(event.target.value)}>
<option value="">Select an IMAP profile</option>
{profiles.filter((profile) => profile.imap).map((profile) => <option key={profile.id} value={profile.id}>{profile.name}</option>)}
{imapProfiles.map((profile) => <option key={profile.id} value={profile.id}>{profile.name}</option>)}
</select>
</FormField>
<FormField label="Bounce folder">
<input value={folder} onChange={(event) => setFolder(event.target.value)} placeholder="INBOX" />
<FormField label="Bounce folder" documentation={MAIL_BOUNCE_DOCUMENTATION}>
<input value={folder} disabled={Boolean(busy)} onChange={(event) => setFolder(event.target.value)} placeholder="INBOX" />
</FormField>
<ToggleSwitch checked={active} onChange={setActive} label="Watch automatically" />
<ToggleSwitch checked={active} disabled={Boolean(busy)} onChange={setActive} label="Watch automatically" />
</div>
</Dialog>
<Dialog open={deleteSource !== null} title="Remove bounce mailbox watcher" onClose={() => !busy && setDeleteSource(null)} footer={<><Button onClick={() => setDeleteSource(null)} disabled={Boolean(busy)}>Cancel</Button><Button variant="danger" onClick={() => void removeSource()} disabled={Boolean(busy)}>Remove</Button></>}>
<p>The watcher will stop scanning this folder. Existing bounce observations and delivery evidence remain available.</p>
</Dialog>
<ConfirmDialog
open={deleteSource !== null}
title="Remove bounce mailbox watcher"
message="The watcher will stop scanning this folder. Existing bounce observations and delivery evidence remain available."
confirmLabel="Remove watcher"
tone="danger"
busy={Boolean(busy)}
onCancel={() => setDeleteSource(null)}
onConfirm={() => void removeSource()} />
</PageScrollViewport>
);
}