119 lines
4.1 KiB
TypeScript
119 lines
4.1 KiB
TypeScript
import type { Category } from "../../app/components/cards/Rule";
|
|
import type { ChipOption } from "../../app/components/controls/MultiSelect/MultiSelect.types";
|
|
import type { CommunityRuleSection } from "../../app/components/type/CommunityRule/CommunityRule.types";
|
|
import type { TemplateFacetGroupKey } from "./customRuleFacets";
|
|
import { templateCategoryToFacetGroupKey } from "./customRuleFacets";
|
|
import { isDocumentEntry } from "./documentEntryGuards";
|
|
|
|
export type { TemplateFacetGroupKey } from "./customRuleFacets";
|
|
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* Normalize a section `categoryName` (as it appears in a template's `body`)
|
|
* to the custom-rule facet-group key. Returns `null` for unknown categories.
|
|
* Keys are matched case- and punctuation-insensitively so variations like
|
|
* "Decision making" / "Decision-making" resolve to the same group.
|
|
*/
|
|
export function templateCategoryToGroupKey(
|
|
categoryName: string,
|
|
): TemplateFacetGroupKey | null {
|
|
return templateCategoryToFacetGroupKey(categoryName);
|
|
}
|
|
|
|
/**
|
|
* Detail for a single chip rendered on a template review — includes the raw
|
|
* entry fields plus the facet-group key so a click can open the matching
|
|
* read-only modal (chip `label` is used to look up the preset method inside
|
|
* the group).
|
|
*/
|
|
export interface TemplateChipDetail {
|
|
chipId: string;
|
|
chipLabel: string;
|
|
categoryName: string;
|
|
groupKey: TemplateFacetGroupKey | null;
|
|
body: string;
|
|
}
|
|
|
|
/**
|
|
* Maps API template `body` (published-rule document shape) to Rule category
|
|
* rows **plus** a chipId → detail lookup for wiring chip clicks to the
|
|
* read-only detail modal.
|
|
*/
|
|
export function templateBodyToReviewData(body: unknown): {
|
|
categories: Category[];
|
|
chipDetailsByChipId: Record<string, TemplateChipDetail>;
|
|
} {
|
|
const empty = { categories: [] as Category[], chipDetailsByChipId: {} };
|
|
if (!body || typeof body !== "object") return empty;
|
|
const sections = (body as Record<string, unknown>).sections;
|
|
if (!Array.isArray(sections)) return empty;
|
|
|
|
const categories: Category[] = [];
|
|
const chipDetailsByChipId: Record<string, TemplateChipDetail> = {};
|
|
for (const raw of sections) {
|
|
if (!isDocumentSection(raw)) continue;
|
|
const groupKey = templateCategoryToGroupKey(raw.categoryName);
|
|
const chipOptions: ChipOption[] = raw.entries.map((e, i) => {
|
|
const chipId = `${raw.categoryName}-${i}`;
|
|
chipDetailsByChipId[chipId] = {
|
|
chipId,
|
|
chipLabel: e.title,
|
|
categoryName: raw.categoryName,
|
|
groupKey,
|
|
body: e.body,
|
|
};
|
|
return {
|
|
id: chipId,
|
|
label: e.title,
|
|
state: "unselected",
|
|
};
|
|
});
|
|
categories.push({
|
|
name: raw.categoryName,
|
|
chipOptions,
|
|
});
|
|
}
|
|
return { categories, chipDetailsByChipId };
|
|
}
|
|
|
|
/**
|
|
* Backwards-compatible wrapper kept so existing consumers can still grab just
|
|
* the rows when they don't need chip-click wiring.
|
|
*/
|
|
export function templateBodyToCategories(body: unknown): Category[] {
|
|
return templateBodyToReviewData(body).categories;
|
|
}
|
|
|
|
/**
|
|
* Summary line under tag rows: prefer API description; else first entry bodies (short).
|
|
*/
|
|
export function templateSummaryFromBody(
|
|
description: string | null | undefined,
|
|
body: unknown,
|
|
): string {
|
|
const d = typeof description === "string" ? description.trim() : "";
|
|
if (d.length > 0) return d;
|
|
|
|
if (!body || typeof body !== "object") return "";
|
|
const sections = (body as Record<string, unknown>).sections;
|
|
if (!Array.isArray(sections)) return "";
|
|
for (const s of sections) {
|
|
if (!isDocumentSection(s)) continue;
|
|
const first = s.entries[0];
|
|
if (isDocumentEntry(first)) {
|
|
const main = first.body.trim();
|
|
if (main.length > 0) return main;
|
|
const fromBlock = first.blocks?.[0]?.body?.trim();
|
|
if (fromBlock && fromBlock.length > 0) return fromBlock;
|
|
}
|
|
}
|
|
return "";
|
|
}
|