import { filterSearchableSelectOptions, unavailableReferenceOption, type ApiSettings, type CredentialReferenceSelectorContext, type CredentialReferenceSelectorsUiCapability, type ReferenceOption, type ReferenceOptionProvider } from "@govoplan/core-webui"; import { fetchMailSettingsDelta, type MailServerProfile } from "../../api/mail"; export const mailCredentialReferenceSelectors: CredentialReferenceSelectorsUiCapability = { serverProvider: createMailServerReferenceProvider }; export function createMailServerReferenceProvider( settings: ApiSettings, context: CredentialReferenceSelectorContext ): ReferenceOptionProvider { let cataloguePromise: Promise | null = null; async function catalogue(signal: AbortSignal): Promise { cataloguePromise ??= loadProfiles(settings, context, signal) .then(mailServerOptions) .catch((error: unknown) => { cataloguePromise = null; throw error; }); const options = await cataloguePromise; if (signal.aborted) throw abortError(); return options; } return { async search(query, providerContext) { const options = await catalogue(providerContext.signal); return filterSearchableSelectOptions( options, query, providerContext.limit ) as ReferenceOption[]; }, async resolve(values, providerContext) { const options = await catalogue(providerContext.signal); const byValue = new Map( options.map((option) => [option.value, option]) ); return values.map( (value) => byValue.get(value) ?? unavailableReferenceOption(value, "Unavailable mail server") ); } }; } function mailServerOptions( profiles: readonly MailServerProfile[] ): ReferenceOption[] { return profiles.flatMap((profile) => (profile.servers ?? []).map((server) => ({ value: `mail:${server.id}`, label: server.name, description: [ profile.name, server.protocol.toUpperCase(), server.is_active ? null : "Inactive", server.id ].filter(Boolean).join(" ยท "), searchText: [ profile.slug, profile.name, server.name, server.protocol, server.id ].join(" "), kind: "mail_server", availability: server.is_active ? "available" : "inactive", disabled: !server.is_active, sourceModule: "mail", provenance: { profileId: profile.id, serverId: server.id, protocol: server.protocol, scopeType: server.scope_type, scopeId: server.scope_id } } satisfies ReferenceOption)) ); } async function loadProfiles( settings: ApiSettings, context: CredentialReferenceSelectorContext, signal: AbortSignal ): Promise { let watermark: string | null = null; let profiles: MailServerProfile[] = []; let first = true; do { const response = await fetchMailSettingsDelta(settings, { scope_type: context.scopeType, scope_id: context.scopeId, include_inactive: true, since: first ? null : watermark, limit: 200 }); if (signal.aborted) throw abortError(); profiles = response.full ? response.profiles : mergeProfiles(profiles, response.profiles, response.deleted); watermark = response.watermark ?? null; first = false; if (!response.has_more) break; } while (watermark); return profiles; } function mergeProfiles( current: readonly MailServerProfile[], changed: readonly MailServerProfile[], deleted: readonly { resource_type: string; resource_id: string }[] ): MailServerProfile[] { const removed = new Set( deleted .filter((item) => item.resource_type === "mail_profile") .map((item) => item.resource_id) ); const merged = new Map( current .filter((profile) => !removed.has(profile.id)) .map((profile) => [profile.id, profile]) ); for (const profile of changed) merged.set(profile.id, profile); return [...merged.values()]; } function abortError(): DOMException { return new DOMException("The operation was aborted.", "AbortError"); }