Migrate Mail surfaces to interface patterns
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user