feat: add reusable mail reference selectors
This commit is contained in:
@@ -45,6 +45,15 @@ 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 {
|
||||
ReferenceMultiSelect,
|
||||
customReferenceOption,
|
||||
platformModuleReferenceProvider,
|
||||
staticReferenceOptionProvider,
|
||||
usePlatformLanguage,
|
||||
usePlatformModules,
|
||||
type ReferenceOption
|
||||
} from "@govoplan/core-webui";
|
||||
import {
|
||||
mailProfileEditTargetInitialSection,
|
||||
mailProfileEditTargetPanelMode,
|
||||
@@ -657,6 +666,7 @@ export function MailProfileScopeManager({
|
||||
|
||||
<ProfileForm
|
||||
settings={settings}
|
||||
profiles={profiles}
|
||||
draft={draft}
|
||||
setDraft={setDraft}
|
||||
editing={editing}
|
||||
@@ -981,6 +991,7 @@ export function MailProfilePolicyEditor({
|
||||
|
||||
function ProfileForm({
|
||||
settings,
|
||||
profiles,
|
||||
draft,
|
||||
setDraft,
|
||||
editing,
|
||||
@@ -994,6 +1005,7 @@ function ProfileForm({
|
||||
setReuseCredentialId
|
||||
}: {
|
||||
settings: ApiSettings;
|
||||
profiles: MailServerProfile[];
|
||||
draft: ProfileDraft;
|
||||
setDraft: (draft: ProfileDraft) => void;
|
||||
editing: EditingProfile;
|
||||
@@ -1006,6 +1018,8 @@ function ProfileForm({
|
||||
reuseCredentialId: string;
|
||||
setReuseCredentialId: (credentialId: string) => void;
|
||||
}) {
|
||||
const modules = usePlatformModules();
|
||||
const { translateText } = usePlatformLanguage();
|
||||
const [smtpTestResult, setSmtpTestResult] = useState<MailServerConnectionTestResult | null>(null);
|
||||
const [imapTestResult, setImapTestResult] = useState<MailServerConnectionTestResult | null>(null);
|
||||
const [mailActionState, setMailActionState] = useState<"smtp" | "imap" | null>(null);
|
||||
@@ -1040,6 +1054,46 @@ function ProfileForm({
|
||||
);
|
||||
const creatingCredential = editTarget.kind === "credentials" && !editTarget.credentialId;
|
||||
const creatingNewCredential = creatingCredential && !reuseCredentialId;
|
||||
const moduleReferenceOptions = useMemo<ReferenceOption[]>(
|
||||
() => modules.map((module) => ({
|
||||
value: module.id,
|
||||
label: translateText(module.label),
|
||||
description: `${module.id} · version ${module.version}`,
|
||||
kind: "module",
|
||||
sourceModule: "core",
|
||||
provenance: { version: module.version }
|
||||
})),
|
||||
[modules, translateText]
|
||||
);
|
||||
const serverReferenceOptions = useMemo<ReferenceOption[]>(
|
||||
() => profiles.flatMap((profile) =>
|
||||
(profile.servers ?? []).map((server) => ({
|
||||
value: `mail:${server.id}`,
|
||||
label: server.name,
|
||||
description: `${profile.name} · ${server.protocol.toUpperCase()} · ${server.id}`,
|
||||
kind: "mail_server",
|
||||
availability: server.is_active ? "available" as const : "inactive" as const,
|
||||
disabled: !server.is_active,
|
||||
sourceModule: "mail",
|
||||
provenance: {
|
||||
profileId: profile.id,
|
||||
serverId: server.id,
|
||||
protocol: server.protocol,
|
||||
scopeType: server.scope_type,
|
||||
scopeId: server.scope_id
|
||||
}
|
||||
}))
|
||||
),
|
||||
[profiles]
|
||||
);
|
||||
const moduleReferenceProvider = useMemo(
|
||||
() => platformModuleReferenceProvider(settings, moduleReferenceOptions),
|
||||
[moduleReferenceOptions, settings]
|
||||
);
|
||||
const serverReferenceProvider = useMemo(
|
||||
() => staticReferenceOptionProvider(serverReferenceOptions),
|
||||
[serverReferenceOptions]
|
||||
);
|
||||
const policyMessages = useMemo(() => validateMailPolicy(effectivePolicy, {
|
||||
smtpHost: draft.smtpHost,
|
||||
imapHost: draft.imapHost
|
||||
@@ -1190,13 +1244,28 @@ function ProfileForm({
|
||||
<FormField label="Description">
|
||||
<input value={draft.credentialDescription} disabled={credentialDisabled} onChange={(event) => setDraft({ ...draft, credentialDescription: event.target.value })} />
|
||||
</FormField>
|
||||
<FormField label="Available to modules" help="Comma-separated module IDs. Keep mail to use this credential here.">
|
||||
<input value={draft.credentialAllowedModules} disabled={credentialDisabled} onChange={(event) => setDraft({ ...draft, credentialAllowedModules: event.target.value })} />
|
||||
<FormField label="Available to modules" help="Choose installed modules. Keep Mail selected to use this credential here.">
|
||||
<ReferenceMultiSelect
|
||||
values={commaValues(draft.credentialAllowedModules)}
|
||||
onChange={(values) => setDraft({ ...draft, credentialAllowedModules: values.join(", ") })}
|
||||
provider={moduleReferenceProvider}
|
||||
aria-label="Credential modules"
|
||||
placeholder="Add a module"
|
||||
disabled={credentialDisabled}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
label="Limited to servers"
|
||||
help="Optional comma-separated references, for example mail:server-id, files:connection-id, calendar:source-id, or addresses:source-id. Leave blank for every permitted server.">
|
||||
<input value={draft.credentialAllowedServerRefs} disabled={credentialDisabled} onChange={(event) => setDraft({ ...draft, credentialAllowedServerRefs: event.target.value })} />
|
||||
help="Choose an announced server or enter a module-qualified reference. Leave blank for every permitted server.">
|
||||
<ReferenceMultiSelect
|
||||
values={commaValues(draft.credentialAllowedServerRefs)}
|
||||
onChange={(values) => setDraft({ ...draft, credentialAllowedServerRefs: values.join(", ") })}
|
||||
provider={serverReferenceProvider}
|
||||
createCustomOption={(value) => customReferenceOption(value, "server")}
|
||||
aria-label="Credential server restrictions"
|
||||
placeholder="Add a server reference"
|
||||
disabled={credentialDisabled}
|
||||
/>
|
||||
</FormField>
|
||||
<ToggleSwitch
|
||||
checked={draft.credentialIsDefault}
|
||||
@@ -1463,18 +1532,8 @@ function updateProfilePayload(
|
||||
function credentialPayload(draft: ProfileDraft, protocol: MailProfileProtocol, preserveBlankPassword: boolean) {
|
||||
const username = protocol === "smtp" ? draft.smtpUsername : draft.imapUsername;
|
||||
const password = protocol === "smtp" ? draft.smtpPassword : draft.imapPassword;
|
||||
const allowedModules = [...new Set(
|
||||
draft.credentialAllowedModules
|
||||
.split(",")
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean)
|
||||
)];
|
||||
const allowedServerRefs = [...new Set(
|
||||
draft.credentialAllowedServerRefs
|
||||
.split(",")
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean)
|
||||
)];
|
||||
const allowedModules = commaValues(draft.credentialAllowedModules);
|
||||
const allowedServerRefs = commaValues(draft.credentialAllowedServerRefs);
|
||||
const payload: {
|
||||
name: string;
|
||||
description: string | null;
|
||||
@@ -1824,6 +1883,15 @@ function stringValue(value: unknown): string {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function commaValues(value: string): string[] {
|
||||
return [...new Set(
|
||||
value
|
||||
.split(",")
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
)];
|
||||
}
|
||||
|
||||
function errorMessage(err: unknown): string {
|
||||
return err instanceof Error ? err.message : String(err);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user