Refine reusable mail profile credential editing

This commit is contained in:
2026-07-31 02:48:56 +02:00
parent 0cb6719b91
commit 2721ee6542
5 changed files with 246 additions and 117 deletions
+3 -104
View File
@@ -13,9 +13,9 @@
"peerDependencies": { "peerDependencies": {
"@govoplan/core-webui": "^0.1.10", "@govoplan/core-webui": "^0.1.10",
"lucide-react": "^1.23.0", "lucide-react": "^1.23.0",
"react": "^19.0.0", "react": ">=19.2.7 <20",
"react-dom": "^19.0.0", "react-dom": ">=19.2.7 <20",
"react-router-dom": ">=7.18.2 <8" "react-router": ">=8.3.0 <9"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@govoplan/core-webui": { "@govoplan/core-webui": {
@@ -23,107 +23,6 @@
} }
} }
}, },
"node_modules/cookie": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/lucide-react": {
"version": "1.24.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.24.0.tgz",
"integrity": "sha512-YT6mBD8lGKkg4nM39enlm94/sfJIiW0YKUT60fBy4YK8tai31ylg1VhGNWxkpSKHo9UagfnZqwIff3HTDQwXeA==",
"license": "ISC",
"peer": true,
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react": {
"version": "19.2.7",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
"integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "19.2.7",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
"integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
"peerDependencies": {
"react": "^19.2.7"
}
},
"node_modules/react-router": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz",
"integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==",
"license": "MIT",
"peer": true,
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-router-dom": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.2.tgz",
"integrity": "sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==",
"license": "MIT",
"peer": true,
"dependencies": {
"react-router": "7.18.2"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"license": "MIT",
"peer": true
},
"node_modules/set-cookie-parser": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
"license": "MIT",
"peer": true
},
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.9.3", "version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+3 -3
View File
@@ -16,9 +16,9 @@
"peerDependencies": { "peerDependencies": {
"@govoplan/core-webui": "^0.1.10", "@govoplan/core-webui": "^0.1.10",
"lucide-react": "^1.23.0", "lucide-react": "^1.23.0",
"react": "^19.0.0", "react": ">=19.2.7 <20",
"react-dom": "^19.0.0", "react-dom": ">=19.2.7 <20",
"react-router-dom": ">=7.18.2 <8" "react-router": ">=8.3.0 <9"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@govoplan/core-webui": { "@govoplan/core-webui": {
+110 -10
View File
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useState, type ReactNode } from "react"; import { useEffect, useMemo, useState, type ReactNode } from "react";
import { AdminSelectionList, ConnectionTree, FieldLabel, LoadingFrame, MailServerSettingsPanel, PolicyLockedHint, PolicyPathHelp, PolicyRow, PolicySourcePath, PolicyTable, StatusBadge, TableActionGroup, ToggleSwitch, hasMailImapSettings, mailImapSettingsPayload, mailServerSecurityOptions, mailSmtpSettingsPayload, mailTextOrNull, mailTransportCredentialsPayload, mergeDeltaRows, normalizeMailServerSecurity, normalizePolicySourcePathItems, useDeltaWatermarks, type ConnectionTreeColumn, type MailServerConnectionTestResult, type MailServerCredentialSettings, type MailServerImapSettings, type MailServerSmtpSettings, type NormalizedPolicySourcePathItem, type PolicySourcePathItem } from "@govoplan/core-webui"; import { AdminSelectionList, ConnectionTree, FieldLabel, LoadingFrame, MailServerSettingsPanel, PolicyLockedHint, PolicyPathHelp, PolicyRow, PolicySourcePath, PolicyTable, StageRail, StatusBadge, TableActionGroup, ToggleSwitch, hasMailImapSettings, mailImapSettingsPayload, mailServerSecurityOptions, mailSmtpSettingsPayload, mailTextOrNull, mailTransportCredentialsPayload, mergeDeltaRows, normalizeMailServerSecurity, normalizePolicySourcePathItems, useDeltaWatermarks, type ConnectionTreeColumn, type MailServerConnectionTestResult, type MailServerCredentialSettings, type MailServerImapSettings, type MailServerSmtpSettings, type NormalizedPolicySourcePathItem, type PolicySourcePathItem } from "@govoplan/core-webui";
import { Link2, Pencil, Plus, Trash2, Unlink } from "lucide-react"; import { ArrowLeft, ArrowRight, Inbox, KeyRound, Link2, Pencil, Plus, Send, Settings2, Trash2, Unlink } from "lucide-react";
import type { ApiSettings } from "../../types"; import type { ApiSettings } from "../../types";
import { import {
bindMailServerCredential, bindMailServerCredential,
@@ -60,7 +60,11 @@ import {
mailProfileEditTargetShowsProfileFields, mailProfileEditTargetShowsProfileFields,
mailProfileEditTargetShowsSettingsPanel, mailProfileEditTargetShowsSettingsPanel,
mailProfileEditTargetVisibleSections, mailProfileEditTargetVisibleSections,
mailProfileCreateStageCanContinue,
mailProfileCreateStagePanel,
mailProfileCreateStages,
mailProfileCreateCredentialsPayload, mailProfileCreateCredentialsPayload,
type MailProfileCreateStage,
type MailProfileEditTarget, type MailProfileEditTarget,
type MailProfileProtocol type MailProfileProtocol
} from "./mailProfileEditorModel"; } from "./mailProfileEditorModel";
@@ -179,6 +183,7 @@ export function MailProfileScopeManager({
const [selectedTargetId, setSelectedTargetId] = useState(scopeId || targetOptions[0]?.id || ""); const [selectedTargetId, setSelectedTargetId] = useState(scopeId || targetOptions[0]?.id || "");
const [editing, setEditing] = useState<EditingProfile>(null); const [editing, setEditing] = useState<EditingProfile>(null);
const [editingTarget, setEditingTarget] = useState<MailProfileEditTarget>({ kind: "create" }); const [editingTarget, setEditingTarget] = useState<MailProfileEditTarget>({ kind: "create" });
const [createStage, setCreateStage] = useState<MailProfileCreateStage>("profile");
const [deactivating, setDeactivating] = useState<MailServerProfile | null>(null); const [deactivating, setDeactivating] = useState<MailServerProfile | null>(null);
const [pendingHierarchyRemoval, setPendingHierarchyRemoval] = useState<PendingHierarchyRemoval>(null); const [pendingHierarchyRemoval, setPendingHierarchyRemoval] = useState<PendingHierarchyRemoval>(null);
const [availableCredentials, setAvailableCredentials] = useState<MailCredentialEnvelope[]>([]); const [availableCredentials, setAvailableCredentials] = useState<MailCredentialEnvelope[]>([]);
@@ -204,6 +209,11 @@ export function MailProfileScopeManager({
profileDraftKey(draft) !== savedProfileDraftKey profileDraftKey(draft) !== savedProfileDraftKey
|| reuseCredentialId !== savedReuseCredentialId || reuseCredentialId !== savedReuseCredentialId
); );
const createStageIndex = mailProfileCreateStages.indexOf(createStage);
const createStageCanContinue = mailProfileCreateStageCanContinue(
createStage,
draft
);
useUnsavedDraftGuard({ useUnsavedDraftGuard({
dirty: profileDirty, dirty: profileDirty,
@@ -287,6 +297,7 @@ export function MailProfileScopeManager({
setAvailableCredentials([]); setAvailableCredentials([]);
setReuseCredentialId(""); setReuseCredentialId("");
setSavedReuseCredentialId(""); setSavedReuseCredentialId("");
setCreateStage("profile");
setEditingTarget({ kind: "create" }); setEditingTarget({ kind: "create" });
setEditing("new"); setEditing("new");
setError(""); setError("");
@@ -338,9 +349,15 @@ export function MailProfileScopeManager({
setAvailableCredentials([]); setAvailableCredentials([]);
setReuseCredentialId(""); setReuseCredentialId("");
setSavedReuseCredentialId(""); setSavedReuseCredentialId("");
setCreateStage("profile");
setEditingTarget({ kind: "create" }); setEditingTarget({ kind: "create" });
} }
function moveCreateStage(offset: number) {
const next = mailProfileCreateStages[createStageIndex + offset];
if (next) setCreateStage(next);
}
async function saveProfile(): Promise<boolean> { async function saveProfile(): Promise<boolean> {
if (!editing || !scopeReady) return false; if (!editing || !scopeReady) return false;
setBusy(true); setBusy(true);
@@ -662,8 +679,52 @@ export function MailProfileScopeManager({
onClose={() => !busy && closeProfileEditor()} onClose={() => !busy && closeProfileEditor()}
className="admin-dialog admin-dialog-wide mail-profile-dialog" className="admin-dialog admin-dialog-wide mail-profile-dialog"
closeDisabled={busy} closeDisabled={busy}
footer={<><Button onClick={closeProfileEditor} disabled={busy}>i18n:govoplan-mail.cancel.77dfd213</Button><Button variant="primary" onClick={() => void saveProfile()} disabled={!canWriteProfiles || busy || !scopeReady || !profileEditorCanSave(draft, editing, editingTarget, reuseCredentialId)}>{busy ? "i18n:govoplan-mail.saving.56a2285c" : "i18n:govoplan-mail.save_profile.f597c0e8"}</Button></>}> footer={editing === "new"
? <>
<Button onClick={closeProfileEditor} disabled={busy}>i18n:govoplan-mail.cancel.77dfd213</Button>
{createStageIndex > 0 && (
<Button onClick={() => moveCreateStage(-1)} disabled={busy}>
<ArrowLeft size={16} />
Back
</Button>
)}
{createStageIndex < mailProfileCreateStages.length - 1
? (
<Button
variant="primary"
onClick={() => moveCreateStage(1)}
disabled={!canWriteProfiles || busy || !createStageCanContinue}
>
Next
<ArrowRight size={16} />
</Button>
)
: (
<Button
variant="primary"
onClick={() => void saveProfile()}
disabled={!canWriteProfiles || busy || !scopeReady || !profileEditorCanSave(draft, editing, editingTarget, reuseCredentialId)}
>
{busy ? "i18n:govoplan-mail.saving.56a2285c" : "Create profile"}
</Button>
)}
</>
: <><Button onClick={closeProfileEditor} disabled={busy}>i18n:govoplan-mail.cancel.77dfd213</Button><Button variant="primary" onClick={() => void saveProfile()} disabled={!canWriteProfiles || busy || !scopeReady || !profileEditorCanSave(draft, editing, editingTarget, reuseCredentialId)}>{busy ? "i18n:govoplan-mail.saving.56a2285c" : "i18n:govoplan-mail.save_profile.f597c0e8"}</Button></>}>
{editing === "new" && (
<StageRail
ariaLabel="Mail profile creation"
className="mail-profile-create-rail"
items={mailProfileCreateStages.map((stage, index) => ({
id: stage,
label: mailProfileCreateStageLabel(stage),
icon: mailProfileCreateStageIcon(stage),
current: stage === createStage,
tone: index < createStageIndex ? "success" : stage === createStage ? "active" : "neutral",
connectorTone: index < createStageIndex ? "success" : "neutral"
}))}
/>
)}
<ProfileForm <ProfileForm
settings={settings} settings={settings}
profiles={profiles} profiles={profiles}
@@ -675,6 +736,7 @@ export function MailProfileScopeManager({
canManageCredentials={canManageCredentials} canManageCredentials={canManageCredentials}
effectivePolicy={profileEffectivePolicy} effectivePolicy={profileEffectivePolicy}
editTarget={editingTarget} editTarget={editingTarget}
createStage={editing === "new" ? createStage : null}
availableCredentials={availableCredentials} availableCredentials={availableCredentials}
reuseCredentialId={reuseCredentialId} reuseCredentialId={reuseCredentialId}
setReuseCredentialId={setReuseCredentialId} /> setReuseCredentialId={setReuseCredentialId} />
@@ -1000,6 +1062,7 @@ function ProfileForm({
canManageCredentials, canManageCredentials,
effectivePolicy, effectivePolicy,
editTarget, editTarget,
createStage,
availableCredentials, availableCredentials,
reuseCredentialId, reuseCredentialId,
setReuseCredentialId setReuseCredentialId
@@ -1014,6 +1077,7 @@ function ProfileForm({
canManageCredentials: boolean; canManageCredentials: boolean;
effectivePolicy?: MailProfilePolicy | null; effectivePolicy?: MailProfilePolicy | null;
editTarget: MailProfileEditTarget; editTarget: MailProfileEditTarget;
createStage: MailProfileCreateStage | null;
availableCredentials: MailCredentialEnvelope[]; availableCredentials: MailCredentialEnvelope[];
reuseCredentialId: string; reuseCredentialId: string;
setReuseCredentialId: (credentialId: string) => void; setReuseCredentialId: (credentialId: string) => void;
@@ -1027,11 +1091,19 @@ function ProfileForm({
const disabled = busy || !canWrite; const disabled = busy || !canWrite;
const credentialDisabled = disabled || !canManageCredentials; const credentialDisabled = disabled || !canManageCredentials;
const existingProfile = editing !== "new" ? editing : null; const existingProfile = editing !== "new" ? editing : null;
const initialSection = mailProfileEditTargetInitialSection(editTarget); const createPanel = editTarget.kind === "create" && createStage
const settingsPanelMode = mailProfileEditTargetPanelMode(editTarget); ? mailProfileCreateStagePanel(createStage)
const visibleSections = mailProfileEditTargetVisibleSections(editTarget); : null;
const showProfileFields = mailProfileEditTargetShowsProfileFields(editTarget); const initialSection = createPanel?.initialSection
const showSettingsPanel = mailProfileEditTargetShowsSettingsPanel(editTarget); ?? mailProfileEditTargetInitialSection(editTarget);
const settingsPanelMode = createPanel?.panelMode
?? mailProfileEditTargetPanelMode(editTarget);
const visibleSections = createPanel?.visibleSections
?? mailProfileEditTargetVisibleSections(editTarget);
const showProfileFields = createPanel?.showProfileFields
?? mailProfileEditTargetShowsProfileFields(editTarget);
const showSettingsPanel = createPanel?.showSettingsPanel
?? mailProfileEditTargetShowsSettingsPanel(editTarget);
const draftHasImap = hasDraftImapSettings(draft); const draftHasImap = hasDraftImapSettings(draft);
const selectedServerId = editTarget.kind === "server" || editTarget.kind === "credentials" ? editTarget.serverId : undefined; const selectedServerId = editTarget.kind === "server" || editTarget.kind === "credentials" ? editTarget.serverId : undefined;
const selectedCredentialId = editTarget.kind === "credentials" ? editTarget.credentialId : undefined; const selectedCredentialId = editTarget.kind === "credentials" ? editTarget.credentialId : undefined;
@@ -1289,7 +1361,11 @@ function ProfileForm({
</div> </div>
} }
{policyMessages.length > 0 && {policyMessages.length > 0 && (
editTarget.kind !== "create"
|| createStage === "smtp_server"
|| createStage === "imap_server"
) &&
<DismissibleAlert tone="warning" resetKey={policyMessages.map((item) => `${item.key}:${item.value}`).join("|")} dismissible={false}> <DismissibleAlert tone="warning" resetKey={policyMessages.map((item) => `${item.key}:${item.value}`).join("|")} dismissible={false}>
<strong>i18n:govoplan-mail.effective_mail_policy_blocks_the_current_profile.1b555820</strong> <strong>i18n:govoplan-mail.effective_mail_policy_blocks_the_current_profile.1b555820</strong>
<ul>{policyMessages.map((item) => <li key={`${item.key}:${item.value}`}>{item.message}</li>)}</ul> <ul>{policyMessages.map((item) => <li key={`${item.key}:${item.value}`}>{item.message}</li>)}</ul>
@@ -1310,7 +1386,14 @@ function ProfileForm({
smtpCredentialDisabled={credentialDisabled} smtpCredentialDisabled={credentialDisabled}
smtpPasswordSaved={Boolean(selectedCredential?.secret_configured || existingProfile?.smtp_password_configured)} smtpPasswordSaved={Boolean(selectedCredential?.secret_configured || existingProfile?.smtp_password_configured)}
imapServerDisabled={disabled} imapServerDisabled={disabled}
imapCredentialDisabled={credentialDisabled} imapCredentialDisabled={
credentialDisabled
|| (
editTarget.kind === "create"
&& createStage === "imap_credentials"
&& !draftHasImap
)
}
imapPasswordSaved={Boolean(selectedCredential?.secret_configured || existingProfile?.imap_password_configured)} imapPasswordSaved={Boolean(selectedCredential?.secret_configured || existingProfile?.imap_password_configured)}
imapActionDisabled={disabled || !draftHasImap} imapActionDisabled={disabled || !draftHasImap}
smtpTestLabel={useSavedSmtpTest ? "i18n:govoplan-mail.test_saved_smtp.008d8054" : "i18n:govoplan-mail.test_smtp.e5697981"} smtpTestLabel={useSavedSmtpTest ? "i18n:govoplan-mail.test_saved_smtp.008d8054" : "i18n:govoplan-mail.test_smtp.e5697981"}
@@ -1409,6 +1492,23 @@ function credentialAvailabilityLabel(credential: MailCredentialEnvelope): string
return `${credential.inherit_to_lower_scopes ? "Inherited" : "Current scope"} · ${modules} · ${servers}`; return `${credential.inherit_to_lower_scopes ? "Inherited" : "Current scope"} · ${modules} · ${servers}`;
} }
function mailProfileCreateStageLabel(stage: MailProfileCreateStage): string {
return {
profile: "Profile",
smtp_server: "SMTP server",
smtp_credentials: "SMTP credential",
imap_server: "IMAP server",
imap_credentials: "IMAP credential"
}[stage];
}
function mailProfileCreateStageIcon(stage: MailProfileCreateStage): ReactNode {
if (stage === "profile") return <Settings2 size={17} />;
if (stage === "smtp_server") return <Send size={17} />;
if (stage === "imap_server") return <Inbox size={17} />;
return <KeyRound size={17} />;
}
function profileDialogTitle(editing: EditingProfile, target: MailProfileEditTarget): string { function profileDialogTitle(editing: EditingProfile, target: MailProfileEditTarget): string {
if (editing === "new") return "i18n:govoplan-mail.create_mail_profile.4d2f8f9f"; if (editing === "new") return "i18n:govoplan-mail.create_mail_profile.4d2f8f9f";
if (target.kind === "server") return `${target.serverId ? "Edit" : "Add"} ${target.protocol.toUpperCase()} server`; if (target.kind === "server") return `${target.serverId ? "Edit" : "Add"} ${target.protocol.toUpperCase()} server`;
@@ -1,6 +1,28 @@
export type MailProfileProtocol = "smtp" | "imap"; export type MailProfileProtocol = "smtp" | "imap";
export type MailProfileEditSection = MailProfileProtocol; export type MailProfileEditSection = MailProfileProtocol;
export type MailProfilePanelMode = "all" | "server" | "credentials"; export type MailProfilePanelMode = "all" | "server" | "credentials";
export type MailProfileCreateStage =
| "profile"
| "smtp_server"
| "smtp_credentials"
| "imap_server"
| "imap_credentials";
export const mailProfileCreateStages: readonly MailProfileCreateStage[] = [
"profile",
"smtp_server",
"smtp_credentials",
"imap_server",
"imap_credentials"
];
export type MailProfileCreateStageFields = {
name: string;
smtpHost: string;
imapHost: string;
imapUsername: string;
imapPassword: string;
};
export type MailProfileEditTarget = export type MailProfileEditTarget =
| {kind: "create";} | {kind: "create";}
@@ -75,6 +97,49 @@ export function mailProfileEditTargetShowsSettingsPanel(target: MailProfileEditT
return target.kind !== "profile"; return target.kind !== "profile";
} }
export function mailProfileCreateStagePanel(
stage: MailProfileCreateStage
): {
showProfileFields: boolean;
showSettingsPanel: boolean;
initialSection: MailProfileEditSection;
visibleSections: MailProfileEditSection[];
panelMode: MailProfilePanelMode | null;
} {
if (stage === "profile") {
return {
showProfileFields: true,
showSettingsPanel: false,
initialSection: "smtp",
visibleSections: [],
panelMode: null
};
}
const protocol = stage.startsWith("smtp") ? "smtp" : "imap";
return {
showProfileFields: false,
showSettingsPanel: true,
initialSection: protocol,
visibleSections: [protocol],
panelMode: stage.endsWith("credentials") ? "credentials" : "server"
};
}
export function mailProfileCreateStageCanContinue(
stage: MailProfileCreateStage,
fields: MailProfileCreateStageFields
): boolean {
if (stage === "profile") return Boolean(fields.name.trim());
if (stage === "smtp_server") return Boolean(fields.smtpHost.trim());
if (
stage === "imap_credentials"
&& (fields.imapUsername.trim() || fields.imapPassword)
) {
return Boolean(fields.imapHost.trim());
}
return true;
}
/** /**
* Keep the PATCH authority surface equal to the focused editor. Profile and * Keep the PATCH authority surface equal to the focused editor. Profile and
* server edits must not accidentally replay credential fields merely because * server edits must not accidentally replay credential fields merely because
@@ -19,6 +19,9 @@ import {
mailProfileEditTargetShowsProfileFields, mailProfileEditTargetShowsProfileFields,
mailProfileEditTargetShowsSettingsPanel, mailProfileEditTargetShowsSettingsPanel,
mailProfileEditTargetVisibleSections, mailProfileEditTargetVisibleSections,
mailProfileCreateStageCanContinue,
mailProfileCreateStagePanel,
mailProfileCreateStages,
mailProfileCreateCredentialsPayload, mailProfileCreateCredentialsPayload,
mailProfileTargetedUpdatePayload mailProfileTargetedUpdatePayload
} from "../src/features/mail/mailProfileEditorModel"; } from "../src/features/mail/mailProfileEditorModel";
@@ -48,6 +51,68 @@ assertEqual(mailProfileEditTargetShowsProfileFields({ kind: "profile" }), true);
assertEqual(mailProfileEditTargetShowsProfileFields({ kind: "server", protocol: "smtp" }), false); assertEqual(mailProfileEditTargetShowsProfileFields({ kind: "server", protocol: "smtp" }), false);
assertEqual(mailProfileEditTargetShowsSettingsPanel({ kind: "profile" }), false); assertEqual(mailProfileEditTargetShowsSettingsPanel({ kind: "profile" }), false);
assertEqual(mailProfileEditTargetShowsSettingsPanel({ kind: "create" }), true); assertEqual(mailProfileEditTargetShowsSettingsPanel({ kind: "create" }), true);
assertDeepEqual(
mailProfileCreateStages,
[
"profile",
"smtp_server",
"smtp_credentials",
"imap_server",
"imap_credentials"
],
"profile creation keeps server and credential stages explicit"
);
assertDeepEqual(
mailProfileCreateStagePanel("smtp_credentials"),
{
showProfileFields: false,
showSettingsPanel: true,
initialSection: "smtp",
visibleSections: ["smtp"],
panelMode: "credentials"
}
);
assertDeepEqual(
mailProfileCreateStagePanel("imap_server"),
{
showProfileFields: false,
showSettingsPanel: true,
initialSection: "imap",
visibleSections: ["imap"],
panelMode: "server"
}
);
assertEqual(
mailProfileCreateStageCanContinue("profile", {
name: "",
smtpHost: "",
imapHost: "",
imapUsername: "",
imapPassword: ""
}),
false
);
assertEqual(
mailProfileCreateStageCanContinue("smtp_server", {
name: "Profile",
smtpHost: "smtp.example.org",
imapHost: "",
imapUsername: "",
imapPassword: ""
}),
true
);
assertEqual(
mailProfileCreateStageCanContinue("imap_credentials", {
name: "Profile",
smtpHost: "smtp.example.org",
imapHost: "",
imapUsername: "imap-user",
imapPassword: ""
}),
false,
"IMAP credentials cannot be submitted without an IMAP server"
);
const updateParts = { const updateParts = {
profile: { name: "Renamed" }, profile: { name: "Renamed" },