Files
govoplan-core/webui/src/platform/referenceProviders.ts
T

182 lines
5.1 KiB
TypeScript

import {
apiFetch,
apiPath,
type ApiQueryParams
} from "../api/client";
import { fetchPlatformModules } from "../api/platform";
import { filterSearchableSelectOptions } from "../components/SearchableSelect";
import {
unavailableReferenceOption,
type ReferenceOption,
type ReferenceOptionProvider
} from "../components/ReferenceSelect";
import type { ApiSettings } from "../types";
type ApiReferenceOptionPayload = {
value: string;
label: string;
description?: string | null;
kind?: string | null;
availability?: ReferenceOption["availability"];
disabled?: boolean;
source_module?: string | null;
provenance?: Record<string, unknown>;
};
export function apiReferenceOptionProvider(
settings: ApiSettings,
path: string,
params: ApiQueryParams = {}
): ReferenceOptionProvider {
async function load(
query: string,
selectedValues: readonly string[],
limit: number,
signal: AbortSignal
): Promise<ReferenceOption[]> {
const response = await apiFetch<{
options: ApiReferenceOptionPayload[];
provider_available: boolean;
}>(
settings,
apiPath(path, {
...params,
q: query,
selected: selectedValues,
limit
}),
{ signal }
);
return response.options.map(referenceOptionFromPayload);
}
return {
search: (query, context) =>
load(query, context.selectedValues, context.limit, context.signal),
async resolve(values, context) {
const uniqueValues = [...new Set(values.map((value) => value.trim()).filter(Boolean))];
const chunks: string[][] = [];
for (let index = 0; index < uniqueValues.length; index += 200) {
chunks.push(uniqueValues.slice(index, index + 200));
}
const pages = await Promise.all(
chunks.map((chunk) =>
load("", chunk, Math.min(200, Math.max(1, chunk.length)), context.signal)
)
);
const byValue = new Map(
pages.flat().map((option) => [option.value, option])
);
return uniqueValues.map(
(value) =>
byValue.get(value)
?? unavailableReferenceOption(value)
);
}
};
}
export function platformModuleReferenceProvider(
settings: ApiSettings,
presentedOptions: readonly ReferenceOption[] = []
): ReferenceOptionProvider {
const presentedByValue = new Map(
presentedOptions.map((option) => [option.value, option])
);
let cataloguePromise: Promise<ReferenceOption[]> | null = null;
async function catalogue(signal: AbortSignal): Promise<ReferenceOption[]> {
cataloguePromise ??= loadPlatformModuleOptions(
settings,
presentedOptions,
presentedByValue
).catch((error: unknown) => {
cataloguePromise = null;
throw error;
});
const options = await cataloguePromise;
if (signal.aborted) throw abortError();
return options;
}
return {
async search(query, context) {
const options = await catalogue(context.signal);
return filterSearchableSelectOptions(
options,
query,
context.limit
) as ReferenceOption[];
},
async resolve(values, context) {
const options = await catalogue(context.signal);
const byValue = new Map(
[...presentedOptions, ...options].map((option) => [
option.value,
option
])
);
return values.map(
(value) =>
byValue.get(value)
?? unavailableReferenceOption(value, "Unavailable module")
);
}
};
}
async function loadPlatformModuleOptions(
settings: ApiSettings,
presentedOptions: readonly ReferenceOption[],
presentedByValue: ReadonlyMap<string, ReferenceOption>
): Promise<ReferenceOption[]> {
let modules;
try {
modules = (await fetchPlatformModules(settings)).modules;
} catch (error) {
if (presentedOptions.length) return [...presentedOptions];
throw error;
}
return modules
.filter((module) => module.enabled)
.map((module) => {
const presented = presentedByValue.get(module.id);
return {
value: module.id,
label: presented?.label ?? module.name ?? module.id,
description: presented?.description
?? `${module.id} · version ${module.version}`,
searchText: presented?.searchText
?? `${module.id} ${module.name} ${module.version}`,
kind: "module",
availability: presented?.availability ?? "available",
disabled: presented?.disabled ?? false,
sourceModule: "core",
provenance: {
version: module.version,
activeRegistry: true,
...(presented?.provenance ?? {})
}
} satisfies ReferenceOption;
});
}
function referenceOptionFromPayload(
option: ApiReferenceOptionPayload
): ReferenceOption {
return {
value: option.value,
label: option.label,
description: option.description,
kind: option.kind,
availability: option.availability,
disabled: option.disabled,
sourceModule: option.source_module,
provenance: option.provenance
};
}
function abortError(): DOMException {
return new DOMException("The operation was aborted.", "AbortError");
}