The real-group profiles stay as a test, so a change that moves a pick fails. Co-authored-by: Cursor <cursoragent@cursor.com>
176 lines
5.7 KiB
TypeScript
176 lines
5.7 KiB
TypeScript
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<string, Record<string, MethodAlias>>;
|
|
coreValues: {
|
|
oldPositionToNewId: Record<string, string | null>;
|
|
labelMerges: Record<string, string | null>;
|
|
};
|
|
};
|
|
|
|
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<DeckId, string[]>,
|
|
): 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<DeckId, keyof CreateFlowState>;
|
|
|
|
const DETAIL_KEYS = {
|
|
decisionApproaches: "decisionApproachDetailsById",
|
|
membership: "membershipMethodDetailsById",
|
|
conflictManagement: "conflictManagementDetailsById",
|
|
communication: "communicationMethodDetailsById",
|
|
} as const satisfies Record<DeckId, keyof CreateFlowState>;
|
|
|
|
/**
|
|
* 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<DeckId, string[]>();
|
|
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<DeckId, Record<string, unknown>>();
|
|
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<string, unknown>)) {
|
|
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<CreateFlowState> = {};
|
|
for (const deck of DECK_IDS) {
|
|
if (!detailsByDeck.has(deck) && state[DETAIL_KEYS[deck]] === undefined) continue;
|
|
(detailsOut as Record<string, unknown>)[DETAIL_KEYS[deck]] =
|
|
detailsByDeck.get(deck) ?? {};
|
|
}
|
|
|
|
const coreIds: string[] = [];
|
|
const seenValues = new Set<string>();
|
|
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<string>();
|
|
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<CreateFlowState> = {};
|
|
for (const deck of DECK_IDS) {
|
|
const key = METHOD_ID_KEYS[deck];
|
|
if (state[key] !== undefined || byDeck.has(deck)) {
|
|
(methodPatch as Record<string, unknown>)[key] = byDeck.get(deck) ?? [];
|
|
}
|
|
}
|
|
|
|
return {
|
|
...state,
|
|
...methodPatch,
|
|
...detailsOut,
|
|
...(state.selectedCoreValueIds
|
|
? { selectedCoreValueIds: coreIds }
|
|
: {}),
|
|
...(snapshot ? { coreValuesChipsSnapshot: snapshot } : {}),
|
|
...(nextCoreDetails ? { coreValueDetailsByChipId: nextCoreDetails } : {}),
|
|
};
|
|
}
|