campaign sending prototype
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { MailServerSettingsPanel, formatDateTime as formatPlatformDateTime, type MailServerConnectionTestResult, type MailServerFolderLookupResult, type MailServerImapSettings, type MailServerSmtpSettings } from "@govoplan/core-webui";
|
||||
import type { ApiSettings } from "../../types";
|
||||
import Button from "../../components/Button";
|
||||
import Card from "../../components/Card";
|
||||
@@ -8,7 +9,6 @@ import LoadingFrame from "../../components/LoadingFrame";
|
||||
import LockedVersionNotice from "./components/LockedVersionNotice";
|
||||
import VersionLine from "./components/VersionLine";
|
||||
import MessagePreviewOverlay, { type MessagePreviewAttachment } from "./components/MessagePreviewOverlay";
|
||||
import ToggleSwitch from "../../components/ToggleSwitch";
|
||||
import DismissibleAlert from "../../components/DismissibleAlert";
|
||||
import DataGrid, { type DataGridColumn } from "../../components/table/DataGrid";
|
||||
import { MailProfilePolicyEditor } from "../mail/MailProfileManagement";
|
||||
@@ -26,8 +26,6 @@ import {
|
||||
testMailProfileSmtp,
|
||||
testSmtpSettings,
|
||||
updateMockMailboxFailures,
|
||||
type MailConnectionTestResponse,
|
||||
type MailImapFolderListResponse,
|
||||
type MailProfilePolicy,
|
||||
type MailSecurity,
|
||||
type MailServerProfile,
|
||||
@@ -42,9 +40,9 @@ const securityOptions = ["plain", "tls", "starttls"];
|
||||
|
||||
export default function MailSettingsPage({ settings, campaignId }: { settings: ApiSettings; campaignId: string }) {
|
||||
const { data, loading, error, reload, setError } = useCampaignWorkspaceData(settings, campaignId);
|
||||
const [smtpTestResult, setSmtpTestResult] = useState<MailConnectionTestResponse | null>(null);
|
||||
const [imapTestResult, setImapTestResult] = useState<MailConnectionTestResponse | null>(null);
|
||||
const [folderResult, setFolderResult] = useState<MailImapFolderListResponse | null>(null);
|
||||
const [smtpTestResult, setSmtpTestResult] = useState<MailServerConnectionTestResult | null>(null);
|
||||
const [imapTestResult, setImapTestResult] = useState<MailServerConnectionTestResult | null>(null);
|
||||
const [folderResult, setFolderResult] = useState<MailServerFolderLookupResult | null>(null);
|
||||
const [mailActionState, setMailActionState] = useState<"smtp" | "imap" | "folders" | "mock" | null>(null);
|
||||
const [mockMessages, setMockMessages] = useState<MockMailboxMessage[]>([]);
|
||||
const [selectedMockMessage, setSelectedMockMessage] = useState<MockMailboxMessage | null>(null);
|
||||
@@ -164,6 +162,25 @@ export default function MailSettingsPage({ settings, campaignId }: { settings: A
|
||||
}
|
||||
}
|
||||
|
||||
function patchSmtpSettings(patchValue: Partial<MailServerSmtpSettings>) {
|
||||
if (patchValue.host !== undefined) patch(["server", "smtp", "host"], String(patchValue.host ?? ""));
|
||||
if (patchValue.port !== undefined) patch(["server", "smtp", "port"], Number(patchValue.port || 0));
|
||||
if (patchValue.username !== undefined) patch(["server", "smtp", "username"], String(patchValue.username ?? ""));
|
||||
if (patchValue.password !== undefined) patch(["server", "smtp", "password"], String(patchValue.password ?? ""));
|
||||
if (patchValue.security !== undefined) patch(["server", "smtp", "security"], String(patchValue.security || "starttls"));
|
||||
if (patchValue.timeout_seconds !== undefined) patch(["server", "smtp", "timeout_seconds"], Number(patchValue.timeout_seconds || 0));
|
||||
}
|
||||
|
||||
function patchImapSettings(patchValue: Partial<MailServerImapSettings>) {
|
||||
if (patchValue.host !== undefined) patch(["server", "imap", "host"], String(patchValue.host ?? ""));
|
||||
if (patchValue.port !== undefined) patch(["server", "imap", "port"], Number(patchValue.port || 0));
|
||||
if (patchValue.username !== undefined) patch(["server", "imap", "username"], String(patchValue.username ?? ""));
|
||||
if (patchValue.password !== undefined) patch(["server", "imap", "password"], String(patchValue.password ?? ""));
|
||||
if (patchValue.security !== undefined) patch(["server", "imap", "security"], String(patchValue.security || "tls"));
|
||||
if (patchValue.sent_folder !== undefined) patch(["server", "imap", "sent_folder"], String(patchValue.sent_folder ?? ""));
|
||||
if (patchValue.timeout_seconds !== undefined) patch(["server", "imap", "timeout_seconds"], Number(patchValue.timeout_seconds || 0));
|
||||
}
|
||||
|
||||
function profileScopeLabel(profile: MailServerProfile): string {
|
||||
if (profile.scope_type === "system") return "system";
|
||||
if (profile.scope_type === "tenant") return "tenant";
|
||||
@@ -451,54 +468,57 @@ export default function MailSettingsPage({ settings, campaignId }: { settings: A
|
||||
</Card>
|
||||
|
||||
<Card title="Mail server settings">
|
||||
<div className="mail-server-settings-grid">
|
||||
<section className="form-subsection mail-server-subsection">
|
||||
<div className="subsection-heading split">
|
||||
<h3>SMTP login</h3>
|
||||
<ToggleSwitch label="Mock server settings" checked={mockSandboxEnabled} disabled={locked || usingMailProfile} onChange={toggleMockMailSandbox} />
|
||||
</div>
|
||||
<div className="form-grid compact responsive-form-grid">
|
||||
<FormField label="Host"><input value={getText(smtp, "host")} disabled={smtpDisabled} onChange={(event) => patch(["server", "smtp", "host"], event.target.value)} /></FormField>
|
||||
<FormField label="Port"><input type="number" value={getNumber(smtp, "port", 587)} disabled={smtpDisabled} onChange={(event) => patch(["server", "smtp", "port"], Number(event.target.value || 0))} /></FormField>
|
||||
<FormField label="Username"><input value={getText(smtp, "username")} disabled={smtpCredentialDisabled} onChange={(event) => patch(["server", "smtp", "username"], event.target.value)} /></FormField>
|
||||
<FormField label="Password"><input type="password" value={getText(smtp, "password")} disabled={smtpCredentialDisabled} onChange={(event) => patch(["server", "smtp", "password"], event.target.value)} /></FormField>
|
||||
<FormField label="Security"><select value={getText(smtp, "security", "starttls")} disabled={smtpDisabled} onChange={(event) => patch(["server", "smtp", "security"], event.target.value)}>{securityOptions.map((option) => <option key={option}>{option}</option>)}</select></FormField>
|
||||
<FormField label="Timeout seconds"><input type="number" value={getNumber(smtp, "timeout_seconds", 30)} disabled={smtpDisabled} onChange={(event) => patch(["server", "smtp", "timeout_seconds"], Number(event.target.value || 0))} /></FormField>
|
||||
</div>
|
||||
<div className="button-row compact-actions subsection-bottom-actions">
|
||||
<Button variant="primary" onClick={runSmtpTest} disabled={locked || mailActionState === "smtp"}>{mailActionState === "smtp" ? "Testing…" : (usingMailProfile ? "Test profile SMTP" : "Test SMTP login")}</Button>
|
||||
</div>
|
||||
<MailActionResult result={smtpTestResult} />
|
||||
</section>
|
||||
|
||||
<section className="form-subsection mail-server-subsection">
|
||||
<div className="subsection-heading split">
|
||||
<h3>IMAP sent-folder append</h3>
|
||||
</div>
|
||||
<div className="form-grid compact responsive-form-grid">
|
||||
<div className="form-span-full toggle-span-full">
|
||||
<ToggleSwitch label="Enable IMAP" checked={imapEnabled} disabled={locked || usingMailProfile} onChange={toggleImap} />
|
||||
</div>
|
||||
<FormField label="Host"><input value={getText(imap, "host")} disabled={imapServerDisabled} onChange={(event) => patch(["server", "imap", "host"], event.target.value)} /></FormField>
|
||||
<FormField label="Port"><input type="number" value={getNumber(imap, "port", 993)} disabled={imapServerDisabled} onChange={(event) => patch(["server", "imap", "port"], Number(event.target.value || 0))} /></FormField>
|
||||
<FormField label="Username"><input value={getText(imap, "username")} disabled={imapCredentialDisabled} onChange={(event) => patch(["server", "imap", "username"], event.target.value)} /></FormField>
|
||||
<FormField label="Password"><input type="password" value={getText(imap, "password")} disabled={imapCredentialDisabled} onChange={(event) => patch(["server", "imap", "password"], event.target.value)} /></FormField>
|
||||
<FormField label="Security"><select value={getText(imap, "security", "tls")} disabled={imapServerDisabled} onChange={(event) => patch(["server", "imap", "security"], event.target.value)}>{securityOptions.map((option) => <option key={option}>{option}</option>)}</select></FormField>
|
||||
<FormField label="Detected/saved sent folder"><input value={getText(imap, "sent_folder", "auto")} disabled={imapServerDisabled} onChange={(event) => patch(["server", "imap", "sent_folder"], event.target.value)} /></FormField>
|
||||
<div className="form-span-full toggle-span-full">
|
||||
<ToggleSwitch label="Append successfully sent messages to Sent" checked={getBool(imapAppend, "enabled")} disabled={imapDisabled} onChange={(checked) => patch(["delivery", "imap_append_sent", "enabled"], checked)} />
|
||||
</div>
|
||||
<FormField label="Append folder"><input value={getText(imapAppend, "folder", getText(imap, "sent_folder", "auto"))} disabled={imapDisabled || !getBool(imapAppend, "enabled")} onChange={(event) => patch(["delivery", "imap_append_sent", "folder"], event.target.value)} /></FormField>
|
||||
</div>
|
||||
<div className="button-row compact-actions subsection-bottom-actions">
|
||||
<Button variant="primary" onClick={runImapTest} disabled={imapDisabled || mailActionState === "imap"}>{mailActionState === "imap" ? "Testing…" : (usingMailProfile ? "Test profile IMAP" : "Test IMAP login")}</Button>
|
||||
<Button variant="primary" onClick={runFolderLookup} disabled={imapDisabled || mailActionState === "folders"}>{mailActionState === "folders" ? "Looking up…" : "Folders…"}</Button>
|
||||
</div>
|
||||
<p className="muted small-note">Folder lookup lists visible mailboxes and guesses folders such as Sent, Gesendet or Sent Mail.</p>
|
||||
<MailActionResult result={imapTestResult} />
|
||||
<FolderLookupResult result={folderResult} disabled={imapDisabled} onUseDetected={useDetectedSentFolder} />
|
||||
</section>
|
||||
</div>
|
||||
<MailServerSettingsPanel
|
||||
smtp={{
|
||||
host: getText(smtp, "host"),
|
||||
port: getNumber(smtp, "port", 587),
|
||||
username: getText(smtp, "username"),
|
||||
password: getText(smtp, "password"),
|
||||
security: getText(smtp, "security", "starttls"),
|
||||
timeout_seconds: getNumber(smtp, "timeout_seconds", 30)
|
||||
}}
|
||||
imap={{
|
||||
enabled: imapEnabled,
|
||||
host: getText(imap, "host"),
|
||||
port: getNumber(imap, "port", 993),
|
||||
username: getText(imap, "username"),
|
||||
password: getText(imap, "password"),
|
||||
security: getText(imap, "security", "tls"),
|
||||
sent_folder: getText(imap, "sent_folder", "auto"),
|
||||
timeout_seconds: getNumber(imap, "timeout_seconds", 30)
|
||||
}}
|
||||
onSmtpChange={patchSmtpSettings}
|
||||
onImapChange={patchImapSettings}
|
||||
onImapEnabledChange={toggleImap}
|
||||
smtpDisabled={smtpDisabled}
|
||||
smtpCredentialDisabled={smtpCredentialDisabled}
|
||||
smtpActionDisabled={locked}
|
||||
imapToggleDisabled={locked || usingMailProfile}
|
||||
imapServerDisabled={imapServerDisabled}
|
||||
imapCredentialDisabled={imapCredentialDisabled}
|
||||
imapActionDisabled={imapDisabled}
|
||||
mockToggle={{ checked: mockSandboxEnabled, disabled: locked || usingMailProfile, onChange: toggleMockMailSandbox }}
|
||||
append={{
|
||||
enabled: getBool(imapAppend, "enabled"),
|
||||
folder: getText(imapAppend, "folder", getText(imap, "sent_folder", "auto")),
|
||||
disabled: imapDisabled,
|
||||
folderDisabled: imapDisabled || !getBool(imapAppend, "enabled"),
|
||||
onEnabledChange: (checked) => patch(["delivery", "imap_append_sent", "enabled"], checked),
|
||||
onFolderChange: (folder) => patch(["delivery", "imap_append_sent", "folder"], folder)
|
||||
}}
|
||||
smtpTestLabel={usingMailProfile ? "Test profile SMTP" : "Test SMTP login"}
|
||||
imapTestLabel={usingMailProfile ? "Test profile IMAP" : "Test IMAP login"}
|
||||
busyAction={mailActionState}
|
||||
onTestSmtp={runSmtpTest}
|
||||
onTestImap={runImapTest}
|
||||
onLookupFolders={runFolderLookup}
|
||||
smtpTestResult={smtpTestResult}
|
||||
imapTestResult={imapTestResult}
|
||||
folderLookupResult={folderResult}
|
||||
onUseDetectedFolder={useDetectedSentFolder}
|
||||
useDetectedFolderDisabled={imapDisabled}
|
||||
floatingResults
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card title="Mock mail sandbox">
|
||||
@@ -538,7 +558,7 @@ export default function MailSettingsPage({ settings, campaignId }: { settings: A
|
||||
bodyMode="text"
|
||||
text={selectedMockMessage.body_preview || ""}
|
||||
recipientLabel={selectedMockMessage.kind === "imap_append" ? "Mock IMAP append" : "Mock SMTP delivery"}
|
||||
recipientNote={selectedMockMessage.created_at ? new Date(selectedMockMessage.created_at).toLocaleString() : undefined}
|
||||
recipientNote={selectedMockMessage.created_at ? formatPlatformDateTime(selectedMockMessage.created_at) : undefined}
|
||||
metaItems={mockMessageMetaItems(selectedMockMessage)}
|
||||
attachments={mockMessageAttachments(selectedMockMessage)}
|
||||
raw={selectedMockMessage.raw_eml}
|
||||
@@ -572,10 +592,7 @@ function mockMessageAttachments(message: MockMailboxMessage): MessagePreviewAtta
|
||||
}
|
||||
|
||||
function formatMockDate(value: string): string {
|
||||
if (!value) return "—";
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return value;
|
||||
return date.toLocaleString();
|
||||
return formatPlatformDateTime(value);
|
||||
}
|
||||
|
||||
|
||||
@@ -589,39 +606,3 @@ function mockMailboxColumns(openMockMessage: (id: string) => Promise<void>): Dat
|
||||
{ id: "actions", header: "Actions", width: 110, sticky: "end", render: (message) => <Button onClick={() => openMockMessage(message.id)}>Open</Button> }
|
||||
];
|
||||
}
|
||||
|
||||
function MailActionResult({ result }: { result: MailConnectionTestResponse | null }) {
|
||||
if (!result) return null;
|
||||
const authenticated = result.details?.authenticated;
|
||||
return (
|
||||
<DismissibleAlert tone={result.ok ? "success" : "danger"} resetKey={`${result.ok}:${result.message}`} floating>
|
||||
{result.message}
|
||||
{result.ok && typeof authenticated === "boolean" && (
|
||||
<span> Authentication: {authenticated ? "credentials accepted" : "not used"}.</span>
|
||||
)}
|
||||
</DismissibleAlert>
|
||||
);
|
||||
}
|
||||
|
||||
function FolderLookupResult({ result, disabled, onUseDetected }: { result: MailImapFolderListResponse | null; disabled?: boolean; onUseDetected: () => void }) {
|
||||
if (!result) return null;
|
||||
if (!result.ok) {
|
||||
return <DismissibleAlert tone="danger" resetKey={result.message}>{result.message}</DismissibleAlert>;
|
||||
}
|
||||
|
||||
return (
|
||||
<DismissibleAlert tone="success" resetKey={`${result.message}:${result.detected_sent_folder || ""}`}>
|
||||
<p>{result.message}</p>
|
||||
<p>Detected Sent folder: <strong>{result.detected_sent_folder || "—"}</strong></p>
|
||||
{result.detected_sent_folder && <Button onClick={onUseDetected} disabled={disabled}>Use detected folder</Button>}
|
||||
{result.folders.length > 0 && (
|
||||
<div className="field-chip-list">
|
||||
{result.folders.slice(0, 12).map((folder) => (
|
||||
<span className="field-chip" key={folder.name} title={(folder.flags || []).join(" ")}>{folder.name}</span>
|
||||
))}
|
||||
{result.folders.length > 12 && <span className="field-chip">+{result.folders.length - 12} more</span>}
|
||||
</div>
|
||||
)}
|
||||
</DismissibleAlert>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user