Show the whole Rule on one review card, with quiet flags for missing cards and Continue once each question has one.

This commit is contained in:
adilallo committed 2026-11-06 08:36:34 -07:00
1 parent cc878e3c19
commit 6e2e1915ae
30 files changed
+571 -218

No files matched your search

+54 -5
View File
@@ -50,6 +50,56 @@ export type FinalReviewCategoryNames = {
conflict: string;
};
const CANONICAL_FACET_ORDER: readonly TemplateFacetGroupKey[] = [
"coreValues",
"communication",
"membership",
"decisionApproaches",
"conflictManagement",
] as const;
function nameForGroup(
groupKey: TemplateFacetGroupKey,
names: FinalReviewCategoryNames,
): string {
switch (groupKey) {
case "coreValues":
return names.values;
case "communication":
return names.communication;
case "membership":
return names.membership;
case "decisionApproaches":
return names.decisions;
case "conflictManagement":
return names.conflict;
}
}
/** Always five section rows, in values-first order, including empty ones. */
export function ensureCanonicalFinalReviewRows(
rows: readonly FinalReviewCategoryRowDetailed[],
names: FinalReviewCategoryNames,
): FinalReviewCategoryRowDetailed[] {
const byGroup = new Map<TemplateFacetGroupKey, FinalReviewCategoryRowDetailed>();
for (const row of rows) {
if (row.groupKey == null) continue;
byGroup.set(row.groupKey, {
...row,
name: nameForGroup(row.groupKey, names),
});
}
return CANONICAL_FACET_ORDER.map((groupKey) => {
const existing = byGroup.get(groupKey);
if (existing) return existing;
return {
name: nameForGroup(groupKey, names),
groupKey,
entries: [],
};
});
}
type MethodPreset = { id: string; label: string };
/**
@@ -230,7 +280,7 @@ export function buildFinalReviewCategoryRowsDetailed(
if (entries.length === 0) continue;
rows.push({ name: s.categoryName, groupKey, entries });
}
return rows;
return ensureCanonicalFinalReviewRows(rows, names);
}
const rows: FinalReviewCategoryRowDetailed[] = [
@@ -276,7 +326,7 @@ export function buildFinalReviewCategoryRowsDetailed(
),
},
];
return rows.filter((r) => r.entries.length > 0);
return ensureCanonicalFinalReviewRows(rows, names);
}
/**
@@ -293,9 +343,8 @@ export function buildFinalReviewCategoryRowsDetailed(
* against the curated message `methods[]` list to get the display labels,
* matching what the user saw as chips in-flow.
*
* Empty categories are filtered out so the review card doesn't render headings
* with no chips. If nothing in state resolves to any chip, the caller should
* fall back to the demo categories shipped in `finalReview.json`.
* Empty categories stay on the card so add chips and flags can show. The five
* named rows always render, in values-first order.
*/
export function buildFinalReviewCategoriesFromState(
state: CreateFlowState,
+7
View File
@@ -0,0 +1,7 @@
/** One or two letters from a community name for the Rule card mark. */
export function communityInitialsFromTitle(title: string): string {
const parts = title.trim().split(/\s+/).filter(Boolean);
if (parts.length === 0) return "";
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
return `${parts[0][0] ?? ""}${parts[1][0] ?? ""}`.toUpperCase();
}
+90
View File
@@ -0,0 +1,90 @@
import type {
CommunicationMethodDetailEntry,
ConflictManagementDetailEntry,
CreateFlowState,
DecisionApproachDetailEntry,
MembershipMethodDetailEntry,
} from "../../app/(app)/create/types";
import type { TemplateFacetGroupKey } from "./templateReviewMapping";
function missingText(...values: readonly (string | undefined)[]): boolean {
return values.some((value) => (value ?? "").trim().length === 0);
}
function communicationMissingWords(
saved: CommunicationMethodDetailEntry | undefined,
): boolean {
if (!saved) return true;
return missingText(
saved.corePrinciple,
saved.logisticsAdmin,
saved.codeOfConduct,
);
}
function membershipMissingWords(
saved: MembershipMethodDetailEntry | undefined,
): boolean {
if (!saved) return true;
return missingText(
saved.eligibility,
saved.joiningProcess,
saved.expectations,
);
}
function decisionMissingWords(
saved: DecisionApproachDetailEntry | undefined,
): boolean {
if (!saved) return true;
return missingText(
saved.corePrinciple,
saved.stepByStepInstructions,
saved.objectionsDeadlocks,
);
}
function conflictMissingWords(
saved: ConflictManagementDetailEntry | undefined,
): boolean {
if (!saved) return true;
return missingText(
saved.corePrinciple,
saved.processProtocol,
saved.restorationFallbacks,
);
}
/**
* A review chip shows a warning when its value is missing meaning or signals,
* or a card is missing its words.
*/
export function finalReviewChipHasWarning(
state: CreateFlowState,
groupKey: TemplateFacetGroupKey | null,
overrideKey: string | null,
): boolean {
if (!groupKey || !overrideKey) return true;
switch (groupKey) {
case "coreValues": {
const saved = state.coreValueDetailsByChipId?.[overrideKey];
return missingText(saved?.meaning, saved?.signals);
}
case "communication":
return communicationMissingWords(
state.communicationMethodDetailsById?.[overrideKey],
);
case "membership":
return membershipMissingWords(
state.membershipMethodDetailsById?.[overrideKey],
);
case "decisionApproaches":
return decisionMissingWords(
state.decisionApproachDetailsById?.[overrideKey],
);
case "conflictManagement":
return conflictMissingWords(
state.conflictManagementDetailsById?.[overrideKey],
);
}
}