Cleanup pass
This commit is contained in:
@@ -135,8 +135,14 @@ function CreateFlowLayoutContent({
|
|||||||
} = useCreateFlowNavigation(
|
} = useCreateFlowNavigation(
|
||||||
skipCommunitySave ? { skipCommunitySave: true } : undefined,
|
skipCommunitySave ? { skipCommunitySave: true } : undefined,
|
||||||
);
|
);
|
||||||
const { state, clearState, updateState, resetCustomRuleSelections, setMethodSectionsPinCommitted, replaceState } =
|
const {
|
||||||
useCreateFlow();
|
state,
|
||||||
|
clearState,
|
||||||
|
updateState,
|
||||||
|
resetCustomRuleSelections,
|
||||||
|
setMethodSectionsPinCommitted,
|
||||||
|
replaceState,
|
||||||
|
} = useCreateFlow();
|
||||||
const { draftSaveBannerMessage, setDraftSaveBannerMessage } =
|
const { draftSaveBannerMessage, setDraftSaveBannerMessage } =
|
||||||
useCreateFlowDraftSaveBanner();
|
useCreateFlowDraftSaveBanner();
|
||||||
const [communitySaveMagicLinkSubmitting, setCommunitySaveMagicLinkSubmitting] =
|
const [communitySaveMagicLinkSubmitting, setCommunitySaveMagicLinkSubmitting] =
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ import { buildTemplateCustomizePrefill } from "../../../../lib/create/applyTempl
|
|||||||
import { loadTemplateReviewBySlug } from "../../../../lib/create/loadTemplateReviewBySlug";
|
import { loadTemplateReviewBySlug } from "../../../../lib/create/loadTemplateReviewBySlug";
|
||||||
import { stripCustomRuleSelectionFields } from "../../../../lib/create/stripCustomRuleSelectionFields";
|
import { stripCustomRuleSelectionFields } from "../../../../lib/create/stripCustomRuleSelectionFields";
|
||||||
import messages from "../../../../messages/en/index";
|
import messages from "../../../../messages/en/index";
|
||||||
import type { CreateFlowState } from "../types";
|
import type {
|
||||||
|
CreateFlowContextValue,
|
||||||
|
CreateFlowState,
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
type AppRouterLike = { push: (_href: string) => void };
|
type AppRouterLike = { push: (_href: string) => void };
|
||||||
type UpdateState = (_patch: Partial<CreateFlowState>) => void;
|
type UpdateState = CreateFlowContextValue["updateState"];
|
||||||
type ReplaceStateFn = (
|
type ReplaceStateFn = CreateFlowContextValue["replaceState"];
|
||||||
_next: CreateFlowState | ((_prev: CreateFlowState) => CreateFlowState),
|
|
||||||
) => void;
|
|
||||||
|
|
||||||
export type UseTemplateReviewActionsResult = {
|
export type UseTemplateReviewActionsResult = {
|
||||||
/** True iff the current pathname is a template-review route (locale/basePath tolerant). */
|
/** True iff the current pathname is a template-review route (locale/basePath tolerant). */
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ export function FinalReviewScreen({
|
|||||||
[markCreateFlowInteraction, updateState, state],
|
[markCreateFlowInteraction, updateState, state],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { categories: finalReviewCategories, chipLookup } = useMemo(() => {
|
const { categories: finalReviewCategories } = useMemo(() => {
|
||||||
const { names, rows: fallbackRows } = readFallbackCategoryRows(
|
const { names, rows: fallbackRows } = readFallbackCategoryRows(
|
||||||
m.create.reviewAndComplete.finalReview.categories,
|
m.create.reviewAndComplete.finalReview.categories,
|
||||||
);
|
);
|
||||||
@@ -196,7 +196,7 @@ export function FinalReviewScreen({
|
|||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return { categories: cats, chipLookup: lookup };
|
return { categories: cats };
|
||||||
}, [
|
}, [
|
||||||
m.create.reviewAndComplete.finalReview.categories,
|
m.create.reviewAndComplete.finalReview.categories,
|
||||||
state,
|
state,
|
||||||
@@ -204,7 +204,6 @@ export function FinalReviewScreen({
|
|||||||
goToStep,
|
goToStep,
|
||||||
variant,
|
variant,
|
||||||
]);
|
]);
|
||||||
void chipLookup;
|
|
||||||
|
|
||||||
const ruleCardTitle = useMemo(() => {
|
const ruleCardTitle = useMemo(() => {
|
||||||
const raw = typeof state.title === "string" ? state.title.trim() : "";
|
const raw = typeof state.title === "string" ? state.title.trim() : "";
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ import { parseRequestedFacetsFromSearchParams } from "../../../lib/server/valida
|
|||||||
*
|
*
|
||||||
* No params → curated ordering (`featured` desc, `sortOrder` asc, `title`
|
* No params → curated ordering (`featured` desc, `sortOrder` asc, `title`
|
||||||
* asc). With `facet.<group>=<value>` query params (repeatable per group),
|
* asc). With `facet.<group>=<value>` query params (repeatable per group),
|
||||||
* templates are re-ranked by composed-method match count; ties fall back to
|
* templates are re-ranked by `TemplateFacet` matrix match when seeded for
|
||||||
* the curated order, score-0 templates remain at the end.
|
* 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.
|
* See `docs/guides/template-recommendation-matrix.md` §9.1.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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`
|
* Map a curated template `body` (DB shape — `sections[]` with `categoryName`
|
||||||
* + `entries[].title`) to the `CreateFlowState` keys the Create Custom Rule
|
* + `entries[].title`) to the `CreateFlowState` keys the Create Custom Rule
|
||||||
@@ -173,3 +143,19 @@ export function buildTemplateCustomizePrefill(
|
|||||||
|
|
||||||
return prefill;
|
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. */
|
/** Valid MultiSelect chip state from snapshot JSON. */
|
||||||
function normalizeChipState(s: unknown): ChipOption["state"] | undefined {
|
function normalizeChipState(s: unknown): ChipOption["state"] | undefined {
|
||||||
return s === "selected" ||
|
return s === "selected" ||
|
||||||
@@ -58,7 +41,10 @@ export function buildCoreValueChipOptionsFromDraft(
|
|||||||
const selected = new Set(selectedCoreValueIds ?? []);
|
const selected = new Set(selectedCoreValueIds ?? []);
|
||||||
|
|
||||||
if (!snapshot?.length) {
|
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));
|
const snapById = new Map(snapshot.map((r) => [r.id, r] as const));
|
||||||
|
|||||||
@@ -18,6 +18,39 @@ type TemplatesResponse = {
|
|||||||
scores?: Record<string, TemplateFacetScoreDto>;
|
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. */
|
/** Matches `listRankedRuleTemplatesFromDb` / GET `/api/templates` with facet params. */
|
||||||
export type TemplateFacetScoreDto = {
|
export type TemplateFacetScoreDto = {
|
||||||
score: number;
|
score: number;
|
||||||
@@ -48,27 +81,23 @@ export function isTemplatesFetchAborted(e: unknown): boolean {
|
|||||||
export async function fetchTemplates(
|
export async function fetchTemplates(
|
||||||
options?: FetchTemplatesOptions,
|
options?: FetchTemplatesOptions,
|
||||||
): Promise<RuleTemplateDto[] | { error: string }> {
|
): Promise<RuleTemplateDto[] | { error: string }> {
|
||||||
try {
|
const got = await getTemplatesJson("", options?.signal);
|
||||||
const res = await fetch("/api/templates", {
|
if (got.ok === false) {
|
||||||
credentials: "include",
|
if (got.error === "aborted") {
|
||||||
signal: options?.signal,
|
throw new DOMException("Aborted", "AbortError");
|
||||||
});
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
return { error: "Could not load templates" };
|
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) {
|
if (options.facetQuery.length === 0) {
|
||||||
return { error: "Could not load templates" };
|
return { error: "Could not load templates" };
|
||||||
}
|
}
|
||||||
try {
|
const got = await getTemplatesJson(options.facetQuery, options.signal);
|
||||||
const res = await fetch(`/api/templates?${options.facetQuery}`, {
|
if (got.ok === false) {
|
||||||
credentials: "include",
|
if (got.error === "aborted") {
|
||||||
signal: options.signal,
|
throw new DOMException("Aborted", "AbortError");
|
||||||
});
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
return { error: "Could not load templates" };
|
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(
|
export async function fetchTemplateBySlug(
|
||||||
|
|||||||
@@ -14,9 +14,8 @@ export type GovernanceTemplateCatalogEntry = {
|
|||||||
/** Path under public/ for getAssetPath() — Figma Asset / Template Mark */
|
/** Path under public/ for getAssetPath() — Figma Asset / Template Mark */
|
||||||
iconPath: string;
|
iconPath: string;
|
||||||
/**
|
/**
|
||||||
* When true, the templates grid shows the “RECOMMENDED” tag (facet-based
|
* When true, static catalog rows show the “RECOMMENDED” tag. Facet-ranked
|
||||||
* scores will set this in `ruleTemplateToGridEntry` when wired; catalog
|
* `/templates` sets this in `gridEntriesWithFacetScores` instead.
|
||||||
* entries omit unless intentionally static).
|
|
||||||
*/
|
*/
|
||||||
recommended?: boolean;
|
recommended?: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -114,22 +114,6 @@ export function gridEntriesWithFacetScores(
|
|||||||
const bySlug = (templates: RuleTemplateDto[]) =>
|
const bySlug = (templates: RuleTemplateDto[]) =>
|
||||||
new Map(templates.map((t) => [t.slug, t] as const));
|
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.
|
* Home row: prefer API row per slug; if missing, use static Figma catalog entry.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user