Prepare v0.1.0 release

This commit is contained in:
2026-06-24 20:00:38 +02:00
parent a9d16a2c89
commit 97015e5a01
10 changed files with 409 additions and 79 deletions

View File

@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from "react";
import { MailServerSettingsPanel, type MailServerConnectionTestResult, type MailServerFolderLookupResult, type MailServerImapSettings, type MailServerSmtpSettings } from "@govoplan/core-webui";
import { EffectivePolicyBlock, EffectivePolicyValue, MailServerSettingsPanel, PolicyLockedHint, type MailServerConnectionTestResult, type MailServerFolderLookupResult, type MailServerImapSettings, type MailServerSmtpSettings, type PolicySourcePathItem } from "@govoplan/core-webui";
import { Pencil, Plus, Trash2 } from "lucide-react";
import type { ApiSettings } from "../../types";
import {
@@ -329,6 +329,8 @@ export function MailProfilePolicyEditor({
}: MailProfilePolicyEditorProps) {
const [policy, setPolicy] = useState<MailProfilePolicy>(blankPolicy);
const [effectivePolicy, setEffectivePolicy] = useState<MailProfilePolicy | null>(null);
const [parentPolicy, setParentPolicy] = useState<MailProfilePolicy | null>(null);
const [effectivePolicySources, setEffectivePolicySources] = useState<PolicySourcePathItem[]>([]);
const [loading, setLoading] = useState(false);
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
@@ -345,6 +347,8 @@ export function MailProfilePolicyEditor({
if (!scopeReady) {
setPolicy(blankPolicy);
setEffectivePolicy(null);
setParentPolicy(null);
setEffectivePolicySources([]);
return;
}
setLoading(true);
@@ -352,9 +356,13 @@ export function MailProfilePolicyEditor({
const response = await getMailProfilePolicy(settings, scopeType, scopeId, campaignId);
setPolicy(normalizePolicy(response.policy));
setEffectivePolicy(response.effective_policy ? normalizePolicy(response.effective_policy) : null);
setParentPolicy(response.parent_policy ? normalizePolicy(response.parent_policy) : null);
setEffectivePolicySources(response.effective_policy_sources ?? []);
} catch (err) {
setPolicy(blankPolicy);
setEffectivePolicy(null);
setParentPolicy(null);
setEffectivePolicySources([]);
setError(errorMessage(err));
} finally {
setLoading(false);
@@ -370,6 +378,8 @@ export function MailProfilePolicyEditor({
const response = await updateMailProfilePolicy(settings, scopeType, normalizePolicy(policy), scopeId);
setPolicy(normalizePolicy(response.policy));
setEffectivePolicy(response.effective_policy ? normalizePolicy(response.effective_policy) : null);
setParentPolicy(response.parent_policy ? normalizePolicy(response.parent_policy) : null);
setEffectivePolicySources(response.effective_policy_sources ?? []);
setSuccess("Mail profile policy saved.");
await onSaved?.();
} catch (err) {
@@ -385,6 +395,18 @@ export function MailProfilePolicyEditor({
);
const selectedProfileIds = new Set(policy.allowed_profile_ids ?? []);
const disabled = locked || busy || loading || !canWrite || !scopeReady;
const parentAllowedProfileIds = parentPolicy?.allowed_profile_ids?.length ? new Set(parentPolicy.allowed_profile_ids) : null;
const parentBlocksUserProfiles = parentPolicy?.allow_user_profiles === false;
const parentBlocksGroupProfiles = parentPolicy?.allow_group_profiles === false;
const parentBlocksCampaignProfiles = parentPolicy?.allow_campaign_profiles === false;
const smtpCredentialLocked = parentPolicy?.smtp_credentials?.allow_override === false;
const imapCredentialLocked = parentPolicy?.imap_credentials?.allow_override === false;
const blockedProfileDefinitions = [
parentBlocksUserProfiles ? "user" : "",
parentBlocksGroupProfiles ? "group" : "",
parentBlocksCampaignProfiles ? "campaign-local settings" : ""
].filter(Boolean).join(", ");
const lockedCredentialKinds = [smtpCredentialLocked ? "SMTP" : "", imapCredentialLocked ? "IMAP" : ""].filter(Boolean).join(" and ");
function patchPolicy(patch: Partial<MailProfilePolicy>) {
setPolicy((current) => normalizePolicy({ ...current, ...patch }));
@@ -438,31 +460,34 @@ export function MailProfilePolicyEditor({
<div className="mail-profile-checkbox-grid">
{candidateProfiles.map((profile) => (
<label className="mail-profile-checkbox" key={profile.id}>
<input type="checkbox" checked={selectedProfileIds.has(profile.id)} disabled={disabled} onChange={(event) => toggleAllowedProfile(profile.id, event.target.checked)} />
<input type="checkbox" checked={selectedProfileIds.has(profile.id)} disabled={disabled || Boolean(parentAllowedProfileIds && !parentAllowedProfileIds.has(profile.id) && !selectedProfileIds.has(profile.id))} onChange={(event) => toggleAllowedProfile(profile.id, event.target.checked)} />
<span><strong>{profile.name}</strong><small>{scopeLabel(profile)} · {transportLabel(profile.smtp)}</small></span>
</label>
))}
{candidateProfiles.length === 0 && <p className="muted small-note">No profiles are visible for this policy scope.</p>}
</div>
{parentAllowedProfileIds && <p className="muted small-note">An ancestor allow-list limits selectable profiles to {parentAllowedProfileIds.size} profile(s).</p>}
</section>
<section className="mail-policy-section">
<h3>Lower-level profile definitions</h3>
<h3>Lower-level mail definitions</h3>
<div className="admin-form-grid three-columns mail-policy-flags">
<PolicyFlagSelect label="User profiles" value={booleanToFlag(policy.allow_user_profiles)} disabled={disabled} onChange={(value) => setFlag("allow_user_profiles", value)} />
<PolicyFlagSelect label="Group profiles" value={booleanToFlag(policy.allow_group_profiles)} disabled={disabled} onChange={(value) => setFlag("allow_group_profiles", value)} />
<PolicyFlagSelect label="Campaign profiles" value={booleanToFlag(policy.allow_campaign_profiles)} disabled={disabled} onChange={(value) => setFlag("allow_campaign_profiles", value)} />
<PolicyFlagSelect label="User profiles" value={booleanToFlag(policy.allow_user_profiles)} disabled={disabled} allowDisabled={parentBlocksUserProfiles} onChange={(value) => setFlag("allow_user_profiles", value)} />
<PolicyFlagSelect label="Group profiles" value={booleanToFlag(policy.allow_group_profiles)} disabled={disabled} allowDisabled={parentBlocksGroupProfiles} onChange={(value) => setFlag("allow_group_profiles", value)} />
<PolicyFlagSelect label="Campaign-local settings" value={booleanToFlag(policy.allow_campaign_profiles)} disabled={disabled} allowDisabled={parentBlocksCampaignProfiles} onChange={(value) => setFlag("allow_campaign_profiles", value)} />
</div>
{blockedProfileDefinitions && <PolicyLockedHint>Explicit allow is unavailable for {blockedProfileDefinitions} because an ancestor policy blocks those definitions.</PolicyLockedHint>}
</section>
<section className="mail-policy-section">
<h3>Credential inheritance</h3>
<div className="admin-form-grid two-columns mail-policy-flags">
<CredentialInheritanceSelect label="SMTP credentials" value={credentialInheritanceToValue(policy.smtp_credentials?.inherit)} disabled={disabled} onChange={(value) => setCredentialPolicy("smtp", { inherit: valueToCredentialInheritance(value) })} />
<CredentialOverrideSelect label="SMTP override" value={credentialOverrideToValue(policy.smtp_credentials?.allow_override)} disabled={disabled} onChange={(value) => setCredentialPolicy("smtp", { allow_override: valueToCredentialOverride(value) })} />
<CredentialInheritanceSelect label="IMAP credentials" value={credentialInheritanceToValue(policy.imap_credentials?.inherit)} disabled={disabled} onChange={(value) => setCredentialPolicy("imap", { inherit: valueToCredentialInheritance(value) })} />
<CredentialOverrideSelect label="IMAP override" value={credentialOverrideToValue(policy.imap_credentials?.allow_override)} disabled={disabled} onChange={(value) => setCredentialPolicy("imap", { allow_override: valueToCredentialOverride(value) })} />
<CredentialInheritanceSelect label="SMTP credentials" value={credentialInheritanceToValue(policy.smtp_credentials?.inherit)} disabled={disabled || smtpCredentialLocked} onChange={(value) => setCredentialPolicy("smtp", { inherit: valueToCredentialInheritance(value) })} />
<CredentialOverrideSelect label="SMTP override" value={credentialOverrideToValue(policy.smtp_credentials?.allow_override)} disabled={disabled || smtpCredentialLocked} onChange={(value) => setCredentialPolicy("smtp", { allow_override: valueToCredentialOverride(value) })} />
<CredentialInheritanceSelect label="IMAP credentials" value={credentialInheritanceToValue(policy.imap_credentials?.inherit)} disabled={disabled || imapCredentialLocked} onChange={(value) => setCredentialPolicy("imap", { inherit: valueToCredentialInheritance(value) })} />
<CredentialOverrideSelect label="IMAP override" value={credentialOverrideToValue(policy.imap_credentials?.allow_override)} disabled={disabled || imapCredentialLocked} onChange={(value) => setCredentialPolicy("imap", { allow_override: valueToCredentialOverride(value) })} />
</div>
{lockedCredentialKinds && <PolicyLockedHint>{lockedCredentialKinds} credential inheritance is locked by an ancestor policy.</PolicyLockedHint>}
</section>
<section className="mail-policy-section">
@@ -480,17 +505,19 @@ export function MailProfilePolicyEditor({
</section>
{effectivePolicy && (
<section className="mail-policy-section mail-policy-effective">
<h3>Effective campaign policy</h3>
<div className="mail-policy-effective-grid">
<div><span>Profiles</span><strong>{effectiveProfileLabel(effectivePolicy.allowed_profile_ids)}</strong></div>
<div><span>User profiles</span><strong>{effectivePolicy.allow_user_profiles ? "Allowed" : "Blocked"}</strong></div>
<div><span>Group profiles</span><strong>{effectivePolicy.allow_group_profiles ? "Allowed" : "Blocked"}</strong></div>
<div><span>Campaign profiles</span><strong>{effectivePolicy.allow_campaign_profiles ? "Allowed" : "Blocked"}</strong></div>
<div><span>SMTP credentials</span><strong>{credentialPolicyLabel(effectivePolicy.smtp_credentials)}</strong></div>
<div><span>IMAP credentials</span><strong>{credentialPolicyLabel(effectivePolicy.imap_credentials)}</strong></div>
</div>
</section>
<EffectivePolicyBlock
title="Effective mail policy"
sourcePath={effectivePolicySources.length > 0 ? effectivePolicySources : mailPolicySourcePath(scopeType)}
className="mail-policy-section mail-policy-effective"
gridClassName="mail-policy-effective-grid"
>
<EffectivePolicyValue label="Profiles" value={effectiveProfileLabel(effectivePolicy.allowed_profile_ids)} />
<EffectivePolicyValue label="User profiles" value={effectivePolicy.allow_user_profiles ? "Allowed" : "Blocked"} />
<EffectivePolicyValue label="Group profiles" value={effectivePolicy.allow_group_profiles ? "Allowed" : "Blocked"} />
<EffectivePolicyValue label="Campaign-local" value={effectivePolicy.allow_campaign_profiles ? "Allowed" : "Blocked"} />
<EffectivePolicyValue label="SMTP credentials" value={credentialPolicyLabel(effectivePolicy.smtp_credentials)} />
<EffectivePolicyValue label="IMAP credentials" value={credentialPolicyLabel(effectivePolicy.imap_credentials)} />
</EffectivePolicyBlock>
)}
</div>
</Card>
@@ -626,9 +653,11 @@ function ProfileForm({
onImapEnabledChange={(imapEnabled) => setDraft({ ...draft, imapEnabled })}
smtpDisabled={disabled}
smtpCredentialDisabled={credentialDisabled}
smtpPasswordSaved={Boolean(existingProfile?.smtp_password_configured)}
imapToggleDisabled={imapToggleDisabled}
imapServerDisabled={disabled || !draft.imapEnabled}
imapCredentialDisabled={credentialDisabled || !draft.imapEnabled}
imapPasswordSaved={Boolean(existingProfile?.imap_password_configured)}
imapActionDisabled={disabled || !draft.imapEnabled}
smtpTitle="SMTP"
imapTitle="IMAP"
@@ -648,12 +677,12 @@ function ProfileForm({
);
}
function PolicyFlagSelect({ label, value, disabled, onChange }: { label: string; value: PolicyFlagValue; disabled: boolean; onChange: (value: PolicyFlagValue) => void }) {
function PolicyFlagSelect({ label, value, disabled, allowDisabled = false, onChange }: { label: string; value: PolicyFlagValue; disabled: boolean; allowDisabled?: boolean; onChange: (value: PolicyFlagValue) => void }) {
return (
<FormField label={label}>
<select value={value} disabled={disabled} onChange={(event) => onChange(event.target.value as PolicyFlagValue)}>
<option value="inherit">Inherit</option>
<option value="allow">Explicit allow</option>
<option value="allow" disabled={allowDisabled}>Explicit allow</option>
<option value="deny">Deny</option>
</select>
</FormField>
@@ -916,6 +945,14 @@ function effectiveProfileLabel(value: string[] | null | undefined): string {
return `${value.length} profile(s)`;
}
function mailPolicySourcePath(scopeType: MailProfileScope): string[] {
if (scopeType === "system") return ["System"];
if (scopeType === "tenant") return ["System", "Tenant"];
if (scopeType === "user") return ["System", "Tenant", "User"];
if (scopeType === "group") return ["System", "Tenant", "Group"];
return ["System", "Tenant", "Owner policy", "Campaign"];
}
function transportLabel(transport: MailSmtpTestPayload | MailImapTestPayload | null | undefined): string {
if (!transport) return "Not configured";
const host = transport.host || "No host";