Final review edit modals created
This commit is contained in:
@@ -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),
|
||||
}));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user