chore: consolidate platform split checks
Dependency Audit / dependency-audit (push) Has been cancelled
Module Matrix / module-matrix (push) Has been cancelled

This commit is contained in:
2026-07-10 12:51:19 +02:00
parent 150b720f12
commit 635d25c74c
216 changed files with 23336 additions and 4077 deletions
@@ -3,6 +3,7 @@ import Button from "../Button";
import DismissibleAlert from "../DismissibleAlert";
import FormField from "../FormField";
import PasswordField from "../PasswordField";
import SegmentedControl from "../SegmentedControl";
import ToggleSwitch from "../ToggleSwitch";
export type MailServerSecurity = "plain" | "tls" | "starttls" | string;
@@ -43,7 +44,7 @@ export type MailServerFolderLookupResult = {
security?: MailServerSecurity | null;
message: string;
detected_sent_folder?: string | null;
folders?: { name: string; flags?: string[] }[];
folders?: {name: string;flags?: string[];}[];
details?: Record<string, unknown> | null;
};
@@ -133,19 +134,19 @@ export function mailNumberOrDefault(value: string | number | null | undefined, f
}
export function normalizeMailServerSecurity<TSecurity extends string = MailServerSecurityOption>(
value: string | null | undefined,
options: { fallback: TSecurity; allowedSecurity?: readonly TSecurity[] },
): TSecurity {
const allowed = options.allowedSecurity ?? (mailServerSecurityOptions as unknown as readonly TSecurity[]);
return allowed.includes(value as TSecurity) ? (value as TSecurity) : options.fallback;
value: string | null | undefined,
options: {fallback: TSecurity;allowedSecurity?: readonly TSecurity[];})
: TSecurity {
const allowed = options.allowedSecurity ?? mailServerSecurityOptions as unknown as readonly TSecurity[];
return allowed.includes(value as TSecurity) ? value as TSecurity : options.fallback;
}
export function mailTransportCredentialsPayload(
username: string | number | null | undefined,
password: string | number | null | undefined,
preserveBlankPassword: boolean,
): { username?: string | null; password?: string | null } {
const payload: { username?: string | null; password?: string | null } = { username: mailTextOrNull(username) };
username: string | number | null | undefined,
password: string | number | null | undefined,
preserveBlankPassword: boolean)
: {username?: string | null;password?: string | null;} {
const payload: {username?: string | null;password?: string | null;} = { username: mailTextOrNull(username) };
if (String(password ?? "") || !preserveBlankPassword) payload.password = mailTextOrNull(password, false);
return payload;
}
@@ -157,39 +158,39 @@ function mailRecordText(record: Record<string, unknown>, key: string, fallback =
}
export function mailTransportCredentialsPayloadFromRecords(
primary: Record<string, unknown>,
legacy: Record<string, unknown>,
preserveBlankPassword: boolean,
): { username?: string | null; password?: string | null } {
primary: Record<string, unknown>,
legacy: Record<string, unknown>,
preserveBlankPassword: boolean)
: {username?: string | null;password?: string | null;} {
return mailTransportCredentialsPayload(
mailRecordText(primary, "username", mailRecordText(legacy, "username")),
mailRecordText(primary, "password", mailRecordText(legacy, "password")),
preserveBlankPassword,
preserveBlankPassword
);
}
export function mailSmtpSettingsPayload<TSecurity extends string = MailServerSecurityOption>(
settings: MailServerSmtpSettings,
options: { fallbackSecurity: TSecurity; allowedSecurity?: readonly TSecurity[]; fallbackTimeoutSeconds?: number },
): { host: string | null; port: number | null; security: TSecurity; timeout_seconds: number } {
settings: MailServerSmtpSettings,
options: {fallbackSecurity: TSecurity;allowedSecurity?: readonly TSecurity[];fallbackTimeoutSeconds?: number;})
: {host: string | null;port: number | null;security: TSecurity;timeout_seconds: number;} {
return {
host: mailTextOrNull(settings.host),
port: mailNumberOrNull(settings.port),
security: normalizeMailServerSecurity(settings.security ? String(settings.security) : null, { fallback: options.fallbackSecurity, allowedSecurity: options.allowedSecurity }),
timeout_seconds: mailNumberOrDefault(settings.timeout_seconds, options.fallbackTimeoutSeconds ?? 30),
timeout_seconds: mailNumberOrDefault(settings.timeout_seconds, options.fallbackTimeoutSeconds ?? 30)
};
}
export function mailImapSettingsPayload<TSecurity extends string = MailServerSecurityOption>(
settings: MailServerImapSettings,
options: { fallbackSecurity: TSecurity; allowedSecurity?: readonly TSecurity[]; fallbackTimeoutSeconds?: number },
): { host: string | null; port: number | null; security: TSecurity; sent_folder: string; timeout_seconds: number } {
settings: MailServerImapSettings,
options: {fallbackSecurity: TSecurity;allowedSecurity?: readonly TSecurity[];fallbackTimeoutSeconds?: number;})
: {host: string | null;port: number | null;security: TSecurity;sent_folder: string;timeout_seconds: number;} {
return {
host: mailTextOrNull(settings.host),
port: mailNumberOrNull(settings.port),
security: normalizeMailServerSecurity(settings.security ? String(settings.security) : null, { fallback: options.fallbackSecurity, allowedSecurity: options.allowedSecurity }),
sent_folder: mailTextOrNull(settings.sent_folder) || "auto",
timeout_seconds: mailNumberOrDefault(settings.timeout_seconds, options.fallbackTimeoutSeconds ?? 30),
timeout_seconds: mailNumberOrDefault(settings.timeout_seconds, options.fallbackTimeoutSeconds ?? 30)
};
}
@@ -216,9 +217,9 @@ export default function MailServerSettingsPanel({
imapPasswordSaved = false,
imapSavedPasswordPlaceholder = "••••••••",
imapActionDisabled = imapServerDisabled,
smtpTestLabel = "Test SMTP",
imapTestLabel = "Test IMAP",
folderLookupLabel = "Folders...",
smtpTestLabel = "i18n:govoplan-core.test_smtp.e5697981",
imapTestLabel = "i18n:govoplan-core.test_imap.ef1bd79c",
folderLookupLabel = "i18n:govoplan-core.folders.c603ab65",
busyAction = null,
onTestSmtp,
onTestImap,
@@ -250,15 +251,15 @@ export default function MailServerSettingsPanel({
const appendTargetFolder = append ? append.folder : stringValue(imap.sent_folder, "auto");
const appendTargetDisabled = append ? disabled || append.folderDisabled : imapFieldsDisabled;
const canLookupAppendFolders = Boolean(onLookupFolders) && !appendTargetDisabled;
const appendTargetHelp = append
? "Folder for sent-message copies. Leave as auto unless this campaign needs a different target."
: "Folder used when this IMAP account is used for sent-message copies. Leave as auto to use the server default.";
const appendTargetHelp = append ?
"i18n:govoplan-core.folder_for_sent_message_copies_leave_as_auto_unl.a62586e9" :
"i18n:govoplan-core.folder_used_when_this_imap_account_is_used_for_s.08503f5e";
const [activeSection, setActiveSection] = useState<MailServerSettingsSection>(initialSection);
const sections: { id: MailServerSettingsSection; label: string }[] = [
{ id: "smtp", label: "SMTP" },
{ id: "imap", label: "IMAP" },
{ id: "advanced", label: "Advanced" }
];
const sections: {id: MailServerSettingsSection;label: string;}[] = [
{ id: "smtp", label: "i18n:govoplan-core.smtp.efff9cca" },
{ id: "imap", label: "i18n:govoplan-core.imap.271f9ef2" },
{ id: "advanced", label: "i18n:govoplan-core.advanced.4d064726" }];
function patchSmtpSecurity(security: MailServerSecurity) {
const port = portForSecurityChange("smtp", smtp.port, smtpSecurity, security);
@@ -271,153 +272,147 @@ export default function MailServerSettingsPanel({
}
function patchSmtpCredentials(patch: Partial<MailServerCredentialSettings>) {
if (onSmtpCredentialsChange) onSmtpCredentialsChange(patch);
else onSmtpChange(patch);
if (onSmtpCredentialsChange) onSmtpCredentialsChange(patch);else
onSmtpChange(patch);
}
function patchImapCredentials(patch: Partial<MailServerCredentialSettings>) {
if (onImapCredentialsChange) onImapCredentialsChange(patch);
else onImapChange(patch);
if (onImapCredentialsChange) onImapCredentialsChange(patch);else
onImapChange(patch);
}
return (
<div className={`mail-server-settings-panel ${className}`.trim()}>
<div className="mail-server-segmented-control" role="tablist" aria-label="Mail server settings sections">
{sections.map((section) => (
<button
type="button"
key={section.id}
role="tab"
aria-selected={activeSection === section.id}
className={`mail-server-segmented-tab${activeSection === section.id ? " is-active" : ""}`}
onClick={() => setActiveSection(section.id)}
>
{section.label}
</button>
))}
</div>
<SegmentedControl
className="mail-server-segmented-control"
size="equal"
ariaLabel="i18n:govoplan-core.mail_server_settings_sections.40a163b7"
value={activeSection}
onChange={setActiveSection}
options={sections}
/>
<div className="mail-server-settings-view">
{activeSection === "smtp" && (
<section className="mail-server-subsection" role="tabpanel" aria-label="SMTP settings">
{activeSection === "smtp" &&
<section className="mail-server-subsection" role="tabpanel" aria-label="i18n:govoplan-core.smtp_settings.f103e570">
<div className="form-grid compact responsive-form-grid mail-server-form-grid">
<div className="mail-server-field-heading">Server</div>
<FormField label="Host"><input value={stringValue(smtp.host)} disabled={smtpFieldsDisabled} onChange={(event) => onSmtpChange({ host: event.target.value })} /></FormField>
<FormField label="Port"><input type="number" min={1} max={65535} value={smtpPort} disabled={smtpFieldsDisabled} onChange={(event) => onSmtpChange({ port: event.target.value })} /></FormField>
<FormField label="Security"><SecuritySelect value={smtpSecurity} disabled={smtpFieldsDisabled} onChange={patchSmtpSecurity} /></FormField>
<div className="mail-server-field-heading">Credentials</div>
<FormField label="Username"><input value={stringValue(smtpCredentialValues.username)} disabled={smtpCredentialFieldsDisabled} onChange={(event) => patchSmtpCredentials({ username: event.target.value })} /></FormField>
<FormField label="Password">
<div className="mail-server-field-heading">i18n:govoplan-core.server.cb0cb170</div>
<FormField label="i18n:govoplan-core.host.3960ec4c"><input value={stringValue(smtp.host)} disabled={smtpFieldsDisabled} onChange={(event) => onSmtpChange({ host: event.target.value })} /></FormField>
<FormField label="i18n:govoplan-core.port.fe035157"><input type="number" min={1} max={65535} value={smtpPort} disabled={smtpFieldsDisabled} onChange={(event) => onSmtpChange({ port: event.target.value })} /></FormField>
<FormField label="i18n:govoplan-core.security.f25ce1b8"><SecuritySelect value={smtpSecurity} disabled={smtpFieldsDisabled} onChange={patchSmtpSecurity} /></FormField>
<div className="mail-server-field-heading">i18n:govoplan-core.credentials.dd097a22</div>
<FormField label="i18n:govoplan-core.username.84c29015"><input value={stringValue(smtpCredentialValues.username)} disabled={smtpCredentialFieldsDisabled} onChange={(event) => patchSmtpCredentials({ username: event.target.value })} /></FormField>
<FormField label="i18n:govoplan-core.password.8be3c943">
<PasswordField
value={stringValue(smtpCredentialValues.password)}
disabled={smtpCredentialFieldsDisabled}
saved={smtpPasswordSaved}
savedPlaceholder={smtpSavedPasswordPlaceholder}
autoComplete="new-password"
onValueChange={(password) => patchSmtpCredentials({ password })}
/>
value={stringValue(smtpCredentialValues.password)}
disabled={smtpCredentialFieldsDisabled}
saved={smtpPasswordSaved}
savedPlaceholder={smtpSavedPasswordPlaceholder}
autoComplete="new-password"
onValueChange={(password) => patchSmtpCredentials({ password })} />
</FormField>
</div>
{onTestSmtp && (
<div className="button-row compact-actions mail-server-actions">
<Button type="button" variant="primary" onClick={onTestSmtp} disabled={smtpActionsDisabled || busyAction === "smtp"}>{busyAction === "smtp" ? "Testing..." : smtpTestLabel}</Button>
{onTestSmtp &&
<div className="button-row compact-actions mail-server-actions">
<Button type="button" variant="primary" onClick={onTestSmtp} disabled={smtpActionsDisabled || busyAction === "smtp"}>{busyAction === "smtp" ? "i18n:govoplan-core.testing.15ccc832" : smtpTestLabel}</Button>
</div>
)}
}
<MailServerActionResult result={smtpTestResult} floating={floatingResults} />
</section>
)}
}
{activeSection === "imap" && (
<section className="mail-server-subsection" role="tabpanel" aria-label="IMAP settings">
{activeSection === "imap" &&
<section className="mail-server-subsection" role="tabpanel" aria-label="i18n:govoplan-core.imap_settings.ab8d8247">
<div className="form-grid compact responsive-form-grid mail-server-form-grid">
<div className="mail-server-field-heading">Server</div>
<FormField label="Host"><input value={stringValue(imap.host)} disabled={imapFieldsDisabled} onChange={(event) => onImapChange({ host: event.target.value })} /></FormField>
<FormField label="Port"><input type="number" min={1} max={65535} value={imapPort} disabled={imapFieldsDisabled} onChange={(event) => onImapChange({ port: event.target.value })} /></FormField>
<FormField label="Security"><SecuritySelect value={imapSecurity} disabled={imapFieldsDisabled} onChange={patchImapSecurity} /></FormField>
<div className="mail-server-field-heading">Credentials</div>
<FormField label="Username"><input value={stringValue(imapCredentialValues.username)} disabled={imapCredentialFieldsDisabled} onChange={(event) => patchImapCredentials({ username: event.target.value })} /></FormField>
<FormField label="Password">
<div className="mail-server-field-heading">i18n:govoplan-core.server.cb0cb170</div>
<FormField label="i18n:govoplan-core.host.3960ec4c"><input value={stringValue(imap.host)} disabled={imapFieldsDisabled} onChange={(event) => onImapChange({ host: event.target.value })} /></FormField>
<FormField label="i18n:govoplan-core.port.fe035157"><input type="number" min={1} max={65535} value={imapPort} disabled={imapFieldsDisabled} onChange={(event) => onImapChange({ port: event.target.value })} /></FormField>
<FormField label="i18n:govoplan-core.security.f25ce1b8"><SecuritySelect value={imapSecurity} disabled={imapFieldsDisabled} onChange={patchImapSecurity} /></FormField>
<div className="mail-server-field-heading">i18n:govoplan-core.credentials.dd097a22</div>
<FormField label="i18n:govoplan-core.username.84c29015"><input value={stringValue(imapCredentialValues.username)} disabled={imapCredentialFieldsDisabled} onChange={(event) => patchImapCredentials({ username: event.target.value })} /></FormField>
<FormField label="i18n:govoplan-core.password.8be3c943">
<PasswordField
value={stringValue(imapCredentialValues.password)}
disabled={imapCredentialFieldsDisabled}
saved={imapPasswordSaved}
savedPlaceholder={imapSavedPasswordPlaceholder}
autoComplete="new-password"
onValueChange={(password) => patchImapCredentials({ password })}
/>
value={stringValue(imapCredentialValues.password)}
disabled={imapCredentialFieldsDisabled}
saved={imapPasswordSaved}
savedPlaceholder={imapSavedPasswordPlaceholder}
autoComplete="new-password"
onValueChange={(password) => patchImapCredentials({ password })} />
</FormField>
</div>
{onTestImap && (
<div className="button-row compact-actions mail-server-actions">
<Button type="button" variant="primary" onClick={onTestImap} disabled={imapActionsDisabled || busyAction === "imap"}>{busyAction === "imap" ? "Testing..." : imapTestLabel}</Button>
{onTestImap &&
<div className="button-row compact-actions mail-server-actions">
<Button type="button" variant="primary" onClick={onTestImap} disabled={imapActionsDisabled || busyAction === "imap"}>{busyAction === "imap" ? "i18n:govoplan-core.testing.15ccc832" : imapTestLabel}</Button>
</div>
)}
}
<MailServerActionResult result={imapTestResult} floating={floatingResults} />
</section>
)}
}
{activeSection === "advanced" && (
<section className="mail-server-subsection" role="tabpanel" aria-label="Advanced mail settings">
{activeSection === "advanced" &&
<section className="mail-server-subsection" role="tabpanel" aria-label="i18n:govoplan-core.advanced_mail_settings.1f69439a">
<div className="form-grid compact responsive-form-grid mail-server-form-grid mail-server-advanced-grid">
{mockToggle && (
<div className="mail-server-field-span mail-server-toggle-row">
{mockToggle &&
<div className="mail-server-field-span mail-server-toggle-row">
<ToggleSwitch
label={mockToggle.label ?? "Mock server settings"}
checked={mockToggle.checked}
disabled={disabled || mockToggle.disabled}
onChange={mockToggle.onChange}
/>
label={mockToggle.label ?? "i18n:govoplan-core.mock_server_settings.9361d5c5"}
checked={mockToggle.checked}
disabled={disabled || mockToggle.disabled}
onChange={mockToggle.onChange} />
</div>
)}
<FormField label="SMTP timeout seconds"><input type="number" min={1} value={stringValue(smtp.timeout_seconds)} disabled={smtpFieldsDisabled} onChange={(event) => onSmtpChange({ timeout_seconds: event.target.value })} /></FormField>
<FormField label="IMAP timeout seconds"><input type="number" min={1} value={stringValue(imap.timeout_seconds)} disabled={imapFieldsDisabled} onChange={(event) => onImapChange({ timeout_seconds: event.target.value })} /></FormField>
{append && (
<div className="mail-server-field-span mail-server-toggle-row mail-server-plain-toggle-row">
<ToggleSwitch label="Append successfully sent messages to Sent" checked={append.enabled} disabled={disabled || append.disabled} onChange={append.onEnabledChange} />
}
<FormField label="i18n:govoplan-core.smtp_timeout_seconds.ac87c8d2"><input type="number" min={1} value={stringValue(smtp.timeout_seconds)} disabled={smtpFieldsDisabled} onChange={(event) => onSmtpChange({ timeout_seconds: event.target.value })} /></FormField>
<FormField label="i18n:govoplan-core.imap_timeout_seconds.489af2a4"><input type="number" min={1} value={stringValue(imap.timeout_seconds)} disabled={imapFieldsDisabled} onChange={(event) => onImapChange({ timeout_seconds: event.target.value })} /></FormField>
{append &&
<div className="mail-server-field-span mail-server-toggle-row mail-server-plain-toggle-row">
<ToggleSwitch label="i18n:govoplan-core.append_successfully_sent_messages_to_sent.002fd67e" checked={append.enabled} disabled={disabled || append.disabled} onChange={append.onEnabledChange} />
</div>
)}
<FormField label="Append target folder" help={appendTargetHelp}>
}
<FormField label="i18n:govoplan-core.append_target_folder.0aaacc0c" help={appendTargetHelp}>
<div className="field-with-action mail-server-folder-field">
<input
value={appendTargetFolder}
disabled={appendTargetDisabled}
onChange={(event) => append ? append.onFolderChange(event.target.value) : onImapChange({ sent_folder: event.target.value })}
placeholder="auto"
/>
{canLookupAppendFolders && <Button type="button" variant="primary" onClick={onLookupFolders} disabled={imapActionsDisabled || busyAction === "folders"}>{busyAction === "folders" ? "Looking up..." : folderLookupLabel}</Button>}
value={appendTargetFolder}
disabled={appendTargetDisabled}
onChange={(event) => append ? append.onFolderChange(event.target.value) : onImapChange({ sent_folder: event.target.value })}
placeholder="i18n:govoplan-core.auto.0d612c12" />
{canLookupAppendFolders && <Button type="button" variant="primary" onClick={onLookupFolders} disabled={imapActionsDisabled || busyAction === "folders"}>{busyAction === "folders" ? "i18n:govoplan-core.looking_up.5fc6d2a2" : folderLookupLabel}</Button>}
</div>
</FormField>
{canLookupAppendFolders && <MailServerFolderLookupResultView result={folderLookupResult} disabled={useDetectedFolderDisabled || appendTargetDisabled} onUseDetected={onUseDetectedFolder} />}
</div>
</section>
)}
}
</div>
</div>
);
</div>);
}
export function MailServerActionResult({ result, floating = false }: { result: MailServerConnectionTestResult | null | undefined; floating?: boolean }) {
export function MailServerActionResult({ result, floating = false }: {result: MailServerConnectionTestResult | null | undefined;floating?: boolean;}) {
if (!result) return null;
const authenticated = result.details?.authenticated;
return (
<DismissibleAlert tone={result.ok ? "success" : "danger"} resetKey={`${result.ok}:${result.message}`} floating={floating}>
{result.message}
{result.ok && typeof authenticated === "boolean" && (
<span> Authentication: {authenticated ? "credentials accepted" : "not used"}.</span>
)}
</DismissibleAlert>
);
{result.ok && typeof authenticated === "boolean" &&
<span> i18n:govoplan-core.authentication.e665f157 {authenticated ? "i18n:govoplan-core.credentials_accepted.ae8e2629" : "i18n:govoplan-core.not_used.487f8bcd"}.</span>
}
</DismissibleAlert>);
}
export function MailServerFolderLookupResultView({
result,
disabled = false,
onUseDetected
}: {
result: MailServerFolderLookupResult | null | undefined;
disabled?: boolean;
onUseDetected?: () => void;
}) {
}: {result: MailServerFolderLookupResult | null | undefined;disabled?: boolean;onUseDetected?: () => void;}) {
if (!result) return null;
if (!result.ok) {
return <DismissibleAlert tone="danger" resetKey={result.message}>{result.message}</DismissibleAlert>;
@@ -427,21 +422,21 @@ export function MailServerFolderLookupResultView({
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 && onUseDetected && <Button type="button" onClick={onUseDetected} disabled={disabled}>Use detected folder</Button>}
{folders.length > 0 && (
<div className="mail-server-folder-chip-list">
{folders.slice(0, 12).map((folder) => (
<span className="mail-server-folder-chip" key={folder.name} title={(folder.flags || []).join(" ")}>{folder.name}</span>
))}
<p>i18n:govoplan-core.detected_sent_folder.cbf8ec8d <strong>{result.detected_sent_folder || "-"}</strong></p>
{result.detected_sent_folder && onUseDetected && <Button type="button" onClick={onUseDetected} disabled={disabled}>i18n:govoplan-core.use_detected_folder.5ec4965c</Button>}
{folders.length > 0 &&
<div className="mail-server-folder-chip-list">
{folders.slice(0, 12).map((folder) =>
<span className="mail-server-folder-chip" key={folder.name} title={(folder.flags || []).join(" ")}>{folder.name}</span>
)}
{folders.length > 12 && <span className="mail-server-folder-chip">+{folders.length - 12} more</span>}
</div>
)}
</DismissibleAlert>
);
}
</DismissibleAlert>);
}
function SecuritySelect({ value, disabled, onChange }: { value: string; disabled: boolean; onChange: (value: MailServerSecurity) => void }) {
function SecuritySelect({ value, disabled, onChange }: {value: string;disabled: boolean;onChange: (value: MailServerSecurity) => void;}) {
return <select value={value} disabled={disabled} onChange={(event) => onChange(event.target.value)}>{securityOptions.map((option) => <option key={option} value={option}>{option}</option>)}</select>;
}