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
@@ -7,18 +7,19 @@
* Layout matches {@link CreateFlowTwoColumnSelectShell}: one column below `lg` (1024px), two columns
* at `lg+` with a scrollable rail — same breakpoint and height chain as select steps, distinct content.
*
* Card click opens the Figma "Add Approach" create modal (node `20870-72155`) with five controls:
* Core Principle, Applicable Scope, Step-by-Step Instructions, Consensus Level, and Objections &
* Deadlocks. Section defaults are sourced from `messages/en/create/customRule/decisionApproaches.json` (read
* via `m.create.customRule.decisionApproaches`) and will be replaced with DB-driven content; labels are
* hard-coded per the Figma design.
* Card click opens the Figma "Add Approach" create modal (node `20870-72155`) with five controls
* rendered by {@link DecisionApproachEditFields}: Core Principle, Applicable Scope, Step-by-Step
* Instructions, Consensus Level, and Objections & Deadlocks. The same field set is reused on
* `/create/final-review` — see `FinalReviewChipEditModal`. Confirm persists both the chip
* selection and any user edits as a `decisionApproachDetailsById[id]` override; section
* defaults come from `messages/en/create/customRule/decisionApproaches.json` and will be
* replaced with DB-driven content.
*/
import { useState, useCallback, useMemo } from "react";
import DecisionMakingSidebar from "../../../../components/utility/DecisionMakingSidebar";
import CardStack from "../../../../components/utility/CardStack";
import Create from "../../../../components/modals/Create";
import IncrementerBlock from "../../../../components/controls/IncrementerBlock";
import InlineTextButton from "../../../../components/buttons/InlineTextButton";
import type { InfoMessageBoxItem } from "../../../../components/utility/InfoMessageBox/InfoMessageBox.types";
import type { CardStackItem } from "../../../../components/utility/CardStack/CardStack.types";
@@ -31,110 +32,9 @@ import {
useFacetRecommendations,
} from "../../hooks/useFacetRecommendations";
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
import ModalTextAreaField from "../../components/ModalTextAreaField";
import ApplicableScopeField from "../../components/ApplicableScopeField";
const CONSENSUS_LEVEL_MIN = 0;
const CONSENSUS_LEVEL_MAX = 100;
const CONSENSUS_LEVEL_STEP = 5;
const CONSENSUS_LEVEL_DEFAULT = 75;
type RightRailModalSections = {
corePrinciple: string;
applicableScope: string[];
selectedApplicableScope: string[];
stepByStepInstructions: string;
consensusLevel: number;
objectionsDeadlocks: string;
};
function AddDecisionApproachModalContent({
approachCardId,
}: {
approachCardId: string;
}) {
const { markCreateFlowInteraction } = useCreateFlow();
const m = useMessages();
const da = m.create.customRule.decisionApproaches;
const method = da.methods.find((entry) => entry.id === approachCardId);
const modalSections = method?.sections;
const defaults: RightRailModalSections = {
corePrinciple: modalSections?.corePrinciple ?? "",
applicableScope: modalSections?.applicableScope ?? [],
selectedApplicableScope: [],
stepByStepInstructions: modalSections?.stepByStepInstructions ?? "",
consensusLevel: modalSections?.consensusLevel ?? CONSENSUS_LEVEL_DEFAULT,
objectionsDeadlocks: modalSections?.objectionsDeadlocks ?? "",
};
const [sections, setSections] = useState<RightRailModalSections>(() => ({
corePrinciple: defaults.corePrinciple,
applicableScope: [...defaults.applicableScope],
selectedApplicableScope: [...defaults.selectedApplicableScope],
stepByStepInstructions: defaults.stepByStepInstructions,
consensusLevel: defaults.consensusLevel,
objectionsDeadlocks: defaults.objectionsDeadlocks,
}));
const patch = useCallback(
<K extends keyof RightRailModalSections>(
key: K,
value: RightRailModalSections[K],
) => {
markCreateFlowInteraction();
setSections((prev) => ({ ...prev, [key]: value }));
},
[markCreateFlowInteraction],
);
return (
<div className="flex flex-col gap-6">
<ModalTextAreaField
label={da.sectionHeadings.corePrinciple}
value={sections.corePrinciple}
onChange={(v) => patch("corePrinciple", v)}
/>
<ApplicableScopeField
label={da.sectionHeadings.applicableScope}
addLabel={da.scopeAddButtonLabel}
scopes={sections.applicableScope}
selectedScopes={sections.selectedApplicableScope}
onToggleScope={(scope) =>
patch(
"selectedApplicableScope",
sections.selectedApplicableScope.includes(scope)
? sections.selectedApplicableScope.filter((s) => s !== scope)
: [...sections.selectedApplicableScope, scope],
)
}
onAddScope={(scope) =>
patch("applicableScope", [...sections.applicableScope, scope])
}
/>
<ModalTextAreaField
label={da.sectionHeadings.stepByStepInstructions}
value={sections.stepByStepInstructions}
onChange={(v) => patch("stepByStepInstructions", v)}
/>
<IncrementerBlock
label={da.sectionHeadings.consensusLevel}
value={sections.consensusLevel}
min={CONSENSUS_LEVEL_MIN}
max={CONSENSUS_LEVEL_MAX}
step={CONSENSUS_LEVEL_STEP}
onChange={(next) => patch("consensusLevel", next)}
formatValue={(v) => `${v}%`}
decrementAriaLabel="Decrease consensus level"
incrementAriaLabel="Increase consensus level"
/>
<ModalTextAreaField
label={da.sectionHeadings.objectionsDeadlocks}
value={sections.objectionsDeadlocks}
onChange={(v) => patch("objectionsDeadlocks", v)}
/>
</div>
);
}
import { DecisionApproachEditFields } from "../../components/methodEditFields";
import { decisionApproachPresetFor } from "../../../../../lib/create/finalReviewChipPresets";
import type { DecisionApproachDetailEntry } from "../../types";
export function DecisionApproachesScreen() {
const m = useMessages();
@@ -147,16 +47,11 @@ export function DecisionApproachesScreen() {
const [expanded, setExpanded] = useState(false);
const [createModalOpen, setCreateModalOpen] = useState(false);
const [pendingCardId, setPendingCardId] = useState<string | null>(null);
const [pendingDraft, setPendingDraft] =
useState<DecisionApproachDetailEntry | null>(null);
const selectedIds = state.selectedDecisionApproachIds ?? [];
const setSelectedIds = useCallback(
(next: string[]) => {
updateState({ selectedDecisionApproachIds: next });
},
[updateState],
);
const messageBoxItems: InfoMessageBoxItem[] = useMemo(
() =>
da.messageBox.items.map((item) => ({
@@ -219,12 +114,36 @@ export function DecisionApproachesScreen() {
[markCreateFlowInteraction],
);
const seedDraft = useCallback(
(id: string): DecisionApproachDetailEntry => {
const saved = state.decisionApproachDetailsById?.[id];
if (saved) {
return {
...saved,
applicableScope: [...saved.applicableScope],
selectedApplicableScope: [...saved.selectedApplicableScope],
};
}
return decisionApproachPresetFor(id);
},
[state.decisionApproachDetailsById],
);
const handleCardSelect = useCallback(
(id: string) => {
markCreateFlowInteraction();
setPendingCardId(id);
setPendingDraft(seedDraft(id));
setCreateModalOpen(true);
},
[markCreateFlowInteraction, seedDraft],
);
const handleDraftChange = useCallback(
(next: DecisionApproachDetailEntry) => {
markCreateFlowInteraction();
setPendingDraft(next);
},
[markCreateFlowInteraction],
);
@@ -236,37 +155,49 @@ export function DecisionApproachesScreen() {
const handleCreateModalClose = useCallback(() => {
setCreateModalOpen(false);
setPendingCardId(null);
setPendingDraft(null);
}, []);
const handleCreateModalConfirm = useCallback(() => {
markCreateFlowInteraction();
if (pendingCardId) {
setSelectedIds(
selectedIds.includes(pendingCardId)
? selectedIds
: [...selectedIds, pendingCardId],
);
if (!pendingCardId || !pendingDraft) {
handleCreateModalClose();
return;
}
setCreateModalOpen(false);
setPendingCardId(null);
}, [markCreateFlowInteraction, pendingCardId, selectedIds, setSelectedIds]);
markCreateFlowInteraction();
updateState({
selectedDecisionApproachIds: selectedIds.includes(pendingCardId)
? selectedIds
: [...selectedIds, pendingCardId],
decisionApproachDetailsById: {
...(state.decisionApproachDetailsById ?? {}),
[pendingCardId]: pendingDraft,
},
});
handleCreateModalClose();
}, [
handleCreateModalClose,
markCreateFlowInteraction,
pendingCardId,
pendingDraft,
selectedIds,
state.decisionApproachDetailsById,
updateState,
]);
const modalConfig = (() => {
if (!pendingCardId) {
return {
const modalConfig = pendingCardId
? (() => {
const method = methodById.get(pendingCardId);
return {
title: method?.label ?? da.confirmModal.title,
description: method?.supportText ?? da.confirmModal.description,
nextButtonText: da.addApproach.nextButtonText,
};
})()
: {
title: da.confirmModal.title,
description: da.confirmModal.description,
nextButtonText: da.confirmModal.nextButtonText,
};
}
const method = methodById.get(pendingCardId);
return {
title: method?.label ?? da.confirmModal.title,
description: method?.supportText ?? da.confirmModal.description,
nextButtonText: da.addApproach.nextButtonText,
};
})();
return (
<CreateFlowTwoColumnSelectShell
@@ -315,10 +246,11 @@ export function DecisionApproachesScreen() {
showBackButton={false}
backdropVariant="loginYellow"
>
{pendingCardId ? (
<AddDecisionApproachModalContent
{pendingCardId && pendingDraft ? (
<DecisionApproachEditFields
key={pendingCardId}
approachCardId={pendingCardId}
value={pendingDraft}
onChange={handleDraftChange}
/>
) : null}
</Create>