campaign sending prototype
This commit is contained in:
@@ -1,13 +1,20 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { MailServerSettingsPanel, type MailServerConnectionTestResult, type MailServerFolderLookupResult, type MailServerImapSettings, type MailServerSmtpSettings } from "@govoplan/core-webui";
|
||||
import { Pencil, Plus, Trash2 } from "lucide-react";
|
||||
import type { ApiSettings } from "../../types";
|
||||
import {
|
||||
createMailServerProfile,
|
||||
deactivateMailServerProfile,
|
||||
getMailProfilePolicy,
|
||||
listImapFolders,
|
||||
listMailProfileImapFolders,
|
||||
listMailServerProfiles,
|
||||
mailProfilePatternKeys,
|
||||
updateMailProfilePolicy,
|
||||
testImapSettings,
|
||||
testMailProfileImap,
|
||||
testMailProfileSmtp,
|
||||
testSmtpSettings,
|
||||
updateMailServerProfile,
|
||||
type MailCredentialPolicy,
|
||||
type MailImapTestPayload,
|
||||
@@ -28,7 +35,6 @@ import DataGrid, { type DataGridColumn } from "../../components/table/DataGrid";
|
||||
import Dialog from "../../components/Dialog";
|
||||
import DismissibleAlert from "../../components/DismissibleAlert";
|
||||
import FormField from "../../components/FormField";
|
||||
import ToggleSwitch from "../../components/ToggleSwitch";
|
||||
|
||||
export type MailProfileTargetOption = {
|
||||
id: string;
|
||||
@@ -290,7 +296,7 @@ export function MailProfileScopeManager({
|
||||
closeDisabled={busy}
|
||||
footer={<><Button onClick={() => setEditing(null)} disabled={busy}>Cancel</Button><Button variant="primary" onClick={() => void saveProfile()} disabled={!canWriteProfiles || busy || !draft.name.trim() || !scopeReady}>{busy ? "Saving…" : "Save profile"}</Button></>}
|
||||
>
|
||||
<ProfileForm draft={draft} setDraft={setDraft} editing={editing} busy={busy} canWrite={canWriteProfiles} canManageCredentials={canManageCredentials} />
|
||||
<ProfileForm settings={settings} draft={draft} setDraft={setDraft} editing={editing} busy={busy} canWrite={canWriteProfiles} canManageCredentials={canManageCredentials} />
|
||||
</Dialog>
|
||||
|
||||
<ConfirmDialog
|
||||
@@ -492,6 +498,7 @@ export function MailProfilePolicyEditor({
|
||||
}
|
||||
|
||||
function ProfileForm({
|
||||
settings,
|
||||
draft,
|
||||
setDraft,
|
||||
editing,
|
||||
@@ -499,6 +506,7 @@ function ProfileForm({
|
||||
canWrite,
|
||||
canManageCredentials
|
||||
}: {
|
||||
settings: ApiSettings;
|
||||
draft: ProfileDraft;
|
||||
setDraft: (draft: ProfileDraft) => void;
|
||||
editing: EditingProfile;
|
||||
@@ -506,10 +514,101 @@ function ProfileForm({
|
||||
canWrite: boolean;
|
||||
canManageCredentials: boolean;
|
||||
}) {
|
||||
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" | null>(null);
|
||||
|
||||
const disabled = busy || !canWrite;
|
||||
const credentialDisabled = disabled || !canManageCredentials;
|
||||
const existingHasImap = editing !== "new" && Boolean(editing?.imap);
|
||||
const existingProfile = editing !== "new" ? editing : null;
|
||||
const existingHasImap = Boolean(existingProfile?.imap);
|
||||
const imapToggleDisabled = disabled || (!canManageCredentials && existingHasImap && draft.imapEnabled);
|
||||
const useSavedSmtpTest = Boolean(existingProfile && !draft.smtpPassword && existingProfile.smtp_password_configured);
|
||||
const useSavedImapTest = Boolean(existingProfile && !draft.imapPassword && existingProfile.imap_password_configured);
|
||||
|
||||
useEffect(() => {
|
||||
setSmtpTestResult(null);
|
||||
setImapTestResult(null);
|
||||
setFolderResult(null);
|
||||
setMailActionState(null);
|
||||
}, [editing]);
|
||||
|
||||
function patchSmtpSettings(patch: Partial<MailServerSmtpSettings>) {
|
||||
setDraft({
|
||||
...draft,
|
||||
smtpHost: patch.host !== undefined ? String(patch.host ?? "") : draft.smtpHost,
|
||||
smtpPort: patch.port !== undefined ? String(patch.port ?? "") : draft.smtpPort,
|
||||
smtpUsername: patch.username !== undefined ? String(patch.username ?? "") : draft.smtpUsername,
|
||||
smtpPassword: patch.password !== undefined ? String(patch.password ?? "") : draft.smtpPassword,
|
||||
smtpSecurity: patch.security !== undefined ? readSecurity(String(patch.security || "starttls"), "starttls") : draft.smtpSecurity,
|
||||
smtpTimeout: patch.timeout_seconds !== undefined ? String(patch.timeout_seconds ?? "") : draft.smtpTimeout
|
||||
});
|
||||
}
|
||||
|
||||
function patchImapSettings(patch: Partial<MailServerImapSettings>) {
|
||||
setDraft({
|
||||
...draft,
|
||||
imapHost: patch.host !== undefined ? String(patch.host ?? "") : draft.imapHost,
|
||||
imapPort: patch.port !== undefined ? String(patch.port ?? "") : draft.imapPort,
|
||||
imapUsername: patch.username !== undefined ? String(patch.username ?? "") : draft.imapUsername,
|
||||
imapPassword: patch.password !== undefined ? String(patch.password ?? "") : draft.imapPassword,
|
||||
imapSecurity: patch.security !== undefined ? readSecurity(String(patch.security || "tls"), "tls") : draft.imapSecurity,
|
||||
imapSentFolder: patch.sent_folder !== undefined ? String(patch.sent_folder ?? "") : draft.imapSentFolder,
|
||||
imapTimeout: patch.timeout_seconds !== undefined ? String(patch.timeout_seconds ?? "") : draft.imapTimeout
|
||||
});
|
||||
}
|
||||
|
||||
async function runSmtpTest() {
|
||||
setMailActionState("smtp");
|
||||
setSmtpTestResult(null);
|
||||
try {
|
||||
setSmtpTestResult(useSavedSmtpTest && existingProfile
|
||||
? await testMailProfileSmtp(settings, existingProfile.id)
|
||||
: await testSmtpSettings(settings, smtpPayload(draft, false)));
|
||||
} catch (err) {
|
||||
setSmtpTestResult({ ok: false, protocol: "smtp", message: errorMessage(err), details: {} });
|
||||
} finally {
|
||||
setMailActionState(null);
|
||||
}
|
||||
}
|
||||
|
||||
async function runImapTest() {
|
||||
if (!draft.imapEnabled) return;
|
||||
setMailActionState("imap");
|
||||
setImapTestResult(null);
|
||||
try {
|
||||
setImapTestResult(useSavedImapTest && existingProfile
|
||||
? await testMailProfileImap(settings, existingProfile.id)
|
||||
: await testImapSettings(settings, imapPayload(draft, false)));
|
||||
} catch (err) {
|
||||
setImapTestResult({ ok: false, protocol: "imap", message: errorMessage(err), details: {} });
|
||||
} finally {
|
||||
setMailActionState(null);
|
||||
}
|
||||
}
|
||||
|
||||
async function runFolderLookup() {
|
||||
if (!draft.imapEnabled) return;
|
||||
setMailActionState("folders");
|
||||
setFolderResult(null);
|
||||
try {
|
||||
setFolderResult(useSavedImapTest && existingProfile
|
||||
? await listMailProfileImapFolders(settings, existingProfile.id)
|
||||
: await listImapFolders(settings, imapPayload(draft, false)));
|
||||
} catch (err) {
|
||||
setFolderResult({ ok: false, message: errorMessage(err), folders: [] });
|
||||
} finally {
|
||||
setMailActionState(null);
|
||||
}
|
||||
}
|
||||
|
||||
function useDetectedSentFolder() {
|
||||
const folder = folderResult?.detected_sent_folder;
|
||||
if (!folder || disabled || !draft.imapEnabled) return;
|
||||
setDraft({ ...draft, imapSentFolder: folder });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mail-profile-form">
|
||||
<div className="admin-form-grid two-columns">
|
||||
@@ -519,29 +618,32 @@ function ProfileForm({
|
||||
<FormField label="Description"><textarea rows={3} value={draft.description} disabled={disabled} onChange={(event) => setDraft({ ...draft, description: event.target.value })} /></FormField>
|
||||
</div>
|
||||
|
||||
<h3>SMTP</h3>
|
||||
<div className="admin-form-grid three-columns">
|
||||
<FormField label="Host"><input value={draft.smtpHost} disabled={disabled} onChange={(event) => setDraft({ ...draft, smtpHost: event.target.value })} /></FormField>
|
||||
<FormField label="Port"><input type="number" min={1} max={65535} value={draft.smtpPort} disabled={disabled} onChange={(event) => setDraft({ ...draft, smtpPort: event.target.value })} /></FormField>
|
||||
<FormField label="Security"><SecuritySelect value={draft.smtpSecurity} disabled={disabled} onChange={(smtpSecurity) => setDraft({ ...draft, smtpSecurity })} /></FormField>
|
||||
<FormField label="Username"><input value={draft.smtpUsername} disabled={disabled} onChange={(event) => setDraft({ ...draft, smtpUsername: event.target.value })} /></FormField>
|
||||
<FormField label="Password" help={editing === "new" ? undefined : "Leave empty to keep the current password."}><input type="password" value={draft.smtpPassword} disabled={credentialDisabled} onChange={(event) => setDraft({ ...draft, smtpPassword: event.target.value })} /></FormField>
|
||||
<FormField label="Timeout seconds"><input type="number" min={1} value={draft.smtpTimeout} disabled={disabled} onChange={(event) => setDraft({ ...draft, smtpTimeout: event.target.value })} /></FormField>
|
||||
</div>
|
||||
|
||||
<div className="mail-profile-imap-heading">
|
||||
<h3>IMAP</h3>
|
||||
<ToggleSwitch label="Enable IMAP" checked={draft.imapEnabled} disabled={imapToggleDisabled} onChange={(imapEnabled) => setDraft({ ...draft, imapEnabled })} />
|
||||
</div>
|
||||
<div className="admin-form-grid three-columns">
|
||||
<FormField label="Host"><input value={draft.imapHost} disabled={disabled || !draft.imapEnabled} onChange={(event) => setDraft({ ...draft, imapHost: event.target.value })} /></FormField>
|
||||
<FormField label="Port"><input type="number" min={1} max={65535} value={draft.imapPort} disabled={disabled || !draft.imapEnabled} onChange={(event) => setDraft({ ...draft, imapPort: event.target.value })} /></FormField>
|
||||
<FormField label="Security"><SecuritySelect value={draft.imapSecurity} disabled={disabled || !draft.imapEnabled} onChange={(imapSecurity) => setDraft({ ...draft, imapSecurity })} /></FormField>
|
||||
<FormField label="Username"><input value={draft.imapUsername} disabled={disabled || !draft.imapEnabled} onChange={(event) => setDraft({ ...draft, imapUsername: event.target.value })} /></FormField>
|
||||
<FormField label="Password" help={editing === "new" ? undefined : "Leave empty to keep the current password."}><input type="password" value={draft.imapPassword} disabled={credentialDisabled || !draft.imapEnabled} onChange={(event) => setDraft({ ...draft, imapPassword: event.target.value })} /></FormField>
|
||||
<FormField label="Sent folder"><input value={draft.imapSentFolder} disabled={disabled || !draft.imapEnabled} onChange={(event) => setDraft({ ...draft, imapSentFolder: event.target.value })} /></FormField>
|
||||
<FormField label="Timeout seconds"><input type="number" min={1} value={draft.imapTimeout} disabled={disabled || !draft.imapEnabled} onChange={(event) => setDraft({ ...draft, imapTimeout: event.target.value })} /></FormField>
|
||||
</div>
|
||||
<MailServerSettingsPanel
|
||||
smtp={{ host: draft.smtpHost, port: draft.smtpPort, username: draft.smtpUsername, password: draft.smtpPassword, security: draft.smtpSecurity, timeout_seconds: draft.smtpTimeout }}
|
||||
imap={{ enabled: draft.imapEnabled, host: draft.imapHost, port: draft.imapPort, username: draft.imapUsername, password: draft.imapPassword, security: draft.imapSecurity, sent_folder: draft.imapSentFolder, timeout_seconds: draft.imapTimeout }}
|
||||
onSmtpChange={patchSmtpSettings}
|
||||
onImapChange={patchImapSettings}
|
||||
onImapEnabledChange={(imapEnabled) => setDraft({ ...draft, imapEnabled })}
|
||||
smtpDisabled={disabled}
|
||||
smtpCredentialDisabled={credentialDisabled}
|
||||
imapToggleDisabled={imapToggleDisabled}
|
||||
imapServerDisabled={disabled || !draft.imapEnabled}
|
||||
imapCredentialDisabled={credentialDisabled || !draft.imapEnabled}
|
||||
imapActionDisabled={disabled || !draft.imapEnabled}
|
||||
smtpTitle="SMTP"
|
||||
imapTitle="IMAP"
|
||||
smtpTestLabel={useSavedSmtpTest ? "Test saved SMTP" : "Test SMTP"}
|
||||
imapTestLabel={useSavedImapTest ? "Test saved IMAP" : "Test IMAP"}
|
||||
busyAction={mailActionState}
|
||||
onTestSmtp={() => void runSmtpTest()}
|
||||
onTestImap={() => void runImapTest()}
|
||||
onLookupFolders={() => void runFolderLookup()}
|
||||
smtpTestResult={smtpTestResult}
|
||||
imapTestResult={imapTestResult}
|
||||
folderLookupResult={folderResult}
|
||||
onUseDetectedFolder={useDetectedSentFolder}
|
||||
useDetectedFolderDisabled={disabled || !draft.imapEnabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -586,10 +688,6 @@ function PatternTextarea({ label, value, disabled, onChange }: { label: string;
|
||||
return <FormField label={label}><textarea rows={3} value={value} disabled={disabled} onChange={(event) => onChange(event.target.value)} placeholder="*.example.org" /></FormField>;
|
||||
}
|
||||
|
||||
function SecuritySelect({ value, disabled, onChange }: { value: MailSecurity; disabled: boolean; onChange: (value: MailSecurity) => void }) {
|
||||
return <select value={value} disabled={disabled} onChange={(event) => onChange(readSecurity(event.target.value, value))}>{securityOptions.map((option) => <option key={option} value={option}>{option}</option>)}</select>;
|
||||
}
|
||||
|
||||
function TransportCell({ profile, protocol }: { profile: MailServerProfile; protocol: "smtp" | "imap" }) {
|
||||
const transport = protocol === "smtp" ? profile.smtp : profile.imap;
|
||||
if (!transport) return <span className="muted">Not configured</span>;
|
||||
|
||||
Reference in New Issue
Block a user