Final review edit modals created

This commit is contained in:
adilallo
2026-04-20 17:57:17 -06:00
parent c08cd62872
commit a22d53e860
27 changed files with 2410 additions and 620 deletions
+179 -72
View File
@@ -7,6 +7,10 @@ import {
buildCoreValuesForDocument,
parseSectionsFromCreateFlowState,
} from "./buildPublishPayload";
import {
templateCategoryToGroupKey,
type TemplateFacetGroupKey,
} from "./templateReviewMapping";
/**
* Chip row shape shared with `messages/en/create/reviewAndComplete/finalReview.json`
@@ -15,6 +19,31 @@ import {
*/
export type FinalReviewCategoryRow = { name: string; chips: string[] };
/**
* Per-chip details needed to open an *editable* chip modal on the final-review
* screen. `overrideKey` is the stable id we use to look up / write user edits
* in `CreateFlowState`:
*
* - `coreValues` → the chip id from `coreValuesChipsSnapshot`
* (round-trips via `coreValueDetailsByChipId`).
* - Method groups → the preset method id from
* `messages/en/create/customRule/{group}.json` `methods[].id`
* (round-trips via `{group}MethodDetailsById`).
* - Unknown / template-only chips → `null` (modal falls back to read-only).
*/
export type FinalReviewChipEntry = {
label: string;
groupKey: TemplateFacetGroupKey | null;
overrideKey: string | null;
};
/** Detailed row paired with per-chip override + group metadata. */
export type FinalReviewCategoryRowDetailed = {
name: string;
groupKey: TemplateFacetGroupKey | null;
entries: FinalReviewChipEntry[];
};
/** Category labels supplied by the caller (pulled from localized messages). */
export type FinalReviewCategoryNames = {
values: string;
@@ -41,24 +70,166 @@ function readMethodsArray(source: unknown): MethodPreset[] {
return out;
}
function labelsFromIds(
/**
* Resolve an ordered list of preset ids to `{label, id}` entries, filtering
* missing/duplicate labels. The id is returned alongside so callers can key
* per-chip overrides by the stable preset id (e.g. `"signal"`) even after
* labels change through localization.
*/
function entriesFromIds(
ids: readonly string[] | undefined,
methods: readonly MethodPreset[],
): string[] {
groupKey: TemplateFacetGroupKey,
): FinalReviewChipEntry[] {
if (!ids || ids.length === 0) return [];
const byId = new Map(methods.map((m) => [m.id, m.label] as const));
const seen = new Set<string>();
const out: string[] = [];
const out: FinalReviewChipEntry[] = [];
for (const id of ids) {
const label = byId.get(id);
if (typeof label !== "string" || label.length === 0) continue;
if (seen.has(label)) continue;
seen.add(label);
out.push(label);
out.push({ label, groupKey, overrideKey: id });
}
return out;
}
/**
* Reverse-lookup a preset id by label for the template-sections path, where
* chips carry entry titles rather than ids. Used to recover an overrideKey
* for chips the user sees on a "Use without changes" template review.
*/
function overrideKeyForLabel(
label: string,
methods: readonly MethodPreset[],
): string | null {
const normalized = label.trim().toLowerCase();
if (normalized.length === 0) return null;
for (const m of methods) {
if (m.label.trim().toLowerCase() === normalized) return m.id;
}
return null;
}
function methodsForGroup(
groupKey: TemplateFacetGroupKey | null,
): readonly MethodPreset[] {
switch (groupKey) {
case "communication":
return readMethodsArray(communicationMessages);
case "membership":
return readMethodsArray(membershipMessages);
case "decisionApproaches":
return readMethodsArray(decisionApproachesMessages);
case "conflictManagement":
return readMethodsArray(conflictManagementMessages);
default:
return [];
}
}
/**
* Detailed builder: same logic as {@link buildFinalReviewCategoriesFromState}
* but each chip is returned with its `overrideKey` + `groupKey` so the
* final-review screen can wire chip clicks to an editable modal that
* round-trips writes back into `CreateFlowState`.
*/
export function buildFinalReviewCategoryRowsDetailed(
state: CreateFlowState,
names: FinalReviewCategoryNames,
): FinalReviewCategoryRowDetailed[] {
const sections = parseSectionsFromCreateFlowState(state);
const coreValues = buildCoreValuesForDocument(state);
const coreValueEntries: FinalReviewChipEntry[] = coreValues.map((r) => ({
label: r.label,
groupKey: "coreValues" as const,
overrideKey: r.chipId,
}));
// Use-without-changes / pre-rendered template body.
if (sections.length > 0) {
const rows: FinalReviewCategoryRowDetailed[] = [];
// Always prefer the chip-snapshot derived entries when present so the
// values row uses stable per-chip ids the edit modal can attach to.
// We then skip any Values section in the template body to avoid
// duplicating the row (the snapshot already represents the same data).
if (coreValueEntries.length > 0) {
rows.push({
name: names.values,
groupKey: "coreValues",
entries: coreValueEntries,
});
}
for (const s of sections) {
const groupKey = templateCategoryToGroupKey(s.categoryName);
if (groupKey === "coreValues" && coreValueEntries.length > 0) continue;
const methods = methodsForGroup(groupKey);
const entries: FinalReviewChipEntry[] = [];
for (const e of s.entries) {
const title = e.title.trim();
if (title.length === 0) continue;
// For the Values section inside template bodies we can't recover a
// stable chip id (no snapshot), so override is unavailable — the
// modal will render read-only. Method sections fall back to label
// → preset-id resolution so matching titles stay editable.
let overrideKey: string | null = null;
if (groupKey && groupKey !== "coreValues") {
overrideKey = overrideKeyForLabel(title, methods);
}
entries.push({ label: title, groupKey, overrideKey });
}
if (entries.length === 0) continue;
rows.push({ name: s.categoryName, groupKey, entries });
}
return rows;
}
const rows: FinalReviewCategoryRowDetailed[] = [
{ name: names.values, groupKey: "coreValues", entries: coreValueEntries },
{
name: names.communication,
groupKey: "communication",
entries: entriesFromIds(
state.selectedCommunicationMethodIds,
methodsForGroup("communication"),
"communication",
),
},
{
name: names.membership,
groupKey: "membership",
entries: entriesFromIds(
state.selectedMembershipMethodIds,
methodsForGroup("membership"),
"membership",
),
},
{
name: names.decisions,
groupKey: "decisionApproaches",
entries: entriesFromIds(
state.selectedDecisionApproachIds,
methodsForGroup("decisionApproaches"),
"decisionApproaches",
),
},
{
name: names.conflict,
groupKey: "conflictManagement",
entries: entriesFromIds(
state.selectedConflictManagementIds,
methodsForGroup("conflictManagement"),
"conflictManagement",
),
},
];
return rows.filter((r) => r.entries.length > 0);
}
/**
* Derive the final-review RuleCard category rows from the current
* {@link CreateFlowState}.
@@ -81,72 +252,8 @@ export function buildFinalReviewCategoriesFromState(
state: CreateFlowState,
names: FinalReviewCategoryNames,
): FinalReviewCategoryRow[] {
const sections = parseSectionsFromCreateFlowState(state);
const coreValueLabels = buildCoreValuesForDocument(state).map((r) => r.label);
// Use-without-changes / pre-rendered template body: the sections array is
// the source of truth. Collapse each section's entries to its titles; the
// RuleCard category UI shows only labels, not per-entry body copy.
if (sections.length > 0) {
const rows: FinalReviewCategoryRow[] = [];
// If core values were also captured (e.g., the template surfaced both),
// keep them up top for visual parity with the custom-rule flow. Otherwise
// any `Values` section already inside `sections` covers the same ground.
if (coreValueLabels.length > 0) {
const hasValuesSection = sections.some(
(s) => s.categoryName.toLowerCase() === names.values.toLowerCase(),
);
if (!hasValuesSection) {
rows.push({ name: names.values, chips: coreValueLabels });
}
}
for (const s of sections) {
const chips = s.entries
.map((e) => e.title.trim())
.filter((t) => t.length > 0);
if (chips.length === 0) continue;
rows.push({ name: s.categoryName, chips });
}
return rows;
}
const communicationMethods = readMethodsArray(communicationMessages);
const membershipMethods = readMethodsArray(membershipMessages);
const decisionApproachMethods = readMethodsArray(decisionApproachesMessages);
const conflictManagementMethods = readMethodsArray(conflictManagementMessages);
const rows: FinalReviewCategoryRow[] = [
{ name: names.values, chips: coreValueLabels },
{
name: names.communication,
chips: labelsFromIds(
state.selectedCommunicationMethodIds,
communicationMethods,
),
},
{
name: names.membership,
chips: labelsFromIds(
state.selectedMembershipMethodIds,
membershipMethods,
),
},
{
name: names.decisions,
chips: labelsFromIds(
state.selectedDecisionApproachIds,
decisionApproachMethods,
),
},
{
name: names.conflict,
chips: labelsFromIds(
state.selectedConflictManagementIds,
conflictManagementMethods,
),
},
];
return rows.filter((r) => r.chips.length > 0);
return buildFinalReviewCategoryRowsDetailed(state, names).map((r) => ({
name: r.name,
chips: r.entries.map((e) => e.label),
}));
}