365 lines
10 KiB
TypeScript
365 lines
10 KiB
TypeScript
import type {
|
|
CommunicationMethodDetailEntry,
|
|
ConflictManagementDetailEntry,
|
|
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,
|
|
publishedMethodDisplayLabel,
|
|
} from "./finalReviewChipPresets";
|
|
import { isDocumentEntry } from "./documentEntryGuards";
|
|
import { replaceMethodSectionsWithMethodSelections } from "./ruleSectionsFromMethodSelections";
|
|
import { templateCategoryToGroupKey } from "./templateReviewMapping";
|
|
|
|
export { isDocumentEntry } from "./documentEntryGuards";
|
|
|
|
function isDocumentSection(x: unknown): x is CommunityRuleSection {
|
|
if (!x || typeof x !== "object") return false;
|
|
const o = x as Record<string, unknown>;
|
|
if (typeof o.categoryName !== "string") return false;
|
|
if (!Array.isArray(o.entries)) return false;
|
|
return o.entries.every(isDocumentEntry);
|
|
}
|
|
|
|
/** Narrow `CreateFlowState.sections` into Community Rule document sections. */
|
|
export function parseSectionsFromCreateFlowState(
|
|
state: CreateFlowState,
|
|
): CommunityRuleSection[] {
|
|
const raw = state.sections;
|
|
if (!Array.isArray(raw)) return [];
|
|
const out: CommunityRuleSection[] = [];
|
|
for (const x of raw) {
|
|
if (isDocumentSection(x)) out.push(x);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** Core values selected in the flow with labels and detail text for the published document. */
|
|
export function buildCoreValuesForDocument(state: CreateFlowState): Array<{
|
|
chipId: string;
|
|
label: string;
|
|
meaning: string;
|
|
signals: string;
|
|
}> {
|
|
const snap = state.coreValuesChipsSnapshot;
|
|
const selected = new Set(state.selectedCoreValueIds ?? []);
|
|
const details = state.coreValueDetailsByChipId ?? {};
|
|
if (!snap?.length) return [];
|
|
return snap
|
|
.filter((r) => selected.has(r.id))
|
|
.map((r) => {
|
|
const merged = mergeCoreValueDetailWithPresets(r.id, r.label, details[r.id]);
|
|
return {
|
|
chipId: r.id,
|
|
label: r.label,
|
|
meaning: merged.meaning,
|
|
signals: merged.signals,
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Structured per-group method selections emitted into `document.methodSelections`
|
|
* at publish time. Each entry carries the preset id (stable key), display
|
|
* label, and the fully-resolved section payload (override on top of preset).
|
|
* Empty groups are omitted so downstream readers can iterate just the set
|
|
* the author actually picked.
|
|
*/
|
|
export type PublishedMethodSelections = {
|
|
communication?: Array<{
|
|
id: string;
|
|
label: string;
|
|
sections: CommunicationMethodDetailEntry;
|
|
}>;
|
|
membership?: Array<{
|
|
id: string;
|
|
label: string;
|
|
sections: MembershipMethodDetailEntry;
|
|
}>;
|
|
decisionApproaches?: Array<{
|
|
id: string;
|
|
label: string;
|
|
sections: DecisionApproachDetailEntry;
|
|
}>;
|
|
conflictManagement?: Array<{
|
|
id: string;
|
|
label: string;
|
|
sections: ConflictManagementDetailEntry;
|
|
}>;
|
|
};
|
|
|
|
export type BuildPublishPayloadResult =
|
|
| {
|
|
ok: true;
|
|
title: string;
|
|
summary?: string;
|
|
document: Record<string, unknown>;
|
|
}
|
|
| { ok: false; error: string };
|
|
|
|
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.";
|
|
|
|
export function buildPublishPayload(
|
|
state: CreateFlowState,
|
|
): BuildPublishPayloadResult {
|
|
const title = typeof state.title === "string" ? state.title.trim() : "";
|
|
if (!title) {
|
|
return { ok: false, error: "missingCommunityName" };
|
|
}
|
|
|
|
const firstNonEmpty = (...candidates: unknown[]): string | undefined => {
|
|
for (const c of candidates) {
|
|
if (typeof c !== "string") continue;
|
|
const t = c.trim();
|
|
if (t.length > 0) return t;
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
/** 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) {
|
|
const body = summary ?? DEFAULT_FALLBACK_BODY;
|
|
sections = [
|
|
{
|
|
categoryName: FALLBACK_CATEGORY,
|
|
entries: [{ title: "Community", body }],
|
|
},
|
|
];
|
|
}
|
|
|
|
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,
|
|
state.customMethodCardFieldBlocksById,
|
|
);
|
|
}
|
|
|
|
const document: Record<string, unknown> = { sections, coreValues };
|
|
if (hasAnyMethodSelection(methodSelections)) {
|
|
document.methodSelections = methodSelections;
|
|
}
|
|
|
|
const avatar =
|
|
typeof state.communityAvatarUrl === "string" &&
|
|
state.communityAvatarUrl.trim().length > 0
|
|
? state.communityAvatarUrl.trim()
|
|
: undefined;
|
|
if (avatar) {
|
|
document.communityAvatarUrl = avatar;
|
|
}
|
|
|
|
const fieldBlocks = state.customMethodCardFieldBlocksById;
|
|
if (fieldBlocks && Object.keys(fieldBlocks).length > 0) {
|
|
document.customMethodCardFieldBlocksById = fieldBlocks;
|
|
}
|
|
|
|
if (summary !== undefined) {
|
|
return { ok: true, title, summary, document };
|
|
}
|
|
return { ok: true, title, document };
|
|
}
|
|
|
|
function hasAnyMethodSelection(m: PublishedMethodSelections): boolean {
|
|
return Boolean(
|
|
m.communication?.length ||
|
|
m.membership?.length ||
|
|
m.decisionApproaches?.length ||
|
|
m.conflictManagement?.length,
|
|
);
|
|
}
|
|
|
|
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
|
|
* shipped `messages/en/create/customRule/*.json` seed any sub-fields the
|
|
* user didn't edit so consumers of `document.methodSelections` always see
|
|
* a complete payload per method.
|
|
*/
|
|
export function buildMethodSelectionsForDocument(
|
|
state: CreateFlowState,
|
|
): PublishedMethodSelections {
|
|
const derived = deriveMethodPresetIdsFromSections(
|
|
parseSectionsFromCreateFlowState(state),
|
|
);
|
|
const out: PublishedMethodSelections = {};
|
|
|
|
const commIds = pickMethodIds(
|
|
state.selectedCommunicationMethodIds,
|
|
derived.communication,
|
|
);
|
|
if (commIds.length > 0) {
|
|
out.communication = commIds.map((id) => {
|
|
const preset = communicationPresetFor(id);
|
|
const override = state.communicationMethodDetailsById?.[id];
|
|
return {
|
|
id,
|
|
label: publishedMethodDisplayLabel(
|
|
"communication",
|
|
id,
|
|
state.customMethodCardMetaById,
|
|
),
|
|
sections: override ? { ...preset, ...override } : preset,
|
|
};
|
|
});
|
|
}
|
|
|
|
const memIds = pickMethodIds(
|
|
state.selectedMembershipMethodIds,
|
|
derived.membership,
|
|
);
|
|
if (memIds.length > 0) {
|
|
out.membership = memIds.map((id) => {
|
|
const preset = membershipPresetFor(id);
|
|
const override = state.membershipMethodDetailsById?.[id];
|
|
return {
|
|
id,
|
|
label: publishedMethodDisplayLabel(
|
|
"membership",
|
|
id,
|
|
state.customMethodCardMetaById,
|
|
),
|
|
sections: override ? { ...preset, ...override } : preset,
|
|
};
|
|
});
|
|
}
|
|
|
|
const daIds = pickMethodIds(
|
|
state.selectedDecisionApproachIds,
|
|
derived.decisionApproaches,
|
|
);
|
|
if (daIds.length > 0) {
|
|
out.decisionApproaches = daIds.map((id) => {
|
|
const preset = decisionApproachPresetFor(id);
|
|
const override = state.decisionApproachDetailsById?.[id];
|
|
return {
|
|
id,
|
|
label: publishedMethodDisplayLabel(
|
|
"decisionApproaches",
|
|
id,
|
|
state.customMethodCardMetaById,
|
|
),
|
|
sections: override ? { ...preset, ...override } : preset,
|
|
};
|
|
});
|
|
}
|
|
|
|
const cmIds = pickMethodIds(
|
|
state.selectedConflictManagementIds,
|
|
derived.conflictManagement,
|
|
);
|
|
if (cmIds.length > 0) {
|
|
out.conflictManagement = cmIds.map((id) => {
|
|
const preset = conflictManagementPresetFor(id);
|
|
const override = state.conflictManagementDetailsById?.[id];
|
|
return {
|
|
id,
|
|
label: publishedMethodDisplayLabel(
|
|
"conflictManagement",
|
|
id,
|
|
state.customMethodCardMetaById,
|
|
),
|
|
sections: override ? { ...preset, ...override } : preset,
|
|
};
|
|
});
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
/** Read `document.sections` from a stored published payload for display. */
|
|
export function parseDocumentSectionsForDisplay(
|
|
document: unknown,
|
|
): CommunityRuleSection[] {
|
|
if (!document || typeof document !== "object") return [];
|
|
const sections = (document as Record<string, unknown>).sections;
|
|
if (!Array.isArray(sections)) return [];
|
|
return sections
|
|
.filter(isDocumentSection)
|
|
.map((section) => ({
|
|
...section,
|
|
entries: section.entries.map((entry) => ({
|
|
...entry,
|
|
body: typeof entry.body === "string" ? entry.body : "",
|
|
})),
|
|
}));
|
|
}
|