182 lines
5.1 KiB
TypeScript
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");
|
|
}
|