import type { CreateFlowMethodCardFacetSection, CreateFlowState, } from "../../app/(app)/create/types"; import { CUSTOM_RULE_FACETS, PUBLISHED_CUSTOM_RULE_SELECTION_KEYS, } from "./customRuleFacets"; import type { PublishedMethodSelections } from "./buildPublishPayload"; import type { StoredLastPublishedRule } from "./lastPublishedRule"; import { methodLabelFor } from "./finalReviewChipPresets"; import type { TemplateFacetGroupKey } from "./templateReviewMapping"; function customMethodCardMetaFromPublishedSelections( ms: PublishedMethodSelections, ): CreateFlowState["customMethodCardMetaById"] | undefined { const meta: NonNullable = {}; const absorb = ( groupKey: TemplateFacetGroupKey, rows: | Array<{ id: string; label: string; }> | undefined, ) => { if (!rows) return; for (const row of rows) { const id = typeof row.id === "string" ? row.id.trim() : ""; if (!id) continue; if (methodLabelFor(groupKey, id).length > 0) continue; const label = typeof row.label === "string" ? row.label.trim() : ""; if (!label) continue; meta[id] = { label, supportText: "" }; } }; absorb("communication", ms.communication); absorb("membership", ms.membership); absorb("decisionApproaches", ms.decisionApproaches); absorb("conflictManagement", ms.conflictManagement); return Object.keys(meta).length > 0 ? meta : undefined; } /** * True when `patch` (from {@link createFlowStateFromPublishedRule}) expects * non-empty facet selections but `state` still has none for that facet. * * Used so `/create/edit-rule` hydration is not skipped after TopNav **Edit** * pre-clears `sections` (which made `sections?.length === 0` look like a * finished hydrate even though method ids were never merged). */ export function isPublishedRuleSelectionMissing( state: CreateFlowState, patch: Partial, ): boolean { for (const k of PUBLISHED_CUSTOM_RULE_SELECTION_KEYS) { const desired = patch[k]; if (!Array.isArray(desired) || desired.length === 0) continue; const actualRaw = state[k]; const actual = Array.isArray(actualRaw) ? actualRaw : []; if (actual.length === 0) return true; } return false; } /** * True when published-rule hydration should run (or continue) — facet ids still * empty, or {@link createFlowStateFromPublishedRule} produced * `customMethodCardMetaById` for user-authored method UUIDs that `state` does * not have yet (final-review chips use meta + id when no preset label exists). */ export function isPublishedRuleHydratePatchIncomplete( state: CreateFlowState, patch: Partial, ): boolean { if (isPublishedRuleSelectionMissing(state, patch)) return true; const pm = patch.customMethodCardMetaById; if (!pm || Object.keys(pm).length === 0) return false; const sm = state.customMethodCardMetaById ?? {}; for (const key of Object.keys(pm)) { if (!sm[key]) return true; } return false; } /** * Pin flags for method-card facets: persisted for hydration and footer Confirm. * Card Stack display pulls selections to the top whenever `selected*` ids are * non-empty (`useMethodCardDeckOrdering`); this map still merges on edit / * template paths so drafts mirror post-Confirm expectations. * * Caller should spread onto existing `methodSectionsPinCommitted` so unrelated facets stay * as-is (`{ ...prior, ...this }`). */ export function methodSectionsPinsForHydratedSelections( patch: Partial, ): Partial> { const out: Partial> = {}; for (const row of CUSTOM_RULE_FACETS) { if (row.kind !== "method" || row.apiMethodSectionId == null) continue; const sel = patch[row.selectedIdsStateKey]; if (Array.isArray(sel) && sel.length > 0) { out[row.apiMethodSectionId] = true; } } return out; } /** @see {@link methodSectionsPinsForHydratedSelections} — published-rule hydrate naming. */ export function methodSectionsPinsFromPublishedHydratePatch( patch: Partial, ): Partial> { return methodSectionsPinsForHydratedSelections(patch); } /** * Rehydrate create-flow fields from a stored published rule so `/create/edit-rule` * can render final-review editors after refresh or when branching from completed. */ export function createFlowStateFromPublishedRule( rule: StoredLastPublishedRule, ): Partial { const doc = rule.document; const out: Partial = { title: rule.title, editingPublishedRuleId: rule.id, }; const sum = typeof rule.summary === "string" ? rule.summary.trim() : ""; if (sum.length > 0) { out.communityContext = sum; out.summary = sum; } const coreValues = doc.coreValues; if (Array.isArray(coreValues) && coreValues.length > 0) { const selectedCoreValueIds: string[] = []; const coreValuesChipsSnapshot: NonNullable< CreateFlowState["coreValuesChipsSnapshot"] > = []; const coreValueDetailsByChipId: NonNullable< CreateFlowState["coreValueDetailsByChipId"] > = {}; for (const row of coreValues) { if (!row || typeof row !== "object") continue; const o = row as Record; const chipIdRaw = typeof o.chipId === "string" ? o.chipId.trim() : ""; const label = typeof o.label === "string" ? o.label.trim() : ""; if (!label) continue; const chipId = chipIdRaw.length > 0 ? chipIdRaw : `hydrated-${label.toLowerCase()}`; selectedCoreValueIds.push(chipId); coreValuesChipsSnapshot.push({ id: chipId, label, state: "selected", }); coreValueDetailsByChipId[chipId] = { meaning: typeof o.meaning === "string" ? o.meaning : "", signals: typeof o.signals === "string" ? o.signals : "", }; } out.selectedCoreValueIds = selectedCoreValueIds; out.coreValuesChipsSnapshot = coreValuesChipsSnapshot; out.coreValueDetailsByChipId = coreValueDetailsByChipId; } const avatarUrl = typeof doc.communityAvatarUrl === "string" ? doc.communityAvatarUrl.trim() : ""; if (avatarUrl.length > 0) { out.communityAvatarUrl = avatarUrl; } const blocksRaw = doc.customMethodCardFieldBlocksById; if ( blocksRaw && typeof blocksRaw === "object" && !Array.isArray(blocksRaw) ) { out.customMethodCardFieldBlocksById = blocksRaw as NonNullable; } const msRaw = doc.methodSelections; if (!msRaw || typeof msRaw !== "object" || Array.isArray(msRaw)) { out.sections = []; return out; } const ms = msRaw as PublishedMethodSelections; if (Array.isArray(ms.communication) && ms.communication.length > 0) { out.selectedCommunicationMethodIds = ms.communication.map((x) => x.id); out.communicationMethodDetailsById = Object.fromEntries( ms.communication.map((x) => [x.id, x.sections]), ); } if (Array.isArray(ms.membership) && ms.membership.length > 0) { out.selectedMembershipMethodIds = ms.membership.map((x) => x.id); out.membershipMethodDetailsById = Object.fromEntries( ms.membership.map((x) => [x.id, x.sections]), ); } if ( Array.isArray(ms.decisionApproaches) && ms.decisionApproaches.length > 0 ) { out.selectedDecisionApproachIds = ms.decisionApproaches.map((x) => x.id); out.decisionApproachDetailsById = Object.fromEntries( ms.decisionApproaches.map((x) => [x.id, x.sections]), ); } if ( Array.isArray(ms.conflictManagement) && ms.conflictManagement.length > 0 ) { out.selectedConflictManagementIds = ms.conflictManagement.map( (x) => x.id, ); out.conflictManagementDetailsById = Object.fromEntries( ms.conflictManagement.map((x) => [x.id, x.sections]), ); } const customMeta = customMethodCardMetaFromPublishedSelections(ms); if (customMeta) { out.customMethodCardMetaById = customMeta; } /** Drop template `sections` so final-review uses `methodSelections` / selected ids (edit path). */ out.sections = []; return out; }