Cleanup pass

This commit is contained in:
adilallo
2026-04-29 21:18:36 -06:00
parent 7fde82a94c
commit a31a36c926
9 changed files with 103 additions and 121 deletions
+8 -2
View File
@@ -135,8 +135,14 @@ function CreateFlowLayoutContent({
} = useCreateFlowNavigation(
skipCommunitySave ? { skipCommunitySave: true } : undefined,
);
const { state, clearState, updateState, resetCustomRuleSelections, setMethodSectionsPinCommitted, replaceState } =
useCreateFlow();
const {
state,
clearState,
updateState,
resetCustomRuleSelections,
setMethodSectionsPinCommitted,
replaceState,
} = useCreateFlow();
const { draftSaveBannerMessage, setDraftSaveBannerMessage } =
useCreateFlowDraftSaveBanner();
const [communitySaveMagicLinkSubmitting, setCommunitySaveMagicLinkSubmitting] =
@@ -5,13 +5,14 @@ import { buildTemplateCustomizePrefill } from "../../../../lib/create/applyTempl
import { loadTemplateReviewBySlug } from "../../../../lib/create/loadTemplateReviewBySlug";
import { stripCustomRuleSelectionFields } from "../../../../lib/create/stripCustomRuleSelectionFields";
import messages from "../../../../messages/en/index";
import type { CreateFlowState } from "../types";
import type {
CreateFlowContextValue,
CreateFlowState,
} from "../types";
type AppRouterLike = { push: (_href: string) => void };
type UpdateState = (_patch: Partial<CreateFlowState>) => void;
type ReplaceStateFn = (
_next: CreateFlowState | ((_prev: CreateFlowState) => CreateFlowState),
) => void;
type UpdateState = CreateFlowContextValue["updateState"];
type ReplaceStateFn = CreateFlowContextValue["replaceState"];
export type UseTemplateReviewActionsResult = {
/** True iff the current pathname is a template-review route (locale/basePath tolerant). */
@@ -123,7 +123,7 @@ export function FinalReviewScreen({
[markCreateFlowInteraction, updateState, state],
);
const { categories: finalReviewCategories, chipLookup } = useMemo(() => {
const { categories: finalReviewCategories } = useMemo(() => {
const { names, rows: fallbackRows } = readFallbackCategoryRows(
m.create.reviewAndComplete.finalReview.categories,
);
@@ -196,7 +196,7 @@ export function FinalReviewScreen({
: undefined,
};
});
return { categories: cats, chipLookup: lookup };
return { categories: cats };
}, [
m.create.reviewAndComplete.finalReview.categories,
state,
@@ -204,7 +204,6 @@ export function FinalReviewScreen({
goToStep,
variant,
]);
void chipLookup;
const ruleCardTitle = useMemo(() => {
const raw = typeof state.title === "string" ? state.title.trim() : "";
+3 -2
View File
@@ -9,8 +9,9 @@ import { parseRequestedFacetsFromSearchParams } from "../../../lib/server/valida
*
* No params → curated ordering (`featured` desc, `sortOrder` asc, `title`
* asc). With `facet.<group>=<value>` query params (repeatable per group),
* templates are re-ranked by composed-method match count; ties fall back to
* the curated order, score-0 templates remain at the end.
* templates are re-ranked by `TemplateFacet` matrix match when seeded for
* that slug, else by composed-method × `MethodFacet` match count; ties fall
* back to the curated order, score-0 templates remain at the end.
*
* See `docs/guides/template-recommendation-matrix.md` §9.1.
*/
+16 -30
View File
@@ -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,
};
}
+4 -18
View File
@@ -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));
+62 -42
View File
@@ -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(
+2 -3
View File
@@ -14,9 +14,8 @@ export type GovernanceTemplateCatalogEntry = {
/** Path under public/ for getAssetPath() — Figma Asset / Template Mark */
iconPath: string;
/**
* When true, the templates grid shows the “RECOMMENDED” tag (facet-based
* scores will set this in `ruleTemplateToGridEntry` when wired; catalog
* entries omit unless intentionally static).
* When true, static catalog rows show the “RECOMMENDED” tag. Facet-ranked
* `/templates` sets this in `gridEntriesWithFacetScores` instead.
*/
recommended?: boolean;
};
-16
View File
@@ -114,22 +114,6 @@ export function gridEntriesWithFacetScores(
const bySlug = (templates: RuleTemplateDto[]) =>
new Map(templates.map((t) => [t.slug, t] as const));
/**
* Ordered subset for home: follow `slugOrder`; skip missing slugs.
*/
export function gridEntriesForSlugOrder(
templates: RuleTemplateDto[],
slugOrder: readonly string[],
): TemplateGridCardEntry[] {
const map = bySlug(templates);
const out: TemplateGridCardEntry[] = [];
for (const slug of slugOrder) {
const t = map.get(slug);
if (t) out.push(ruleTemplateToGridEntry(t));
}
return out;
}
/**
* Home row: prefer API row per slug; if missing, use static Figma catalog entry.
*/