Cleanup pass
This commit is contained in:
@@ -76,36 +76,6 @@ function buildCoreValuePrefill(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Variant of {@link buildTemplateCustomizePrefill} that pulls *only* the
|
||||
* Values section out of a template body. Used by the "Use without changes"
|
||||
* handler so the verbatim template flow still seeds
|
||||
* `coreValuesChipsSnapshot` + `selectedCoreValueIds` — without that, the
|
||||
* final-review screen has no per-chip ids to attach edits to and falls
|
||||
* back to the read-only chip modal for values.
|
||||
*
|
||||
* Returns an empty object when the body is malformed or has no Values
|
||||
* section.
|
||||
*/
|
||||
export function buildCoreValuesPrefillFromTemplateBody(
|
||||
body: unknown,
|
||||
): Partial<CreateFlowState> {
|
||||
if (!body || typeof body !== "object") return {};
|
||||
const sections = (body as { sections?: unknown }).sections;
|
||||
if (!Array.isArray(sections)) return {};
|
||||
|
||||
for (const raw of sections) {
|
||||
if (!isTemplateSection(raw)) continue;
|
||||
const key = normaliseCategoryKey(raw.categoryName as string);
|
||||
if (key !== "values" && key !== "corevalues") continue;
|
||||
const titles = entryTitles(raw.entries);
|
||||
if (titles.length === 0) continue;
|
||||
return buildCoreValuePrefill(titles);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a curated template `body` (DB shape — `sections[]` with `categoryName`
|
||||
* + `entries[].title`) to the `CreateFlowState` keys the Create Custom Rule
|
||||
@@ -173,3 +143,19 @@ export function buildTemplateCustomizePrefill(
|
||||
|
||||
return prefill;
|
||||
}
|
||||
|
||||
/**
|
||||
* Values section only — delegates to {@link buildTemplateCustomizePrefill}
|
||||
* (same matching rules as Customize). Used by tests and any caller that
|
||||
* needs core-value snapshot seeding without method fields.
|
||||
*/
|
||||
export function buildCoreValuesPrefillFromTemplateBody(
|
||||
body: unknown,
|
||||
): Partial<CreateFlowState> {
|
||||
const full = buildTemplateCustomizePrefill(body);
|
||||
if (full.selectedCoreValueIds === undefined) return {};
|
||||
return {
|
||||
selectedCoreValueIds: full.selectedCoreValueIds,
|
||||
coreValuesChipsSnapshot: full.coreValuesChipsSnapshot,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,23 +13,6 @@ function chipRowsFromPresets(presets: readonly CoreValuePreset[]): ChipOption[]
|
||||
}));
|
||||
}
|
||||
|
||||
function applySavedSelectionToPresetsOnly(
|
||||
options: ChipOption[],
|
||||
saved: string[] | undefined,
|
||||
): ChipOption[] {
|
||||
const selected = new Set(saved ?? []);
|
||||
return options.map((opt) =>
|
||||
opt.state === "custom"
|
||||
? opt
|
||||
: {
|
||||
...opt,
|
||||
state: selected.has(opt.id)
|
||||
? ("selected" as const)
|
||||
: ("unselected" as const),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** Valid MultiSelect chip state from snapshot JSON. */
|
||||
function normalizeChipState(s: unknown): ChipOption["state"] | undefined {
|
||||
return s === "selected" ||
|
||||
@@ -58,7 +41,10 @@ export function buildCoreValueChipOptionsFromDraft(
|
||||
const selected = new Set(selectedCoreValueIds ?? []);
|
||||
|
||||
if (!snapshot?.length) {
|
||||
return applySavedSelectionToPresetsOnly(presetBase, selectedCoreValueIds);
|
||||
return presetBase.map((opt) => ({
|
||||
...opt,
|
||||
state: selected.has(opt.id) ? ("selected" as const) : ("unselected" as const),
|
||||
}));
|
||||
}
|
||||
|
||||
const snapById = new Map(snapshot.map((r) => [r.id, r] as const));
|
||||
|
||||
@@ -18,6 +18,39 @@ type TemplatesResponse = {
|
||||
scores?: Record<string, TemplateFacetScoreDto>;
|
||||
};
|
||||
|
||||
function parseScoresPayload(
|
||||
raw: unknown,
|
||||
): Record<string, TemplateFacetScoreDto> {
|
||||
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
||||
return raw as Record<string, TemplateFacetScoreDto>;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
async function getTemplatesJson(
|
||||
queryString: string,
|
||||
signal: AbortSignal | undefined,
|
||||
): Promise<
|
||||
| { ok: true; data: TemplatesResponse & { error?: string }; statusOk: boolean }
|
||||
| { ok: false; error: "network" | "aborted" }
|
||||
> {
|
||||
const url =
|
||||
queryString.length > 0 ? `/api/templates?${queryString}` : "/api/templates";
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
credentials: "include",
|
||||
signal,
|
||||
});
|
||||
const data = (await res.json()) as TemplatesResponse & { error?: string };
|
||||
return { ok: true, data, statusOk: res.ok };
|
||||
} catch (e) {
|
||||
if (isAbortError(e)) {
|
||||
return { ok: false, error: "aborted" };
|
||||
}
|
||||
return { ok: false, error: "network" };
|
||||
}
|
||||
}
|
||||
|
||||
/** Matches `listRankedRuleTemplatesFromDb` / GET `/api/templates` with facet params. */
|
||||
export type TemplateFacetScoreDto = {
|
||||
score: number;
|
||||
@@ -48,27 +81,23 @@ export function isTemplatesFetchAborted(e: unknown): boolean {
|
||||
export async function fetchTemplates(
|
||||
options?: FetchTemplatesOptions,
|
||||
): Promise<RuleTemplateDto[] | { error: string }> {
|
||||
try {
|
||||
const res = await fetch("/api/templates", {
|
||||
credentials: "include",
|
||||
signal: options?.signal,
|
||||
});
|
||||
const data = (await res.json()) as TemplatesResponse & { error?: string };
|
||||
if (!res.ok) {
|
||||
return {
|
||||
error:
|
||||
typeof data.error === "string"
|
||||
? data.error
|
||||
: "Could not load templates",
|
||||
};
|
||||
}
|
||||
return Array.isArray(data.templates) ? data.templates : [];
|
||||
} catch (e) {
|
||||
if (isAbortError(e)) {
|
||||
throw e;
|
||||
const got = await getTemplatesJson("", options?.signal);
|
||||
if (got.ok === false) {
|
||||
if (got.error === "aborted") {
|
||||
throw new DOMException("Aborted", "AbortError");
|
||||
}
|
||||
return { error: "Could not load templates" };
|
||||
}
|
||||
const { data, statusOk } = got;
|
||||
if (!statusOk) {
|
||||
return {
|
||||
error:
|
||||
typeof data.error === "string"
|
||||
? data.error
|
||||
: "Could not load templates",
|
||||
};
|
||||
}
|
||||
return Array.isArray(data.templates) ? data.templates : [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,33 +111,24 @@ export async function fetchRankedTemplatesByFacets(options: {
|
||||
if (options.facetQuery.length === 0) {
|
||||
return { error: "Could not load templates" };
|
||||
}
|
||||
try {
|
||||
const res = await fetch(`/api/templates?${options.facetQuery}`, {
|
||||
credentials: "include",
|
||||
signal: options.signal,
|
||||
});
|
||||
const data = (await res.json()) as TemplatesResponse & { error?: string };
|
||||
if (!res.ok) {
|
||||
return {
|
||||
error:
|
||||
typeof data.error === "string"
|
||||
? data.error
|
||||
: "Could not load templates",
|
||||
};
|
||||
}
|
||||
const templates = Array.isArray(data.templates) ? data.templates : [];
|
||||
const raw = data.scores;
|
||||
const scores: Record<string, TemplateFacetScoreDto> =
|
||||
raw && typeof raw === "object" && !Array.isArray(raw)
|
||||
? (raw as Record<string, TemplateFacetScoreDto>)
|
||||
: {};
|
||||
return { templates, scores };
|
||||
} catch (e) {
|
||||
if (isAbortError(e)) {
|
||||
throw e;
|
||||
const got = await getTemplatesJson(options.facetQuery, options.signal);
|
||||
if (got.ok === false) {
|
||||
if (got.error === "aborted") {
|
||||
throw new DOMException("Aborted", "AbortError");
|
||||
}
|
||||
return { error: "Could not load templates" };
|
||||
}
|
||||
const { data, statusOk } = got;
|
||||
if (!statusOk) {
|
||||
return {
|
||||
error:
|
||||
typeof data.error === "string"
|
||||
? data.error
|
||||
: "Could not load templates",
|
||||
};
|
||||
}
|
||||
const templates = Array.isArray(data.templates) ? data.templates : [];
|
||||
return { templates, scores: parseScoresPayload(data.scores) };
|
||||
}
|
||||
|
||||
export async function fetchTemplateBySlug(
|
||||
|
||||
Reference in New Issue
Block a user