import type { CreateFlowState } from "../../app/(app)/create/types"; import idAliasesFile from "../../data/create/customRule/_idAliases.json"; import type { DeckId } from "./deckTypes"; import { DECK_IDS } from "./deckTypes"; type MovedTo = { deck: string; id: string }; type MethodAlias = | string | { target: string | null; movedTo?: MovedTo[]; }; type IdAliasesFile = { methods: Record>; coreValues: { oldPositionToNewId: Record; labelMerges: Record; }; }; const aliases = idAliasesFile as IdAliasesFile; function isDeckId(value: string): value is DeckId { return (DECK_IDS as readonly string[]).includes(value); } export type ResolvedMethodId = { deck: DeckId; id: string }; /** * Old card id → new card id. A cut card follows `movedTo` when that card * lives in a deck. `null` means the card was cut and has nowhere to go. */ export function resolveMethodId( deck: DeckId, id: string, ): ResolvedMethodId | null { const raw = aliases.methods[deck]?.[id]; if (raw === undefined) return { deck, id }; if (typeof raw === "string") return { deck, id: raw }; if (typeof raw.target === "string" && raw.target.length > 0) { return { deck, id: raw.target }; } const moved = raw.movedTo?.find((row) => isDeckId(row.deck)); if (moved && isDeckId(moved.deck)) return { deck: moved.deck, id: moved.id }; return null; } /** Old 1-based core-value position → stable id. `null` means the value was cut. */ export function resolveCoreValueId(id: string): string | null { if (Object.prototype.hasOwnProperty.call(aliases.coreValues.oldPositionToNewId, id)) { return aliases.coreValues.oldPositionToNewId[id] ?? null; } return id; } export function resolveCoreValueLabel(label: string): string | null { if (Object.prototype.hasOwnProperty.call(aliases.coreValues.labelMerges, label)) { return aliases.coreValues.labelMerges[label]; } return label; } function remapIdList( ids: string[] | undefined, deck: DeckId, into: Map, ): void { if (!ids) return; for (const id of ids) { const next = resolveMethodId(deck, id); if (!next) continue; const list = into.get(next.deck) ?? []; if (!list.includes(next.id)) list.push(next.id); into.set(next.deck, list); } } const METHOD_ID_KEYS = { decisionApproaches: "selectedDecisionApproachIds", membership: "selectedMembershipMethodIds", conflictManagement: "selectedConflictManagementIds", communication: "selectedCommunicationMethodIds", } as const satisfies Record; const DETAIL_KEYS = { decisionApproaches: "decisionApproachDetailsById", membership: "membershipMethodDetailsById", conflictManagement: "conflictManagementDetailsById", communication: "communicationMethodDetailsById", } as const satisfies Record; /** * Remap saved method and core-value ids onto the remade decks. * Safe to run more than once: stable ids pass through. */ export function migrateSavedDeckIds( state: CreateFlowState, ): CreateFlowState { const byDeck = new Map(); for (const deck of DECK_IDS) { const key = METHOD_ID_KEYS[deck]; const ids = state[key]; remapIdList(Array.isArray(ids) ? (ids as string[]) : undefined, deck, byDeck); } const detailsByDeck = new Map>(); for (const deck of DECK_IDS) { const key = DETAIL_KEYS[deck]; const details = state[key]; if (!details || typeof details !== "object") continue; for (const [id, value] of Object.entries(details as Record)) { const resolved = resolveMethodId(deck, id); if (!resolved) continue; const next = detailsByDeck.get(resolved.deck) ?? {}; if (next[resolved.id] === undefined) next[resolved.id] = value; detailsByDeck.set(resolved.deck, next); } } const detailsOut: Partial = {}; for (const deck of DECK_IDS) { if (!detailsByDeck.has(deck) && state[DETAIL_KEYS[deck]] === undefined) continue; (detailsOut as Record)[DETAIL_KEYS[deck]] = detailsByDeck.get(deck) ?? {}; } const coreIds: string[] = []; const seenValues = new Set(); for (const id of state.selectedCoreValueIds ?? []) { const next = resolveCoreValueId(id); if (!next || seenValues.has(next)) continue; seenValues.add(next); coreIds.push(next); } const coreDetails = state.coreValueDetailsByChipId; let nextCoreDetails = coreDetails; if (coreDetails) { nextCoreDetails = {}; for (const [id, value] of Object.entries(coreDetails)) { const next = resolveCoreValueId(id); if (!next || nextCoreDetails[next]) continue; nextCoreDetails[next] = value; } } const seenSnapshot = new Set(); const snapshot = state.coreValuesChipsSnapshot?.flatMap((row) => { const nextId = resolveCoreValueId(row.id); if (!nextId || seenSnapshot.has(nextId)) return []; seenSnapshot.add(nextId); const mergedLabel = resolveCoreValueLabel(row.label); if (mergedLabel === null) return []; return [{ ...row, id: nextId, label: mergedLabel }]; }); const methodPatch: Partial = {}; for (const deck of DECK_IDS) { const key = METHOD_ID_KEYS[deck]; if (state[key] !== undefined || byDeck.has(deck)) { (methodPatch as Record)[key] = byDeck.get(deck) ?? []; } } return { ...state, ...methodPatch, ...detailsOut, ...(state.selectedCoreValueIds ? { selectedCoreValueIds: coreIds } : {}), ...(snapshot ? { coreValuesChipsSnapshot: snapshot } : {}), ...(nextCoreDetails ? { coreValueDetailsByChipId: nextCoreDetails } : {}), }; }