import type { CommunicationMethodDetailEntry, ConflictManagementDetailEntry, CoreValueDetailEntry, CreateFlowState, DecisionApproachDetailEntry, MembershipMethodDetailEntry, } from "../../app/(app)/create/types"; import type { CustomMethodCardFieldBlock } from "./customMethodCardFieldBlocks"; import { formatConflictApplicableScopeForTextarea } from "./ruleSectionsFromMethodSelections"; /** Seed for {@link CustomMethodCardWizard} when Customize opens on an existing card. */ export type MethodCardWizardInitialValues = { title: string; description: string; fieldBlocks: CustomMethodCardFieldBlock[]; }; export type MethodCardWizardFacetPrefill = | { group: "communication"; draft: CommunicationMethodDetailEntry; headings: { corePrinciple: string; logisticsAdmin: string; codeOfConduct: string; }; } | { group: "membership"; draft: MembershipMethodDetailEntry; headings: { eligibility: string; joiningProcess: string; expectations: string; }; } | { group: "decisionApproaches"; draft: DecisionApproachDetailEntry; headings: { corePrinciple: string; applicableScope: string; stepByStepInstructions: string; consensusLevel: string; objectionsDeadlocks: string; }; } | { group: "conflictManagement"; draft: ConflictManagementDetailEntry; headings: { corePrinciple: string; applicableScope: string; processProtocol: string; restorationFallbacks: string; }; } | { group: "coreValues"; draft: CoreValueDetailEntry; headings: { meaning: string; signals: string; }; }; const TEXT_BODY_MAX = 8000; function textBlock( id: string, blockTitle: string, placeholderText: string, ): CustomMethodCardFieldBlock { return { kind: "text", id, blockTitle, placeholderText: placeholderText.slice(0, TEXT_BODY_MAX), }; } function hasTrimmedText(value: string): boolean { return value.trim().length > 0; } function clampPercent(n: number): number { if (!Number.isFinite(n)) return 1; return Math.min(100, Math.max(1, Math.round(n))); } function facetPrefillHasContent(prefill: MethodCardWizardFacetPrefill): boolean { switch (prefill.group) { case "communication": return ( hasTrimmedText(prefill.draft.corePrinciple) || hasTrimmedText(prefill.draft.logisticsAdmin) || hasTrimmedText(prefill.draft.codeOfConduct) ); case "membership": return ( hasTrimmedText(prefill.draft.eligibility) || hasTrimmedText(prefill.draft.joiningProcess) || hasTrimmedText(prefill.draft.expectations) ); case "decisionApproaches": return ( hasTrimmedText(prefill.draft.corePrinciple) || hasTrimmedText(prefill.draft.stepByStepInstructions) || hasTrimmedText(prefill.draft.objectionsDeadlocks) || prefill.draft.applicableScope.length > 0 || prefill.draft.selectedApplicableScope.length > 0 ); case "conflictManagement": return ( hasTrimmedText(prefill.draft.corePrinciple) || hasTrimmedText(prefill.draft.processProtocol) || hasTrimmedText(prefill.draft.restorationFallbacks) || formatConflictApplicableScopeForTextarea( prefill.draft.selectedApplicableScope, prefill.draft.applicableScope, ).trim().length > 0 ); case "coreValues": return ( hasTrimmedText(prefill.draft.meaning) || hasTrimmedText(prefill.draft.signals) ); } } function mapFacetPrefillToWizardFieldBlocks( prefill: MethodCardWizardFacetPrefill, ): CustomMethodCardFieldBlock[] { switch (prefill.group) { case "communication": return [ textBlock( "facet-corePrinciple", prefill.headings.corePrinciple, prefill.draft.corePrinciple, ), textBlock( "facet-logisticsAdmin", prefill.headings.logisticsAdmin, prefill.draft.logisticsAdmin, ), textBlock( "facet-codeOfConduct", prefill.headings.codeOfConduct, prefill.draft.codeOfConduct, ), ]; case "membership": return [ textBlock( "facet-eligibility", prefill.headings.eligibility, prefill.draft.eligibility, ), textBlock( "facet-joiningProcess", prefill.headings.joiningProcess, prefill.draft.joiningProcess, ), textBlock( "facet-expectations", prefill.headings.expectations, prefill.draft.expectations, ), ]; case "decisionApproaches": { const scopeOptions = prefill.draft.applicableScope.length > 0 ? [...prefill.draft.applicableScope] : [...prefill.draft.selectedApplicableScope]; const blocks: CustomMethodCardFieldBlock[] = [ textBlock( "facet-corePrinciple", prefill.headings.corePrinciple, prefill.draft.corePrinciple, ), ]; if (scopeOptions.length > 0) { blocks.push({ kind: "badges", id: "facet-applicableScope", blockTitle: prefill.headings.applicableScope, options: scopeOptions, }); } blocks.push( textBlock( "facet-stepByStepInstructions", prefill.headings.stepByStepInstructions, prefill.draft.stepByStepInstructions, ), { kind: "proportion", id: "facet-consensusLevel", blockTitle: prefill.headings.consensusLevel, defaultPercent: clampPercent(prefill.draft.consensusLevel), }, textBlock( "facet-objectionsDeadlocks", prefill.headings.objectionsDeadlocks, prefill.draft.objectionsDeadlocks, ), ); return blocks; } case "conflictManagement": return [ textBlock( "facet-corePrinciple", prefill.headings.corePrinciple, prefill.draft.corePrinciple, ), textBlock( "facet-applicableScope", prefill.headings.applicableScope, formatConflictApplicableScopeForTextarea( prefill.draft.selectedApplicableScope, prefill.draft.applicableScope, ), ), textBlock( "facet-processProtocol", prefill.headings.processProtocol, prefill.draft.processProtocol, ), textBlock( "facet-restorationFallbacks", prefill.headings.restorationFallbacks, prefill.draft.restorationFallbacks, ), ]; case "coreValues": return [ textBlock( "facet-meaning", prefill.headings.meaning, prefill.draft.meaning, ), textBlock( "facet-signals", prefill.headings.signals, prefill.draft.signals, ), ]; } } /** * Map a method card's facet editors onto wizard field blocks so Customize * can show and edit the same sections on step 3. */ export function facetDetailsToWizardFieldBlocks( prefill: MethodCardWizardFacetPrefill, ): CustomMethodCardFieldBlock[] { if (!facetPrefillHasContent(prefill)) { return []; } return mapFacetPrefillToWizardFieldBlocks(prefill); } function facetBlockHasContent(block: CustomMethodCardFieldBlock): boolean { switch (block.kind) { case "text": return hasTrimmedText(block.placeholderText); case "badges": return block.options.length > 0; case "proportion": return true; case "upload": return Boolean(block.fileName?.trim() || block.assetUrl?.trim()); } } function overlayBlockValue( existing: CustomMethodCardFieldBlock, incoming: CustomMethodCardFieldBlock, ): CustomMethodCardFieldBlock { if (existing.kind === "text" && incoming.kind === "text") { return { ...existing, placeholderText: incoming.placeholderText }; } if (existing.kind === "badges" && incoming.kind === "badges") { return { ...existing, options: [...incoming.options] }; } if (existing.kind === "proportion" && incoming.kind === "proportion") { return { ...existing, defaultPercent: incoming.defaultPercent }; } if (existing.kind === "upload" && incoming.kind === "upload") { return { ...existing, fileName: incoming.fileName, ...(incoming.assetUrl !== undefined ? { assetUrl: incoming.assetUrl } : {}), }; } return incoming; } /** * Keep extra wizard-only blocks, but refresh facet-* field values from the * current modal editors so Customize does not show a stale copy. */ export function overlayFacetPrefillValues( blocks: CustomMethodCardFieldBlock[], prefill: MethodCardWizardFacetPrefill, ): CustomMethodCardFieldBlock[] { const incoming = mapFacetPrefillToWizardFieldBlocks(prefill); const incomingById = new Map(incoming.map((block) => [block.id, block])); const seen = new Set(); const next = blocks.map((block) => { const overlay = incomingById.get(block.id); if (!overlay) { return block; } seen.add(block.id); return overlayBlockValue(block, overlay); }); for (const block of incoming) { if (seen.has(block.id) || !facetBlockHasContent(block)) { continue; } next.push(block); } return next; } function textPlaceholderForBlockId( blocks: CustomMethodCardFieldBlock[], id: string, ): string | undefined { const block = blocks.find((b) => b.id === id); if (!block || block.kind !== "text") { return undefined; } return block.placeholderText; } /** Map Customize wizard field blocks back onto meaning/signals for a value chip. */ export function coreValueDetailsFromWizardFieldBlocks( blocks: CustomMethodCardFieldBlock[], fallback: CoreValueDetailEntry, ): CoreValueDetailEntry { return { meaning: textPlaceholderForBlockId(blocks, "facet-meaning") ?? fallback.meaning, signals: textPlaceholderForBlockId(blocks, "facet-signals") ?? fallback.signals, ...(fallback.supportText !== undefined ? { supportText: fallback.supportText } : {}), }; } /** * Title, support text, and field blocks for the custom-policy wizard, preferring * in-modal drafts over persisted meta/blocks. When the card has no wizard * blocks yet, facet body fields are mapped onto step-3 blocks. */ export function buildMethodCardWizardInitialValues(args: { cardId: string; fallbackTitle: string; fallbackDescription: string; meta: CreateFlowState["customMethodCardMetaById"]; persistedBlocks: CreateFlowState["customMethodCardFieldBlocksById"]; draftFieldBlocks: CustomMethodCardFieldBlock[] | null; facetPrefill?: MethodCardWizardFacetPrefill; }): MethodCardWizardInitialValues { const metaRow = args.meta?.[args.cardId]; const persisted = args.persistedBlocks?.[args.cardId]; let fieldBlocks = args.draftFieldBlocks !== null ? structuredClone(args.draftFieldBlocks) : Array.isArray(persisted) && persisted.length > 0 ? structuredClone(persisted) : []; if (fieldBlocks.length === 0 && args.facetPrefill) { fieldBlocks = facetDetailsToWizardFieldBlocks(args.facetPrefill); } else if (fieldBlocks.length > 0 && args.facetPrefill) { fieldBlocks = overlayFacetPrefillValues(fieldBlocks, args.facetPrefill); } return { title: metaRow?.label ?? args.fallbackTitle, description: metaRow?.supportText ?? args.fallbackDescription, fieldBlocks, }; }