import type { CommunityStructureChipSnapshotRow, CreateFlowState, } from "../../app/(app)/create/types"; import coreValuesMessages from "../../messages/en/create/customRule/coreValues.json"; import templateChipsFile from "../../data/templates/templateChips.json"; import { assignTemplateMethodSlugsToPrefill } from "./customRuleFacets"; import { methodSlugFromTitle } from "./methodSlugFromTitle"; import type { DeckId } from "./deckTypes"; type TemplateEntry = { title: unknown }; type TemplateSection = { categoryName: unknown; entries: unknown }; function isTemplateSection(x: unknown): x is TemplateSection { if (!x || typeof x !== "object") return false; const o = x as Record; return typeof o.categoryName === "string" && Array.isArray(o.entries); } function entryTitles(entries: unknown): string[] { if (!Array.isArray(entries)) return []; const out: string[] = []; for (const raw of entries) { if (!raw || typeof raw !== "object") continue; const title = (raw as TemplateEntry).title; if (typeof title !== "string") continue; const trimmed = title.trim(); if (trimmed.length > 0) out.push(trimmed); } return out; } /** Normalise a Figma template category header ("Decision-making") for matching. */ function normaliseCategoryKey(name: string): string { return name.toLowerCase().replace(/[^a-z]+/g, ""); } /** Preset core values keyed by the stable id in `coreValues.json`. */ type CorePresetRow = { id: string; label: string }; const CORE_VALUE_PRESETS: readonly CorePresetRow[] = (() => { const raw = (coreValuesMessages as { values: unknown }).values; if (!Array.isArray(raw)) return []; const out: CorePresetRow[] = []; for (const value of raw) { if (!value || typeof value !== "object") continue; const row = value as { id?: unknown; label?: unknown }; if (typeof row.id !== "string" || typeof row.label !== "string") continue; out.push({ id: row.id, label: row.label }); } return out; })(); function buildCoreValuePrefill( titles: readonly string[], ): Pick { const wantedByLower = new Map(); for (const t of titles) wantedByLower.set(t.toLowerCase(), t); const selected: string[] = []; const snapshot: CommunityStructureChipSnapshotRow[] = []; for (const preset of CORE_VALUE_PRESETS) { const isSelected = wantedByLower.delete(preset.label.toLowerCase()); snapshot.push({ id: preset.id, label: preset.label, state: isSelected ? "selected" : "unselected", }); if (isSelected) selected.push(preset.id); } // Any template labels not matching a preset ride along as custom chip rows // so templates authored with bespoke values still pre-select on the screen. for (const original of wantedByLower.values()) { const id = `template-cv-${methodSlugFromTitle(original) || snapshot.length}`; snapshot.push({ id, label: original, state: "selected" }); selected.push(id); } return { selectedCoreValueIds: selected, coreValuesChipsSnapshot: snapshot, }; } /** * Map a curated template `body` (DB shape — `sections[]` with `categoryName` * + `entries[].title`) to the `CreateFlowState` keys the Create Custom Rule * screens read for pre-selection. Used by the "Customize" handler on * `/create/review-template/[slug]` so clicking Customize drops the user into * the custom-rule flow with the template's chips already highlighted. * * Produces: * - `selectedCoreValueIds` + `coreValuesChipsSnapshot` — preset match by * label; non-matching titles become custom chip rows so bespoke template * values still appear selected. * - `selectedCommunicationMethodIds`, `selectedMembershipMethodIds`, * `selectedDecisionApproachIds`, `selectedConflictManagementIds` — chip * ids derived via {@link methodSlugFromTitle}, matching the `methods[].id` * produced by the one-time messages ingest. * * Returns an empty object for malformed bodies (no sections array). */ type TemplateChipSet = Record; function prefillFromTemplateChips(slug: string): Partial | null { const templates = ( templateChipsFile as { templates: Record } ).templates; const chips = templates[slug]; if (!chips) return null; const selected = new Set(chips.coreValues); return { selectedCoreValueIds: [...chips.coreValues], coreValuesChipsSnapshot: CORE_VALUE_PRESETS.map((preset) => ({ id: preset.id, label: preset.label, state: selected.has(preset.id) ? "selected" : "unselected", })), selectedDecisionApproachIds: [...chips.decisionApproaches], selectedMembershipMethodIds: [...chips.membership], selectedConflictManagementIds: [...chips.conflictManagement], selectedCommunicationMethodIds: [...chips.communication], }; } export function buildTemplateCustomizePrefill( body: unknown, templateSlug?: string, ): Partial { if (templateSlug) { const fromIds = prefillFromTemplateChips(templateSlug); if (fromIds) return fromIds; } if (!body || typeof body !== "object") return {}; const sections = (body as { sections?: unknown }).sections; if (!Array.isArray(sections)) return {}; const prefill: Partial = {}; for (const raw of sections) { if (!isTemplateSection(raw)) continue; const key = normaliseCategoryKey(raw.categoryName as string); const titles = entryTitles(raw.entries); if (titles.length === 0) continue; if (key === "values" || key === "corevalues") { Object.assign(prefill, buildCoreValuePrefill(titles)); continue; } const slugs = titles.map(methodSlugFromTitle).filter((s) => s.length > 0); if (slugs.length === 0) continue; assignTemplateMethodSlugsToPrefill(prefill, key, slugs); } 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 { const full = buildTemplateCustomizePrefill(body); if (full.selectedCoreValueIds === undefined) return {}; return { selectedCoreValueIds: full.selectedCoreValueIds, coreValuesChipsSnapshot: full.coreValuesChipsSnapshot, }; }