Files
community-rule/lib/create/applyTemplatePrefill.ts
T
adilalloandCursor bcbc6f30d9 Import the remade decks on stable ids and recommend one card per job, so a saved draft still loads after a chip is renamed.
The real-group profiles stay as a test, so a change that moves a pick fails.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-04 14:22:54 -07:00

176 lines
6.3 KiB
TypeScript

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<string, unknown>;
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<CreateFlowState, "selectedCoreValueIds" | "coreValuesChipsSnapshot"> {
const wantedByLower = new Map<string, string>();
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<DeckId | "coreValues", string[]>;
function prefillFromTemplateChips(slug: string): Partial<CreateFlowState> | null {
const templates = (
templateChipsFile as { templates: Record<string, TemplateChipSet> }
).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<CreateFlowState> {
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<CreateFlowState> = {};
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<CreateFlowState> {
const full = buildTemplateCustomizePrefill(body);
if (full.selectedCoreValueIds === undefined) return {};
return {
selectedCoreValueIds: full.selectedCoreValueIds,
coreValuesChipsSnapshot: full.coreValuesChipsSnapshot,
};
}