Refine reusable mail profile credential editing
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
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 { Link2, Pencil, Plus, Trash2, Unlink } from "lucide-react";
|
||||
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 { ArrowLeft, ArrowRight, Inbox, KeyRound, Link2, Pencil, Plus, Send, Settings2, Trash2, Unlink } from "lucide-react";
|
||||
import type { ApiSettings } from "../../types";
|
||||
import {
|
||||
bindMailServerCredential,
|
||||
@@ -60,7 +60,11 @@ import {
|
||||
mailProfileEditTargetShowsProfileFields,
|
||||
mailProfileEditTargetShowsSettingsPanel,
|
||||
mailProfileEditTargetVisibleSections,
|
||||
mailProfileCreateStageCanContinue,
|
||||
mailProfileCreateStagePanel,
|
||||
mailProfileCreateStages,
|
||||
mailProfileCreateCredentialsPayload,
|
||||
type MailProfileCreateStage,
|
||||
type MailProfileEditTarget,
|
||||
type MailProfileProtocol
|
||||
} from "./mailProfileEditorModel";
|
||||
@@ -179,6 +183,7 @@ export function MailProfileScopeManager({
|
||||
const [selectedTargetId, setSelectedTargetId] = useState(scopeId || targetOptions[0]?.id || "");
|
||||
const [editing, setEditing] = useState<EditingProfile>(null);
|
||||
const [editingTarget, setEditingTarget] = useState<MailProfileEditTarget>({ kind: "create" });
|
||||
const [createStage, setCreateStage] = useState<MailProfileCreateStage>("profile");
|
||||
const [deactivating, setDeactivating] = useState<MailServerProfile | null>(null);
|
||||
const [pendingHierarchyRemoval, setPendingHierarchyRemoval] = useState<PendingHierarchyRemoval>(null);
|
||||
const [availableCredentials, setAvailableCredentials] = useState<MailCredentialEnvelope[]>([]);
|
||||
@@ -204,6 +209,11 @@ export function MailProfileScopeManager({
|
||||
profileDraftKey(draft) !== savedProfileDraftKey
|
||||
|| reuseCredentialId !== savedReuseCredentialId
|
||||
);
|
||||
const createStageIndex = mailProfileCreateStages.indexOf(createStage);
|
||||
const createStageCanContinue = mailProfileCreateStageCanContinue(
|
||||
createStage,
|
||||
draft
|
||||
);
|
||||
|
||||
useUnsavedDraftGuard({
|
||||
dirty: profileDirty,
|
||||
@@ -287,6 +297,7 @@ export function MailProfileScopeManager({
|
||||
setAvailableCredentials([]);
|
||||
setReuseCredentialId("");
|
||||
setSavedReuseCredentialId("");
|
||||
setCreateStage("profile");
|
||||
setEditingTarget({ kind: "create" });
|
||||
setEditing("new");
|
||||
setError("");
|
||||
@@ -338,9 +349,15 @@ export function MailProfileScopeManager({
|
||||
setAvailableCredentials([]);
|
||||
setReuseCredentialId("");
|
||||
setSavedReuseCredentialId("");
|
||||
setCreateStage("profile");
|
||||
setEditingTarget({ kind: "create" });
|
||||
}
|
||||
|
||||
function moveCreateStage(offset: number) {
|
||||
const next = mailProfileCreateStages[createStageIndex + offset];
|
||||
if (next) setCreateStage(next);
|
||||
}
|
||||
|
||||
async function saveProfile(): Promise<boolean> {
|
||||
if (!editing || !scopeReady) return false;
|
||||
setBusy(true);
|
||||
@@ -662,8 +679,52 @@ export function MailProfileScopeManager({
|
||||
onClose={() => !busy && closeProfileEditor()}
|
||||
className="admin-dialog admin-dialog-wide mail-profile-dialog"
|
||||
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
|
||||
settings={settings}
|
||||
profiles={profiles}
|
||||
@@ -675,6 +736,7 @@ export function MailProfileScopeManager({
|
||||
canManageCredentials={canManageCredentials}
|
||||
effectivePolicy={profileEffectivePolicy}
|
||||
editTarget={editingTarget}
|
||||
createStage={editing === "new" ? createStage : null}
|
||||
availableCredentials={availableCredentials}
|
||||
reuseCredentialId={reuseCredentialId}
|
||||
setReuseCredentialId={setReuseCredentialId} />
|
||||
@@ -1000,6 +1062,7 @@ function ProfileForm({
|
||||
canManageCredentials,
|
||||
effectivePolicy,
|
||||
editTarget,
|
||||
createStage,
|
||||
availableCredentials,
|
||||
reuseCredentialId,
|
||||
setReuseCredentialId
|
||||
@@ -1014,6 +1077,7 @@ function ProfileForm({
|
||||
canManageCredentials: boolean;
|
||||
effectivePolicy?: MailProfilePolicy | null;
|
||||
editTarget: MailProfileEditTarget;
|
||||
createStage: MailProfileCreateStage | null;
|
||||
availableCredentials: MailCredentialEnvelope[];
|
||||
reuseCredentialId: string;
|
||||
setReuseCredentialId: (credentialId: string) => void;
|
||||
@@ -1027,11 +1091,19 @@ 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 createPanel = editTarget.kind === "create" && createStage
|
||||
? mailProfileCreateStagePanel(createStage)
|
||||
: null;
|
||||
const initialSection = createPanel?.initialSection
|
||||
?? 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 selectedServerId = editTarget.kind === "server" || editTarget.kind === "credentials" ? editTarget.serverId : undefined;
|
||||
const selectedCredentialId = editTarget.kind === "credentials" ? editTarget.credentialId : undefined;
|
||||
@@ -1289,7 +1361,11 @@ function ProfileForm({
|
||||
</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}>
|
||||
<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>
|
||||
@@ -1310,7 +1386,14 @@ function ProfileForm({
|
||||
smtpCredentialDisabled={credentialDisabled}
|
||||
smtpPasswordSaved={Boolean(selectedCredential?.secret_configured || existingProfile?.smtp_password_configured)}
|
||||
imapServerDisabled={disabled}
|
||||
imapCredentialDisabled={credentialDisabled}
|
||||
imapCredentialDisabled={
|
||||
credentialDisabled
|
||||
|| (
|
||||
editTarget.kind === "create"
|
||||
&& createStage === "imap_credentials"
|
||||
&& !draftHasImap
|
||||
)
|
||||
}
|
||||
imapPasswordSaved={Boolean(selectedCredential?.secret_configured || existingProfile?.imap_password_configured)}
|
||||
imapActionDisabled={disabled || !draftHasImap}
|
||||
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}`;
|
||||
}
|
||||
|
||||
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 {
|
||||
if (editing === "new") return "i18n:govoplan-mail.create_mail_profile.4d2f8f9f";
|
||||
if (target.kind === "server") return `${target.serverId ? "Edit" : "Add"} ${target.protocol.toUpperCase()} server`;
|
||||
|
||||
Reference in New Issue
Block a user