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

@@ -14,8 +14,11 @@ import {
} from "../../components/CreateFlowLockupCardStepShell";
import {
buildFinalReviewCategoryRowsDetailed,
type FinalReviewCategoryNames,
type FinalReviewCategoryRowDetailed,
} from "../../../../../lib/create/buildFinalReviewCategories";
import { communityInitialsFromTitle } from "../../../../../lib/create/communityInitials";
import { finalReviewChipHasWarning } from "../../../../../lib/create/finalReviewChipWarnings";
import { applyFinalReviewChipEditPatch } from "../../../../../lib/create/applyFinalReviewChipEditPatch";
import type {
TemplateChipDetail,
@@ -32,10 +35,6 @@ import { useCommunityAvatarDisplayUrl } from "../../hooks/useCommunityAvatarDisp
import { usePreviewSurfaceColor } from "../../hooks/usePreviewSurfaceColor";
import { useCreateFlowNavigation } from "../../hooks/useCreateFlowNavigation";
import { createFlowStepForFacetGroup } from "../../utils/facetGroupToCreateFlowStep";
import {
getAssetPath,
vectorMarkPath,
} from "../../../../../lib/assetUtils";
import { isPreviewSurfaceColor } from "../../../../../lib/create/previewSurfaceColor";
import {
persistDraftPreviewSurfaceColor,
@@ -51,53 +50,17 @@ function bindingArbitrationNeedsSeparateYes(state: CreateFlowState): boolean {
return joinsWithoutAYes && conflict.includes("binding-arbitration");
}
const FACET_FALLBACK_ORDER: readonly TemplateFacetGroupKey[] = [
"coreValues",
"communication",
"membership",
"decisionApproaches",
"conflictManagement",
] as const;
/**
* `finalReview.json.categories` ships a demo ordering + localized names
* (Values / Communication / Membership / Decision-making / Conflict
* management). We reuse that ordering for the state-derived rows so the
* Rule layout stays stable across customize / use-without-changes /
* plain-custom flows, and fall back to the demo chips when state resolves
* to nothing selected.
*/
function readFallbackCategoryRows(
categories: readonly { name: string; chips: readonly string[] }[],
): {
names: {
values: string;
communication: string;
membership: string;
decisions: string;
conflict: string;
};
rows: FinalReviewCategoryRowDetailed[];
} {
function categoryNamesFromMessages(
categories: readonly { name: string }[],
): FinalReviewCategoryNames {
const get = (i: number): string =>
typeof categories[i]?.name === "string" ? categories[i].name : "";
return {
names: {
values: get(0),
communication: get(1),
membership: get(2),
decisions: get(3),
conflict: get(4),
},
rows: categories.map((c) => ({
name: c.name,
groupKey: null,
entries: [...c.chips].map((label) => ({
label,
groupKey: null,
overrideKey: null,
})),
})),
values: get(0),
communication: get(1),
membership: get(2),
decisions: get(3),
conflict: get(4),
};
}
@@ -145,28 +108,32 @@ export function FinalReviewScreen({
);
const { categories: finalReviewCategories } = useMemo(() => {
const { names, rows: fallbackRows } = readFallbackCategoryRows(
m.create.reviewAndComplete.finalReview.categories,
);
const derived = buildFinalReviewCategoryRowsDetailed(state, names);
const copy = m.create.reviewAndComplete.finalReview;
const names = categoryNamesFromMessages(copy.categories);
const rowsToRender: readonly FinalReviewCategoryRowDetailed[] =
derived.length > 0 ? derived : fallbackRows;
const usingFallbackRows = derived.length === 0;
buildFinalReviewCategoryRowsDetailed(state, names);
const lookup = new Map<
string,
{ target: FinalReviewChipEditTarget | null; readOnly: TemplateChipDetail }
>();
const cats: Category[] = rowsToRender.map((row, rowIndex) => {
const effectiveGroupKey: TemplateFacetGroupKey | null =
row.groupKey ??
(usingFallbackRows && rowIndex < FACET_FALLBACK_ORDER.length
? FACET_FALLBACK_ORDER[rowIndex]
: null);
const cats: Category[] = rowsToRender.map((row) => {
const effectiveGroupKey: TemplateFacetGroupKey | null = row.groupKey;
const reviewReturn =
variant === "editPublished" ? ("edit-rule" as const) : ("final-review" as const);
const isEmpty = row.entries.length === 0;
const flag =
isEmpty && effectiveGroupKey === "coreValues"
? copy.flags.noValues
: isEmpty && effectiveGroupKey != null
? copy.flags.noCard
: undefined;
const addButtonText = isEmpty
? effectiveGroupKey === "coreValues"
? copy.addValue
: copy.addCard
: "";
const chipOptions = row.entries.map((entry, idx) => {
const chipId = `${row.name}-${idx}`;
@@ -190,12 +157,19 @@ export function FinalReviewScreen({
id: chipId,
label: entry.label,
state: "unselected" as const,
hasWarning: finalReviewChipHasWarning(
state,
entry.groupKey,
entry.overrideKey,
),
};
});
return {
name: row.name,
chipOptions,
addButton: effectiveGroupKey != null,
addButtonText,
flag,
onChipClick: (_categoryName: string, chipId: string) => {
const hit = lookup.get(chipId);
if (!hit) return;
@@ -219,7 +193,7 @@ export function FinalReviewScreen({
});
return { categories: cats };
}, [
m.create.reviewAndComplete.finalReview.categories,
m.create.reviewAndComplete.finalReview,
state,
markCreateFlowInteraction,
goToStep,
@@ -249,8 +223,7 @@ export function FinalReviewScreen({
const rawTitleForModal =
typeof state.title === "string" ? state.title : "";
const descriptionEmptyHint =
variant === "editPublished" ? t("communityContextEditModal.emptyHint") : undefined;
const descriptionEmptyHint = t("communityContextEditModal.emptyHint");
const avatarUrl = useCommunityAvatarDisplayUrl(state.communityAvatarUrl);
const storedFallback =
@@ -305,34 +278,21 @@ export function FinalReviewScreen({
<Rule
title={ruleCardTitle}
description={ruleCardDescription}
onTitleClick={
variant === "editPublished"
? () => setTitleModalOpen(true)
: undefined
}
titleEditAriaLabel={
variant === "editPublished"
? t("titleEditModal.ariaEditTitle")
: undefined
}
onDescriptionClick={
variant === "editPublished"
? () => setCommunityContextModalOpen(true)
: undefined
}
onTitleClick={() => setTitleModalOpen(true)}
titleEditAriaLabel={t("titleEditModal.ariaEditTitle")}
onDescriptionClick={() => setCommunityContextModalOpen(true)}
descriptionEmptyHint={descriptionEmptyHint}
descriptionEditAriaLabel={
variant === "editPublished"
? t("communityContextEditModal.ariaEditDescription")
: undefined
}
descriptionEditAriaLabel={t("communityContextEditModal.ariaEditDescription")}
size={mdUp ? "L" : "M"}
expanded={true}
descriptionAtBottom
backgroundColor="bg-transparent"
surfaceColor={surfaceColor}
logoUrl={avatarUrl ?? getAssetPath(vectorMarkPath("mutual-aid"))}
logoUrl={avatarUrl}
logoAlt={ruleCardTitle}
communityInitials={
avatarUrl ? undefined : communityInitialsFromTitle(ruleCardTitle)
}
categories={finalReviewCategories}
className={`${CREATE_FLOW_REVIEW_RULE_LAYOUT_CLASS}${surfaceColor ? "" : " invisible"}`}
onClick={() => {}}
@@ -352,28 +312,24 @@ export function FinalReviewScreen({
onClose={() => setActiveReadOnlyDetail(null)}
detail={activeReadOnlyDetail}
/>
{variant === "editPublished" ? (
<>
<FinalReviewTitleEditModal
isOpen={titleModalOpen}
onClose={() => setTitleModalOpen(false)}
initialValue={rawTitleForModal}
onSave={(value) => {
markCreateFlowInteraction();
updateState({ title: value });
}}
/>
<FinalReviewCommunityContextEditModal
isOpen={communityContextModalOpen}
onClose={() => setCommunityContextModalOpen(false)}
initialValue={rawCommunityContextForModal}
onSave={(value) => {
markCreateFlowInteraction();
updateState({ communityContext: value, summary: value });
}}
/>
</>
) : null}
<FinalReviewTitleEditModal
isOpen={titleModalOpen}
onClose={() => setTitleModalOpen(false)}
initialValue={rawTitleForModal}
onSave={(value) => {
markCreateFlowInteraction();
updateState({ title: value });
}}
/>
<FinalReviewCommunityContextEditModal
isOpen={communityContextModalOpen}
onClose={() => setCommunityContextModalOpen(false)}
initialValue={rawCommunityContextForModal}
onSave={(value) => {
markCreateFlowInteraction();
updateState({ communityContext: value, summary: value });
}}
/>
</CreateFlowLockupCardStepShell>
);
}