intermittent commit
This commit is contained in:
@@ -37,6 +37,16 @@ import { ConfirmDialog } from "@govoplan/core-webui";
|
||||
import { Dialog } from "@govoplan/core-webui";
|
||||
import { DismissibleAlert } from "@govoplan/core-webui";
|
||||
import { FormField, i18nMessage, useUnsavedDraftGuard } from "@govoplan/core-webui";
|
||||
import {
|
||||
mailProfileChildDescriptors,
|
||||
mailProfileEditTargetInitialSection,
|
||||
mailProfileEditTargetPanelMode,
|
||||
mailProfileEditTargetShowsProfileFields,
|
||||
mailProfileEditTargetShowsSettingsPanel,
|
||||
mailProfileEditTargetVisibleSections,
|
||||
type MailProfileEditTarget,
|
||||
type MailProfileProtocol
|
||||
} from "./mailProfileEditorModel";
|
||||
export type MailProfileTargetOption = {
|
||||
id: string;
|
||||
label: string;
|
||||
@@ -96,7 +106,8 @@ type PolicyFlagValue = "inherit" | "allow" | "deny";
|
||||
type CredentialInheritanceValue = "inherit" | "profile" | "local";
|
||||
type MailProfileTreeRow =
|
||||
{kind: "profile";id: string;profile: MailServerProfile;} |
|
||||
{kind: "credential";id: string;profile: MailServerProfile;protocol: "smtp" | "imap";};
|
||||
{kind: "server";id: string;profile: MailServerProfile;protocol: MailProfileProtocol;} |
|
||||
{kind: "credential";id: string;profile: MailServerProfile;protocol: MailProfileProtocol;};
|
||||
|
||||
const securityOptions = mailServerSecurityOptions as readonly MailSecurity[];
|
||||
|
||||
@@ -135,6 +146,7 @@ export function MailProfileScopeManager({
|
||||
const [profiles, setProfiles] = useState<MailServerProfile[]>([]);
|
||||
const [selectedTargetId, setSelectedTargetId] = useState(scopeId || targetOptions[0]?.id || "");
|
||||
const [editing, setEditing] = useState<EditingProfile>(null);
|
||||
const [editingTarget, setEditingTarget] = useState<MailProfileEditTarget>({ kind: "create" });
|
||||
const [deactivating, setDeactivating] = useState<MailServerProfile | null>(null);
|
||||
const [draft, setDraft] = useState<ProfileDraft>(emptyProfileDraft());
|
||||
const [savedProfileDraftKey, setSavedProfileDraftKey] = useState(profileDraftKey(emptyProfileDraft()));
|
||||
@@ -233,15 +245,17 @@ export function MailProfileScopeManager({
|
||||
const nextDraft = emptyProfileDraft();
|
||||
setDraft(nextDraft);
|
||||
setSavedProfileDraftKey(profileDraftKey(nextDraft));
|
||||
setEditingTarget({ kind: "create" });
|
||||
setEditing("new");
|
||||
setError("");
|
||||
setSuccess("");
|
||||
}
|
||||
|
||||
function openEdit(profile: MailServerProfile) {
|
||||
function openEdit(profile: MailServerProfile, target: MailProfileEditTarget = { kind: "profile" }) {
|
||||
const nextDraft = profileToDraft(profile);
|
||||
setDraft(nextDraft);
|
||||
setSavedProfileDraftKey(profileDraftKey(nextDraft));
|
||||
setEditingTarget(target);
|
||||
setEditing(profile);
|
||||
setError("");
|
||||
setSuccess("");
|
||||
@@ -252,6 +266,7 @@ export function MailProfileScopeManager({
|
||||
const nextDraft = emptyProfileDraft();
|
||||
setDraft(nextDraft);
|
||||
setSavedProfileDraftKey(profileDraftKey(nextDraft));
|
||||
setEditingTarget({ kind: "create" });
|
||||
}
|
||||
|
||||
async function saveProfile(): Promise<boolean> {
|
||||
@@ -308,6 +323,8 @@ export function MailProfileScopeManager({
|
||||
width: "minmax(260px, 1.4fr)",
|
||||
render: (row) => row.kind === "profile" ?
|
||||
<div className="connection-tree-main"><strong>{row.profile.name}</strong><div className="connection-tree-muted-line">{row.profile.slug} · {scopeLabel(row.profile)}</div></div> :
|
||||
row.kind === "server" ?
|
||||
<MailServerTreeCell profile={row.profile} protocol={row.protocol} /> :
|
||||
<MailCredentialTreeCell profile={row.profile} protocol={row.protocol} />
|
||||
},
|
||||
{
|
||||
@@ -322,7 +339,7 @@ export function MailProfileScopeManager({
|
||||
id: "policy",
|
||||
header: "i18n:govoplan-mail.policy.bb9cf141",
|
||||
width: "minmax(150px, 0.8fr)",
|
||||
render: (row) => row.kind === "profile" ? scopeLabel(row.profile) : mailCredentialPolicySummary(profileEffectivePolicy, row.protocol)
|
||||
render: (row) => row.kind === "profile" ? scopeLabel(row.profile) : row.kind === "server" ? row.protocol.toUpperCase() : mailCredentialPolicySummary(profileEffectivePolicy, row.protocol)
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
@@ -330,22 +347,33 @@ export function MailProfileScopeManager({
|
||||
width: "110px",
|
||||
render: (row) => row.kind === "profile" ?
|
||||
<span className={`status-badge ${row.profile.is_active ? "success" : "neutral"}`}>{row.profile.is_active ? "i18n:govoplan-mail.active.a733b809" : "i18n:govoplan-mail.inactive.09af574c"}</span> :
|
||||
row.kind === "server" ?
|
||||
<span className={`status-badge ${transportConfigured(row.profile, row.protocol) ? "success" : "neutral"}`}>{transportConfigured(row.profile, row.protocol) ? "i18n:govoplan-core.configured.668c5fff" : "i18n:govoplan-core.not_configured.811931bb"}</span> :
|
||||
<span className={`status-badge ${mailCredentialConfigured(row.profile, row.protocol) ? "success" : "neutral"}`}>{mailCredentialConfigured(row.profile, row.protocol) ? "i18n:govoplan-mail.saved.c0ae8f6e" : "i18n:govoplan-mail.local.dc99d54d"}</span>
|
||||
}],
|
||||
[profileEffectivePolicy]);
|
||||
|
||||
function mailProfileChildren(row: MailProfileTreeRow): MailProfileTreeRow[] {
|
||||
if (row.kind !== "profile") return [];
|
||||
const children: MailProfileTreeRow[] = [{ kind: "credential", id: `credential:${row.profile.id}:smtp`, profile: row.profile, protocol: "smtp" }];
|
||||
if (row.profile.imap) children.push({ kind: "credential", id: `credential:${row.profile.id}:imap`, profile: row.profile, protocol: "imap" });
|
||||
const children: MailProfileTreeRow[] = [
|
||||
...mailProfileChildDescriptors(row.profile).map((child) => ({ ...child, profile: row.profile }))
|
||||
];
|
||||
return children;
|
||||
}
|
||||
|
||||
function renderMailProfileActions(row: MailProfileTreeRow) {
|
||||
if (row.kind === "server") {
|
||||
const label = `Edit ${row.protocol.toUpperCase()} server`;
|
||||
return (
|
||||
<div className="admin-icon-actions">
|
||||
<Button type="button" className="admin-icon-button" title={label} aria-label={label} onClick={() => openEdit(row.profile, { kind: "server", protocol: row.protocol })} disabled={!canWriteProfiles || busy}><Pencil size={16} /></Button>
|
||||
</div>);
|
||||
|
||||
}
|
||||
if (row.kind === "credential") {
|
||||
return (
|
||||
<div className="admin-icon-actions">
|
||||
<Button type="button" className="admin-icon-button" title={i18nMessage("i18n:govoplan-mail.edit_value_credentials.1e2dc4f6", { value0: row.protocol.toUpperCase() })} aria-label={i18nMessage("i18n:govoplan-mail.edit_value_credentials.1e2dc4f6", { value0: row.protocol.toUpperCase() })} onClick={() => openEdit(row.profile)} disabled={!canWriteProfiles || busy}><Pencil size={16} /></Button>
|
||||
<Button type="button" className="admin-icon-button" title={i18nMessage("i18n:govoplan-mail.edit_value_credentials.1e2dc4f6", { value0: row.protocol.toUpperCase() })} aria-label={i18nMessage("i18n:govoplan-mail.edit_value_credentials.1e2dc4f6", { value0: row.protocol.toUpperCase() })} onClick={() => openEdit(row.profile, { kind: "credentials", protocol: row.protocol })} disabled={!canWriteProfiles || busy}><Pencil size={16} /></Button>
|
||||
</div>);
|
||||
|
||||
}
|
||||
@@ -411,13 +439,13 @@ export function MailProfileScopeManager({
|
||||
|
||||
<Dialog
|
||||
open={editing !== null}
|
||||
title={editing === "new" ? "i18n:govoplan-mail.create_mail_profile.4d2f8f9f" : "i18n:govoplan-mail.edit_mail_profile.95d1af9c"}
|
||||
onClose={() => !busy && setEditing(null)}
|
||||
title={profileDialogTitle(editing, editingTarget)}
|
||||
onClose={() => !busy && closeProfileEditor()}
|
||||
className="admin-dialog admin-dialog-wide mail-profile-dialog"
|
||||
closeDisabled={busy}
|
||||
footer={<><Button onClick={() => setEditing(null)} disabled={busy}>i18n:govoplan-mail.cancel.77dfd213</Button><Button variant="primary" onClick={() => void saveProfile()} disabled={!canWriteProfiles || busy || !draft.name.trim() || !scopeReady}>{busy ? "i18n:govoplan-mail.saving.56a2285c" : "i18n:govoplan-mail.save_profile.f597c0e8"}</Button></>}>
|
||||
footer={<><Button onClick={closeProfileEditor} disabled={busy}>i18n:govoplan-mail.cancel.77dfd213</Button><Button variant="primary" onClick={() => void saveProfile()} disabled={!canWriteProfiles || busy || !draft.name.trim() || !scopeReady}>{busy ? "i18n:govoplan-mail.saving.56a2285c" : "i18n:govoplan-mail.save_profile.f597c0e8"}</Button></>}>
|
||||
|
||||
<ProfileForm settings={settings} draft={draft} setDraft={setDraft} editing={editing} busy={busy} canWrite={canWriteProfiles} canManageCredentials={canManageCredentials} effectivePolicy={profileEffectivePolicy} />
|
||||
<ProfileForm settings={settings} draft={draft} setDraft={setDraft} editing={editing} busy={busy} canWrite={canWriteProfiles} canManageCredentials={canManageCredentials} effectivePolicy={profileEffectivePolicy} editTarget={editingTarget} />
|
||||
</Dialog>
|
||||
|
||||
<ConfirmDialog
|
||||
@@ -771,17 +799,9 @@ function ProfileForm({
|
||||
busy,
|
||||
canWrite,
|
||||
canManageCredentials,
|
||||
effectivePolicy
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}: {settings: ApiSettings;draft: ProfileDraft;setDraft: (draft: ProfileDraft) => void;editing: EditingProfile;busy: boolean;canWrite: boolean;canManageCredentials: boolean;effectivePolicy?: MailProfilePolicy | null;}) {
|
||||
effectivePolicy,
|
||||
editTarget
|
||||
}: {settings: ApiSettings;draft: ProfileDraft;setDraft: (draft: ProfileDraft) => void;editing: EditingProfile;busy: boolean;canWrite: boolean;canManageCredentials: boolean;effectivePolicy?: MailProfilePolicy | null;editTarget: MailProfileEditTarget;}) {
|
||||
const [smtpTestResult, setSmtpTestResult] = useState<MailServerConnectionTestResult | null>(null);
|
||||
const [imapTestResult, setImapTestResult] = useState<MailServerConnectionTestResult | null>(null);
|
||||
const [folderResult, setFolderResult] = useState<MailServerFolderLookupResult | null>(null);
|
||||
@@ -790,6 +810,11 @@ function ProfileForm({
|
||||
const disabled = busy || !canWrite;
|
||||
const credentialDisabled = disabled || !canManageCredentials;
|
||||
const existingProfile = editing !== "new" ? editing : null;
|
||||
const initialSection = mailProfileEditTargetInitialSection(editTarget);
|
||||
const settingsPanelMode = mailProfileEditTargetPanelMode(editTarget);
|
||||
const visibleSections = mailProfileEditTargetVisibleSections(editTarget);
|
||||
const showProfileFields = mailProfileEditTargetShowsProfileFields(editTarget);
|
||||
const showSettingsPanel = mailProfileEditTargetShowsSettingsPanel(editTarget);
|
||||
const draftHasImap = hasDraftImapSettings(draft);
|
||||
const useSavedSmtpTest = Boolean(existingProfile && !draft.smtpPassword && existingProfile.smtp_password_configured);
|
||||
const useSavedImapTest = Boolean(existingProfile && !draft.imapPassword && existingProfile.imap_password_configured);
|
||||
@@ -803,7 +828,7 @@ function ProfileForm({
|
||||
setImapTestResult(null);
|
||||
setFolderResult(null);
|
||||
setMailActionState(null);
|
||||
}, [editing]);
|
||||
}, [editing, editTarget]);
|
||||
|
||||
function patchSmtpSettings(patch: Partial<MailServerSmtpSettings>) {
|
||||
setDraft({
|
||||
@@ -896,12 +921,18 @@ function ProfileForm({
|
||||
|
||||
return (
|
||||
<div className="mail-profile-form">
|
||||
{showProfileFields &&
|
||||
<div className="admin-form-grid two-columns">
|
||||
<FormField label="i18n:govoplan-mail.name.709a2322"><input value={draft.name} disabled={disabled} onChange={(event) => setDraft({ ...draft, name: event.target.value })} /></FormField>
|
||||
<FormField label="i18n:govoplan-mail.slug.094da9b9"><input value={draft.slug} disabled={disabled} onChange={(event) => setDraft({ ...draft, slug: event.target.value })} placeholder="i18n:govoplan-mail.generated_from_name.33d69a91" /></FormField>
|
||||
<FormField label="i18n:govoplan-mail.status.bae7d5be"><select value={draft.isActive ? "active" : "inactive"} disabled={disabled} onChange={(event) => setDraft({ ...draft, isActive: event.target.value === "active" })}><option value="active">i18n:govoplan-mail.active.a733b809</option><option value="inactive">i18n:govoplan-mail.inactive.09af574c</option></select></FormField>
|
||||
<FormField label="i18n:govoplan-mail.description.55f8ebc8"><textarea rows={3} value={draft.description} disabled={disabled} onChange={(event) => setDraft({ ...draft, description: event.target.value })} /></FormField>
|
||||
</div>
|
||||
}
|
||||
|
||||
{editTarget.kind === "profile" && existingProfile &&
|
||||
<ProfileTransportSummary profile={existingProfile} />
|
||||
}
|
||||
|
||||
{policyMessages.length > 0 &&
|
||||
<DismissibleAlert tone="warning" resetKey={policyMessages.map((item) => `${item.key}:${item.value}`).join("|")} dismissible={false}>
|
||||
@@ -910,6 +941,7 @@ function ProfileForm({
|
||||
</DismissibleAlert>
|
||||
}
|
||||
|
||||
{showSettingsPanel && settingsPanelMode &&
|
||||
<MailServerSettingsPanel
|
||||
smtp={{ host: draft.smtpHost, port: draft.smtpPort, security: draft.smtpSecurity, timeout_seconds: draft.smtpTimeout }}
|
||||
imap={{ host: draft.imapHost, port: draft.imapPort, security: draft.imapSecurity, sent_folder: draft.imapSentFolder, timeout_seconds: draft.imapTimeout }}
|
||||
@@ -936,7 +968,11 @@ function ProfileForm({
|
||||
imapTestResult={imapTestResult}
|
||||
folderLookupResult={folderResult}
|
||||
onUseDetectedFolder={useDetectedSentFolder}
|
||||
useDetectedFolderDisabled={disabled || !draftHasImap} />
|
||||
useDetectedFolderDisabled={disabled || !draftHasImap}
|
||||
initialSection={initialSection}
|
||||
visibleSections={visibleSections}
|
||||
mode={settingsPanelMode} />
|
||||
}
|
||||
|
||||
</div>);
|
||||
|
||||
@@ -992,6 +1028,33 @@ function MailCredentialTreeCell({ profile, protocol }: {profile: MailServerProfi
|
||||
|
||||
}
|
||||
|
||||
function ProfileTransportSummary({ profile }: {profile: MailServerProfile;}) {
|
||||
return (
|
||||
<div className="mail-profile-transport-summary">
|
||||
<div>
|
||||
<span>SMTP server</span>
|
||||
<strong>{transportLabel(profile.smtp)}</strong>
|
||||
<small>{mailCredentialConfigured(profile, "smtp") ? "i18n:govoplan-mail.password_saved.f6fab237" : "i18n:govoplan-mail.no_saved_password.32ce2b16"}</small>
|
||||
</div>
|
||||
<div>
|
||||
<span>IMAP server</span>
|
||||
<strong>{transportLabel(profile.imap)}</strong>
|
||||
<small>{profile.imap ? mailCredentialConfigured(profile, "imap") ? "i18n:govoplan-mail.password_saved.f6fab237" : "i18n:govoplan-mail.no_saved_password.32ce2b16" : "i18n:govoplan-mail.not_configured.811931bb"}</small>
|
||||
</div>
|
||||
</div>);
|
||||
|
||||
}
|
||||
|
||||
function MailServerTreeCell({ profile, protocol }: {profile: MailServerProfile;protocol: "smtp" | "imap";}) {
|
||||
const transport = protocol === "smtp" ? profile.smtp : profile.imap;
|
||||
return (
|
||||
<div className="connection-tree-main">
|
||||
<strong>{protocol.toUpperCase()} server</strong>
|
||||
<div className="connection-tree-muted-line">{transportLabel(transport)}</div>
|
||||
</div>);
|
||||
|
||||
}
|
||||
|
||||
function TransportCell({ profile, protocol }: {profile: MailServerProfile;protocol: "smtp" | "imap";}) {
|
||||
const transport = protocol === "smtp" ? profile.smtp : profile.imap;
|
||||
if (!transport) return <span className="muted">i18n:govoplan-mail.not_configured.811931bb</span>;
|
||||
@@ -999,6 +1062,11 @@ function TransportCell({ profile, protocol }: {profile: MailServerProfile;protoc
|
||||
return <div><strong>{transportLabel(transport)}</strong><div className="muted small-note">i18n:govoplan-mail.password.8be3c943 {hasPassword ? "configured" : "i18n:govoplan-mail.not_configured.67f2141f"}</div></div>;
|
||||
}
|
||||
|
||||
function transportConfigured(profile: MailServerProfile, protocol: "smtp" | "imap"): boolean {
|
||||
const transport = protocol === "smtp" ? profile.smtp : profile.imap;
|
||||
return Boolean(transport?.host);
|
||||
}
|
||||
|
||||
function mailCredentialConfigured(profile: MailServerProfile, protocol: "smtp" | "imap"): boolean {
|
||||
return protocol === "smtp" ? profile.smtp_password_configured : profile.imap_password_configured;
|
||||
}
|
||||
@@ -1007,6 +1075,13 @@ function mailCredentialPolicySummary(policy: MailProfilePolicy | null, protocol:
|
||||
return credentialInheritanceLabel(protocol === "smtp" ? policy?.smtp_credentials : policy?.imap_credentials);
|
||||
}
|
||||
|
||||
function profileDialogTitle(editing: EditingProfile, target: MailProfileEditTarget): string {
|
||||
if (editing === "new") return "i18n:govoplan-mail.create_mail_profile.4d2f8f9f";
|
||||
if (target.kind === "server") return `Edit ${target.protocol.toUpperCase()} server`;
|
||||
if (target.kind === "credentials") return i18nMessage("i18n:govoplan-mail.edit_value_credentials.1e2dc4f6", { value0: target.protocol.toUpperCase() });
|
||||
return "i18n:govoplan-mail.edit_mail_profile.95d1af9c";
|
||||
}
|
||||
|
||||
function emptyProfileDraft(): ProfileDraft {
|
||||
return {
|
||||
name: "",
|
||||
@@ -1112,7 +1187,7 @@ function rawImapPayload(draft: ProfileDraft, preserveBlankPassword: boolean): Ma
|
||||
}
|
||||
|
||||
function hasDraftImapSettings(draft: ProfileDraft): boolean {
|
||||
return hasMailImapSettings([draft.imapHost, draft.imapUsername, draft.imapPassword]);
|
||||
return hasMailImapSettings([draft.imapHost]);
|
||||
}
|
||||
|
||||
function normalizePolicy(value: MailProfilePolicy | null | undefined): MailProfilePolicy {
|
||||
|
||||
Reference in New Issue
Block a user