Persist choices through to completed page

This commit is contained in:
adilallo
2026-04-29 15:02:47 -06:00
parent 815de2fdfd
commit ac1157a172
13 changed files with 804 additions and 30 deletions
+12
View File
@@ -129,6 +129,18 @@ function methodsForGroup(
}
}
/**
* Resolve a preset method id from a chip label (template sections / display
* enrichment where entries carry titles but not stable ids).
*/
export function resolveMethodPresetIdFromLabel(
label: string,
groupKey: TemplateFacetGroupKey,
): string | null {
if (groupKey === "coreValues") return null;
return overrideKeyForLabel(label, methodsForGroup(groupKey));
}
/**
* Detailed builder: same logic as {@link buildFinalReviewCategoriesFromState}
* but each chip is returned with its `overrideKey` + `groupKey` so the
+94 -10
View File
@@ -1,20 +1,23 @@
import type {
CommunicationMethodDetailEntry,
ConflictManagementDetailEntry,
CoreValueDetailEntry,
CreateFlowState,
DecisionApproachDetailEntry,
MembershipMethodDetailEntry,
} from "../../app/(app)/create/types";
import type { CommunityRuleSection } from "../../app/components/type/CommunityRule/CommunityRule.types";
import { resolveMethodPresetIdFromLabel } from "./buildFinalReviewCategories";
import {
communicationPresetFor,
conflictManagementPresetFor,
decisionApproachPresetFor,
membershipPresetFor,
mergeCoreValueDetailWithPresets,
methodLabelFor,
} from "./finalReviewChipPresets";
import { isDocumentEntry } from "./documentEntryGuards";
import { replaceMethodSectionsWithMethodSelections } from "./ruleSectionsFromMethodSelections";
import { templateCategoryToGroupKey } from "./templateReviewMapping";
export { isDocumentEntry } from "./documentEntryGuards";
@@ -53,12 +56,12 @@ export function buildCoreValuesForDocument(state: CreateFlowState): Array<{
return snap
.filter((r) => selected.has(r.id))
.map((r) => {
const d: CoreValueDetailEntry | undefined = details[r.id];
const merged = mergeCoreValueDetailWithPresets(r.id, r.label, details[r.id]);
return {
chipId: r.id,
label: r.label,
meaning: d?.meaning ?? "",
signals: d?.signals ?? "",
meaning: merged.meaning,
signals: merged.signals,
};
});
}
@@ -102,7 +105,9 @@ export type BuildPublishPayloadResult =
}
| { ok: false; error: string };
const FALLBACK_CATEGORY = "Overview";
export const PUBLISH_FALLBACK_OVERVIEW_CATEGORY = "Overview";
const FALLBACK_CATEGORY = PUBLISH_FALLBACK_OVERVIEW_CATEGORY;
const DEFAULT_FALLBACK_BODY =
"This CommunityRule was created in the create flow. Add more detail in a future edit.";
@@ -124,7 +129,8 @@ export function buildPublishPayload(
return undefined;
};
let summary = firstNonEmpty(state.summary, state.communityContext);
/** Community context wins over `summary` (template review no longer copies template description into `summary`). */
let summary = firstNonEmpty(state.communityContext, state.summary);
let sections = parseSectionsFromCreateFlowState(state);
if (sections.length === 0) {
@@ -138,8 +144,18 @@ export function buildPublishPayload(
}
const coreValues = buildCoreValuesForDocument(state);
if (coreValues.length > 0) {
sections = sections.filter(
(s) => templateCategoryToGroupKey(s.categoryName) !== "coreValues",
);
}
const methodSelections = buildMethodSelectionsForDocument(state);
if (hasAnyMethodSelection(methodSelections)) {
sections = replaceMethodSectionsWithMethodSelections(sections, methodSelections);
}
const document: Record<string, unknown> = { sections, coreValues };
if (hasAnyMethodSelection(methodSelections)) {
document.methodSelections = methodSelections;
@@ -160,6 +176,59 @@ function hasAnyMethodSelection(m: PublishedMethodSelections): boolean {
);
}
function deriveMethodPresetIdsFromSections(
sections: CommunityRuleSection[],
): {
communication: string[];
membership: string[];
decisionApproaches: string[];
conflictManagement: string[];
} {
const out = {
communication: [] as string[],
membership: [] as string[],
decisionApproaches: [] as string[],
conflictManagement: [] as string[],
};
for (const s of sections) {
const gk = templateCategoryToGroupKey(s.categoryName);
if (!gk || gk === "coreValues") continue;
const ids: string[] = [];
for (const e of s.entries) {
const title = typeof e.title === "string" ? e.title.trim() : "";
if (title.length === 0) continue;
const id = resolveMethodPresetIdFromLabel(title, gk);
if (id) ids.push(id);
}
if (ids.length === 0) continue;
switch (gk) {
case "communication":
out.communication = ids;
break;
case "membership":
out.membership = ids;
break;
case "decisionApproaches":
out.decisionApproaches = ids;
break;
case "conflictManagement":
out.conflictManagement = ids;
break;
default:
break;
}
}
return out;
}
function pickMethodIds(
fromState: string[] | undefined,
derived: string[],
): string[] {
if (fromState && fromState.length > 0) return fromState;
return derived;
}
/**
* Merge `selected*MethodIds` with any saved `{group}MethodDetailsById`
* overrides authored on the final-review screen. Preset defaults from the
@@ -170,9 +239,15 @@ function hasAnyMethodSelection(m: PublishedMethodSelections): boolean {
export function buildMethodSelectionsForDocument(
state: CreateFlowState,
): PublishedMethodSelections {
const derived = deriveMethodPresetIdsFromSections(
parseSectionsFromCreateFlowState(state),
);
const out: PublishedMethodSelections = {};
const commIds = state.selectedCommunicationMethodIds ?? [];
const commIds = pickMethodIds(
state.selectedCommunicationMethodIds,
derived.communication,
);
if (commIds.length > 0) {
out.communication = commIds.map((id) => {
const preset = communicationPresetFor(id);
@@ -185,7 +260,10 @@ export function buildMethodSelectionsForDocument(
});
}
const memIds = state.selectedMembershipMethodIds ?? [];
const memIds = pickMethodIds(
state.selectedMembershipMethodIds,
derived.membership,
);
if (memIds.length > 0) {
out.membership = memIds.map((id) => {
const preset = membershipPresetFor(id);
@@ -198,7 +276,10 @@ export function buildMethodSelectionsForDocument(
});
}
const daIds = state.selectedDecisionApproachIds ?? [];
const daIds = pickMethodIds(
state.selectedDecisionApproachIds,
derived.decisionApproaches,
);
if (daIds.length > 0) {
out.decisionApproaches = daIds.map((id) => {
const preset = decisionApproachPresetFor(id);
@@ -211,7 +292,10 @@ export function buildMethodSelectionsForDocument(
});
}
const cmIds = state.selectedConflictManagementIds ?? [];
const cmIds = pickMethodIds(
state.selectedConflictManagementIds,
derived.conflictManagement,
);
if (cmIds.length > 0) {
out.conflictManagement = cmIds.map((id) => {
const preset = conflictManagementPresetFor(id);
+44
View File
@@ -169,6 +169,50 @@ export function coreValuePresetFor(chipId: string): CoreValueDetailEntry {
};
}
/** Match `coreValues.json` row by trimmed label (custom chip id / drift fallbacks). */
export function coreValuePresetForLabel(label: string): CoreValueDetailEntry {
const t = label.trim();
if (!t) return { meaning: "", signals: "" };
const values = (coreValuesMessages as { values?: unknown }).values;
if (!Array.isArray(values)) return { meaning: "", signals: "" };
for (const row of values) {
if (typeof row === "string") {
if (row.trim() === t) return { meaning: "", signals: "" };
continue;
}
if (!row || typeof row !== "object") continue;
const o = row as Record<string, unknown>;
if (typeof o.label === "string" && o.label.trim() === t) {
return {
meaning: asString(o.meaning),
signals: asString(o.signals),
};
}
}
return { meaning: "", signals: "" };
}
/**
* Published / display copy: saved draft wins when non-empty; otherwise preset
* by chip id (numeric presets), then by label match in `coreValues.json`.
*/
export function mergeCoreValueDetailWithPresets(
chipId: string,
label: string,
saved: CoreValueDetailEntry | undefined,
): CoreValueDetailEntry {
const savedMeaning =
typeof saved?.meaning === "string" ? saved.meaning.trim() : "";
const savedSignals =
typeof saved?.signals === "string" ? saved.signals.trim() : "";
const fromId = coreValuePresetFor(chipId);
const fromLabel = coreValuePresetForLabel(label);
return {
meaning: savedMeaning || fromId.meaning || fromLabel.meaning,
signals: savedSignals || fromId.signals || fromLabel.signals,
};
}
/** Resolve method preset label by id for a given group (localized display). */
export function methodLabelFor(
groupKey: TemplateFacetGroupKey,
@@ -0,0 +1,282 @@
import type {
CommunityRuleEntry,
CommunityRuleSection,
} from "../../app/components/type/CommunityRule/CommunityRule.types";
import type { PublishedMethodSelections } from "./buildPublishPayload";
import {
PUBLISH_FALLBACK_OVERVIEW_CATEGORY,
parseDocumentSectionsForDisplay,
} from "./buildPublishPayload";
import { resolveMethodPresetIdFromLabel } from "./buildFinalReviewCategories";
import {
communicationPresetFor,
conflictManagementPresetFor,
decisionApproachPresetFor,
membershipPresetFor,
mergeCoreValueDetailWithPresets,
} from "./finalReviewChipPresets";
import {
communityRuleEntryFromMethodChip,
formatScopePayload,
nonEmptyTrimmed,
RULE_SECTION_CATEGORY,
sectionFromCommunication,
sectionFromConflict,
sectionFromDecision,
sectionFromMembership,
} from "./ruleSectionsFromMethodSelections";
import {
templateCategoryToGroupKey,
type TemplateFacetGroupKey,
} from "./templateReviewMapping";
/** Legacy seed placeholder (removed from `prisma/seed.ts`); still hydrate older published rows. */
const TEMPLATE_COMPOSITION_SUGGESTED_BODY =
"Suggested focus for this governance area. Replace with your own language in the create flow.";
const CAT_VALUES = RULE_SECTION_CATEGORY.values;
const CAT_COMMUNICATION = RULE_SECTION_CATEGORY.communication;
const CAT_MEMBERSHIP = RULE_SECTION_CATEGORY.membership;
const CAT_DECISION = RULE_SECTION_CATEGORY.decisionMaking;
const CAT_CONFLICT = RULE_SECTION_CATEGORY.conflictManagement;
const CANONICAL_DISPLAY_SECTION_ORDER = [
CAT_VALUES,
CAT_COMMUNICATION,
CAT_MEMBERSHIP,
CAT_DECISION,
CAT_CONFLICT,
] as const;
const COMM_LABELS: Record<string, string> = {
corePrinciple: "Core Principle & Scope",
logisticsAdmin: "Logistics, Admin & Norms",
codeOfConduct: "Code of Conduct",
};
const MEM_LABELS: Record<string, string> = {
eligibility: "Eligibility & Philosophy",
joiningProcess: "Joining Process",
expectations: "Expectations & Removal",
};
const DEC_LABELS: Record<string, string> = {
corePrinciple: "Core Principle",
applicableScope: "Applicable Scope",
stepByStepInstructions: "Step-by-Step Instructions",
consensusLevel: "Consensus Level",
objectionsDeadlocks: "Objections & Deadlocks",
};
const CM_LABELS: Record<string, string> = {
corePrinciple: "Core Principle",
applicableScope: "Applicable Scope",
processProtocol: "Process Protocol",
restorationFallbacks: "Restoration & Fallbacks",
};
function needsPlaceholderPresetEnrichment(entry: CommunityRuleEntry): boolean {
if (entry.blocks && entry.blocks.length > 0) return false;
const b = (entry.body ?? "").trim();
if (b.length === 0) return true;
return b === TEMPLATE_COMPOSITION_SUGGESTED_BODY;
}
function presetRecordForMethodGroup(
groupKey: Exclude<TemplateFacetGroupKey, "coreValues">,
id: string,
): Record<string, unknown> {
switch (groupKey) {
case "communication":
return { ...communicationPresetFor(id) } as Record<string, unknown>;
case "membership":
return { ...membershipPresetFor(id) } as Record<string, unknown>;
case "decisionApproaches":
return { ...decisionApproachPresetFor(id) } as Record<string, unknown>;
case "conflictManagement":
return { ...conflictManagementPresetFor(id) } as Record<string, unknown>;
}
}
function enrichMethodEntryIfPlaceholder(
entry: CommunityRuleEntry,
categoryName: string,
): CommunityRuleEntry {
const groupKey = templateCategoryToGroupKey(categoryName);
if (!groupKey || groupKey === "coreValues") return entry;
if (!needsPlaceholderPresetEnrichment(entry)) return entry;
const id = resolveMethodPresetIdFromLabel(entry.title, groupKey);
if (!id) return entry;
const record = presetRecordForMethodGroup(groupKey, id);
const merged: Record<string, unknown> = { ...record };
if (groupKey === "decisionApproaches" || groupKey === "conflictManagement") {
const scope =
formatScopePayload(merged.selectedApplicableScope) ??
formatScopePayload(merged.applicableScope);
if (scope) merged.applicableScope = scope;
delete merged.selectedApplicableScope;
}
const labelByKey =
groupKey === "communication"
? COMM_LABELS
: groupKey === "membership"
? MEM_LABELS
: groupKey === "decisionApproaches"
? DEC_LABELS
: CM_LABELS;
const options =
groupKey === "decisionApproaches"
? { consensusLevelKey: "consensusLevel" as const }
: undefined;
const enriched = communityRuleEntryFromMethodChip(
entry.title,
merged,
labelByKey,
options,
);
return enriched ?? entry;
}
function enrichCoreValueEntryIfPlaceholder(
entry: CommunityRuleEntry,
): CommunityRuleEntry {
if (!needsPlaceholderPresetEnrichment(entry)) return entry;
const merged = mergeCoreValueDetailWithPresets("", entry.title, {
meaning: "",
signals: "",
});
const meaning = (merged.meaning ?? "").trim();
const signals = (merged.signals ?? "").trim();
const bodyParts: string[] = [];
if (meaning.length > 0) bodyParts.push(meaning);
if (signals.length > 0) bodyParts.push(signals);
const body = bodyParts.join("\n\n");
if (body.length === 0) return entry;
return { ...entry, body };
}
function enrichDisplaySection(section: CommunityRuleSection): CommunityRuleSection {
const groupKey = templateCategoryToGroupKey(section.categoryName);
if (groupKey === "coreValues") {
return {
...section,
entries: section.entries.map(enrichCoreValueEntryIfPlaceholder),
};
}
return {
...section,
entries: section.entries.map((e) =>
enrichMethodEntryIfPlaceholder(e, section.categoryName),
),
};
}
function sortSectionsCanonical(
sections: CommunityRuleSection[],
): CommunityRuleSection[] {
const order = CANONICAL_DISPLAY_SECTION_ORDER as readonly string[];
const rank = (name: string): number => {
const i = order.indexOf(name);
return i === -1 ? order.length : i;
};
return [...sections].sort((a, b) => {
const d = rank(a.categoryName) - rank(b.categoryName);
if (d !== 0) return d;
return a.categoryName.localeCompare(b.categoryName);
});
}
function sectionFromStoredCoreValues(
raw: unknown,
): CommunityRuleSection | null {
if (!Array.isArray(raw) || raw.length === 0) return null;
const entries: CommunityRuleEntry[] = [];
for (const row of raw) {
if (!row || typeof row !== "object") continue;
const o = row as Record<string, unknown>;
const chipId = typeof o.chipId === "string" ? o.chipId : "";
const label = nonEmptyTrimmed(o.label);
if (!label) continue;
const merged = mergeCoreValueDetailWithPresets(chipId, label, {
meaning: typeof o.meaning === "string" ? o.meaning : "",
signals: typeof o.signals === "string" ? o.signals : "",
});
const meaning = (merged.meaning ?? "").trim();
const signals = (merged.signals ?? "").trim();
const bodyParts: string[] = [];
if (meaning.length > 0) bodyParts.push(meaning);
if (signals.length > 0) bodyParts.push(signals);
const body = bodyParts.join("\n\n");
entries.push({ title: label, body });
}
if (entries.length === 0) return null;
return { categoryName: CAT_VALUES, entries };
}
function parseMethodSelectionsLoose(
document: Record<string, unknown>,
): PublishedMethodSelections | null {
const ms = document.methodSelections;
if (!ms || typeof ms !== "object" || Array.isArray(ms)) return null;
return ms as PublishedMethodSelections;
}
/**
* Full `CommunityRule` sections for a published `document` JSON blob: validated
* `document.sections` plus synthesized categories from `document.coreValues` and
* `document.methodSelections` when those categories are not already present.
* **Overview** sections (see `PUBLISH_FALLBACK_OVERVIEW_CATEGORY` in `buildPublishPayload`) from the publish fallback are dropped so the lockup
* header is the only intro; core value copy is the combined meaning + signals **body**
* under each value **title** (chip label).
*/
export function parsePublishedDocumentForCommunityRuleDisplay(
document: unknown,
): CommunityRuleSection[] {
if (!document || typeof document !== "object") return [];
const doc = document as Record<string, unknown>;
const hasPublishedCoreValues =
Array.isArray(doc.coreValues) && doc.coreValues.length > 0;
const base = parseDocumentSectionsForDisplay(doc).filter(
(s) =>
s.categoryName !== PUBLISH_FALLBACK_OVERVIEW_CATEGORY &&
!(hasPublishedCoreValues && s.categoryName === CAT_VALUES),
);
const seen = new Set(base.map((s) => s.categoryName));
const extra: CommunityRuleSection[] = [];
const valuesSection = sectionFromStoredCoreValues(doc.coreValues);
if (valuesSection && !seen.has(valuesSection.categoryName)) {
extra.push(valuesSection);
seen.add(valuesSection.categoryName);
}
const methodSelections = parseMethodSelectionsLoose(doc);
if (methodSelections) {
const comm = sectionFromCommunication(methodSelections.communication ?? []);
if (comm && !seen.has(comm.categoryName)) {
extra.push(comm);
seen.add(comm.categoryName);
}
const mem = sectionFromMembership(methodSelections.membership ?? []);
if (mem && !seen.has(mem.categoryName)) {
extra.push(mem);
seen.add(mem.categoryName);
}
const dec = sectionFromDecision(methodSelections.decisionApproaches ?? []);
if (dec && !seen.has(dec.categoryName)) {
extra.push(dec);
seen.add(dec.categoryName);
}
const cm = sectionFromConflict(methodSelections.conflictManagement ?? []);
if (cm && !seen.has(cm.categoryName)) {
extra.push(cm);
seen.add(cm.categoryName);
}
}
const combined = [...base, ...extra].map(enrichDisplaySection);
return sortSectionsCanonical(combined);
}
@@ -0,0 +1,194 @@
import type {
CommunityRuleEntry,
CommunityRuleLabeledBlock,
CommunityRuleSection,
} from "../../app/components/type/CommunityRule/CommunityRule.types";
import type { PublishedMethodSelections } from "./buildPublishPayload";
import { templateCategoryToGroupKey } from "./templateReviewMapping";
/** Canonical `categoryName` strings for method groups in published documents. */
export const RULE_SECTION_CATEGORY = {
values: "Values",
communication: "Communication",
membership: "Membership",
decisionMaking: "Decision-making",
conflictManagement: "Conflict management",
} as const;
const COMM_LABELS: Record<string, string> = {
corePrinciple: "Core Principle & Scope",
logisticsAdmin: "Logistics, Admin & Norms",
codeOfConduct: "Code of Conduct",
};
const MEM_LABELS: Record<string, string> = {
eligibility: "Eligibility & Philosophy",
joiningProcess: "Joining Process",
expectations: "Expectations & Removal",
};
const DEC_LABELS: Record<string, string> = {
corePrinciple: "Core Principle",
applicableScope: "Applicable Scope",
stepByStepInstructions: "Step-by-Step Instructions",
consensusLevel: "Consensus Level",
objectionsDeadlocks: "Objections & Deadlocks",
};
const CM_LABELS: Record<string, string> = {
corePrinciple: "Core Principle",
applicableScope: "Applicable Scope",
processProtocol: "Process Protocol",
restorationFallbacks: "Restoration & Fallbacks",
};
export function nonEmptyTrimmed(s: unknown): string | null {
if (typeof s !== "string") return null;
const t = s.trim();
return t.length > 0 ? t : null;
}
export function formatScopePayload(val: unknown): string | null {
if (typeof val === "string") return nonEmptyTrimmed(val);
if (!Array.isArray(val)) return null;
const lines = val.filter((x): x is string => typeof x === "string" && x.trim().length > 0);
if (lines.length === 0) return null;
return lines.join("\n");
}
export function blocksFromKeyedRecord(
sections: Record<string, unknown>,
labelByKey: Record<string, string>,
options?: {
consensusLevelKey?: string;
},
): CommunityRuleLabeledBlock[] {
const blocks: CommunityRuleLabeledBlock[] = [];
for (const [key, label] of Object.entries(labelByKey)) {
if (options?.consensusLevelKey === key) {
const n = sections[key];
if (typeof n === "number" && !Number.isNaN(n)) {
blocks.push({ label, body: `${n}%` });
}
continue;
}
const raw = sections[key];
const text =
key === "applicableScope" || key === "selectedApplicableScope"
? formatScopePayload(raw)
: nonEmptyTrimmed(raw);
if (text) blocks.push({ label, body: text });
}
return blocks;
}
export function communityRuleEntryFromMethodChip(
title: string,
sections: Record<string, unknown>,
labelByKey: Record<string, string>,
options?: { consensusLevelKey?: string },
): CommunityRuleEntry | null {
const blocks = blocksFromKeyedRecord(sections, labelByKey, options);
if (blocks.length === 0) return null;
return { title, body: "", blocks };
}
export function sectionFromCommunication(
ms: NonNullable<PublishedMethodSelections["communication"]>,
): CommunityRuleSection | null {
if (ms.length === 0) return null;
const entries: CommunityRuleEntry[] = [];
for (const m of ms) {
const sec = m.sections as unknown as Record<string, unknown>;
const e = communityRuleEntryFromMethodChip(m.label, sec, COMM_LABELS);
if (e) entries.push(e);
}
return entries.length > 0
? { categoryName: RULE_SECTION_CATEGORY.communication, entries }
: null;
}
export function sectionFromMembership(
ms: NonNullable<PublishedMethodSelections["membership"]>,
): CommunityRuleSection | null {
if (ms.length === 0) return null;
const entries: CommunityRuleEntry[] = [];
for (const m of ms) {
const sec = m.sections as unknown as Record<string, unknown>;
const e = communityRuleEntryFromMethodChip(m.label, sec, MEM_LABELS);
if (e) entries.push(e);
}
return entries.length > 0
? { categoryName: RULE_SECTION_CATEGORY.membership, entries }
: null;
}
export function sectionFromDecision(
ms: NonNullable<PublishedMethodSelections["decisionApproaches"]>,
): CommunityRuleSection | null {
if (ms.length === 0) return null;
const entries: CommunityRuleEntry[] = [];
for (const m of ms) {
const sec = m.sections as unknown as Record<string, unknown>;
const merged: Record<string, unknown> = { ...sec };
const scope =
formatScopePayload(sec.selectedApplicableScope) ??
formatScopePayload(sec.applicableScope);
if (scope) merged.applicableScope = scope;
delete merged.selectedApplicableScope;
const e = communityRuleEntryFromMethodChip(m.label, merged, DEC_LABELS, {
consensusLevelKey: "consensusLevel",
});
if (e) entries.push(e);
}
return entries.length > 0
? { categoryName: RULE_SECTION_CATEGORY.decisionMaking, entries }
: null;
}
export function sectionFromConflict(
ms: NonNullable<PublishedMethodSelections["conflictManagement"]>,
): CommunityRuleSection | null {
if (ms.length === 0) return null;
const entries: CommunityRuleEntry[] = [];
for (const m of ms) {
const sec = m.sections as unknown as Record<string, unknown>;
const merged: Record<string, unknown> = { ...sec };
const scope =
formatScopePayload(sec.selectedApplicableScope) ??
formatScopePayload(sec.applicableScope);
if (scope) merged.applicableScope = scope;
delete merged.selectedApplicableScope;
const e = communityRuleEntryFromMethodChip(m.label, merged, CM_LABELS);
if (e) entries.push(e);
}
return entries.length > 0
? { categoryName: RULE_SECTION_CATEGORY.conflictManagement, entries }
: null;
}
/**
* Swap template `sections` method rows for fully-resolved entries built from
* `methodSelections` (preset + overrides).
*/
export function replaceMethodSectionsWithMethodSelections(
sections: CommunityRuleSection[],
ms: PublishedMethodSelections,
): CommunityRuleSection[] {
return sections.map((s) => {
const gk = templateCategoryToGroupKey(s.categoryName);
if (gk === "communication" && ms.communication?.length) {
return sectionFromCommunication(ms.communication) ?? s;
}
if (gk === "membership" && ms.membership?.length) {
return sectionFromMembership(ms.membership) ?? s;
}
if (gk === "decisionApproaches" && ms.decisionApproaches?.length) {
return sectionFromDecision(ms.decisionApproaches) ?? s;
}
if (gk === "conflictManagement" && ms.conflictManagement?.length) {
return sectionFromConflict(ms.conflictManagement) ?? s;
}
return s;
});
}