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,6 +7,11 @@
* Lives under `screens/card/` (not `select/`): Figma **card stack** layout is a distinct shell from
* two-column chip **select** frames. Future card-stack steps get their own `*Screen.tsx` here and
* reuse `CardStack` / `CreateFlowStepShell` as needed.
*
* Card click opens the Figma "Add Platform" create modal (node `20246-15829`) with three
* editable sections rendered by {@link CommunicationMethodEditFields}. The same field set is
* reused on `/create/final-review` — see `FinalReviewChipEditModal`. Confirm persists both
* the chip selection and any user edits as a `communicationMethodDetailsById[id]` override.
*/
import { useState, useCallback, useMemo } from "react";
@@ -27,61 +32,9 @@ import {
CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS,
CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS,
} from "../../components/createFlowLayoutTokens";
import ModalTextAreaField from "../../components/ModalTextAreaField";
const SECTION_FIELDS = [
"corePrinciple",
"logisticsAdmin",
"codeOfConduct",
] as const;
type SectionField = (typeof SECTION_FIELDS)[number];
function AddPlatformModalContent({
platformCardId,
}: {
platformCardId: string;
}) {
const { markCreateFlowInteraction } = useCreateFlow();
const m = useMessages();
const comm = m.create.customRule.communication;
const method = comm.methods.find((entry) => entry.id === platformCardId);
const sections = method?.sections;
const defaults: Record<SectionField, string> = {
corePrinciple: sections?.corePrinciple ?? "",
logisticsAdmin: sections?.logisticsAdmin ?? "",
codeOfConduct: sections?.codeOfConduct ?? "",
};
const [sectionValues, setSectionValues] = useState<
Record<SectionField, string>
>(() => ({
corePrinciple: defaults.corePrinciple,
logisticsAdmin: defaults.logisticsAdmin,
codeOfConduct: defaults.codeOfConduct,
}));
const updateSection = useCallback(
(key: SectionField, value: string) => {
markCreateFlowInteraction();
setSectionValues((prev) => ({ ...prev, [key]: value }));
},
[markCreateFlowInteraction],
);
return (
<div className="flex flex-col gap-6">
{SECTION_FIELDS.map((field) => (
<ModalTextAreaField
key={field}
label={comm.sectionHeadings[field]}
rows={6}
value={sectionValues[field]}
onChange={(v) => updateSection(field, v)}
/>
))}
</div>
);
}
import { CommunicationMethodEditFields } from "../../components/methodEditFields";
import { communicationPresetFor } from "../../../../../lib/create/finalReviewChipPresets";
import type { CommunicationMethodDetailEntry } from "../../types";
export function CommunicationMethodsScreen() {
const m = useMessages();
@@ -91,16 +44,11 @@ export function CommunicationMethodsScreen() {
const [expanded, setExpanded] = useState(false);
const [createModalOpen, setCreateModalOpen] = useState(false);
const [pendingCardId, setPendingCardId] = useState<string | null>(null);
const [pendingDraft, setPendingDraft] =
useState<CommunicationMethodDetailEntry | null>(null);
const selectedIds = state.selectedCommunicationMethodIds ?? [];
const setSelectedIds = useCallback(
(next: string[]) => {
updateState({ selectedCommunicationMethodIds: next });
},
[updateState],
);
const { scoresBySlug, hasAnyFacets } =
useFacetRecommendations("communication");
const rankedMethods = useMemo(
@@ -148,55 +96,81 @@ export function CommunicationMethodsScreen() {
</>
);
const modalConfig = (() => {
if (!pendingCardId) {
return {
const modalConfig = pendingCardId
? (() => {
const method = methodById.get(pendingCardId);
return {
title: method?.label ?? comm.confirmModal.title,
description: method?.supportText ?? comm.confirmModal.description,
nextButtonText: comm.addPlatform.nextButtonText,
};
})()
: {
title: comm.confirmModal.title,
description: comm.confirmModal.description,
nextButtonText: comm.confirmModal.nextButtonText,
showBackButton: false as const,
currentStep: undefined,
totalSteps: undefined,
};
}
const method = methodById.get(pendingCardId);
return {
title: method?.label ?? comm.confirmModal.title,
description: method?.supportText ?? comm.confirmModal.description,
nextButtonText: comm.addPlatform.nextButtonText,
showBackButton: false as const,
currentStep: undefined,
totalSteps: undefined,
};
})();
const seedDraft = useCallback(
(id: string): CommunicationMethodDetailEntry => {
const saved = state.communicationMethodDetailsById?.[id];
if (saved) {
return { ...saved };
}
return communicationPresetFor(id);
},
[state.communicationMethodDetailsById],
);
const handleCardClick = useCallback(
(id: string) => {
markCreateFlowInteraction();
setPendingCardId(id);
setPendingDraft(seedDraft(id));
setCreateModalOpen(true);
},
[markCreateFlowInteraction, seedDraft],
);
const handleDraftChange = useCallback(
(next: CommunicationMethodDetailEntry) => {
markCreateFlowInteraction();
setPendingDraft(next);
},
[markCreateFlowInteraction],
);
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({
selectedCommunicationMethodIds: selectedIds.includes(pendingCardId)
? selectedIds
: [...selectedIds, pendingCardId],
communicationMethodDetailsById: {
...(state.communicationMethodDetailsById ?? {}),
[pendingCardId]: pendingDraft,
},
});
handleCreateModalClose();
}, [
handleCreateModalClose,
markCreateFlowInteraction,
pendingCardId,
pendingDraft,
selectedIds,
state.communicationMethodDetailsById,
updateState,
]);
return (
<CreateFlowStepShell
@@ -238,15 +212,14 @@ export function CommunicationMethodsScreen() {
title={modalConfig.title}
description={modalConfig.description}
nextButtonText={modalConfig.nextButtonText}
showBackButton={modalConfig.showBackButton}
currentStep={modalConfig.currentStep}
totalSteps={modalConfig.totalSteps}
showBackButton={false}
backdropVariant="loginYellow"
>
{pendingCardId ? (
<AddPlatformModalContent
{pendingCardId && pendingDraft ? (
<CommunicationMethodEditFields
key={pendingCardId}
platformCardId={pendingCardId}
value={pendingDraft}
onChange={handleDraftChange}
/>
) : null}
</Create>
@@ -4,11 +4,12 @@
* `conflict-management` step — Figma compact card stack (node `20879-15979`).
* Registry: `CREATE_FLOW_SCREEN_REGISTRY["conflict-management"]`.
*
* Card click opens the Figma "Add Approach" create modal (node `20874-172292`) with four
* controls: Core Principle, Applicable Scope (capsules), Process Protocol, and Restoration
* & Fallbacks. Section defaults are sourced from
* `messages/en/create/customRule/conflictManagement.json` 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 `20874-172292`)
* with four controls rendered by {@link ConflictManagementEditFields}: Core
* Principle, Applicable Scope (capsules), Process Protocol, and Restoration
* & Fallbacks. The same field set is reused on `/create/final-review` — see
* `FinalReviewChipEditModal`. Confirm persists both the chip selection and
* any user edits as a `conflictManagementDetailsById[id]` override.
*/
import { useState, useCallback, useMemo } from "react";
@@ -29,91 +30,9 @@ import {
CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS,
CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS,
} from "../../components/createFlowLayoutTokens";
import ModalTextAreaField from "../../components/ModalTextAreaField";
import ApplicableScopeField from "../../components/ApplicableScopeField";
type ConflictModalSections = {
corePrinciple: string;
applicableScope: string[];
selectedApplicableScope: string[];
processProtocol: string;
restorationFallbacks: string;
};
function AddConflictApproachModalContent({
approachCardId,
}: {
approachCardId: string;
}) {
const { markCreateFlowInteraction } = useCreateFlow();
const m = useMessages();
const cm = m.create.customRule.conflictManagement;
const method = cm.methods.find((entry) => entry.id === approachCardId);
const modalSections = method?.sections;
const defaults: ConflictModalSections = {
corePrinciple: modalSections?.corePrinciple ?? "",
applicableScope: modalSections?.applicableScope ?? [],
selectedApplicableScope: [],
processProtocol: modalSections?.processProtocol ?? "",
restorationFallbacks: modalSections?.restorationFallbacks ?? "",
};
const [sections, setSections] = useState<ConflictModalSections>(() => ({
corePrinciple: defaults.corePrinciple,
applicableScope: [...defaults.applicableScope],
selectedApplicableScope: [...defaults.selectedApplicableScope],
processProtocol: defaults.processProtocol,
restorationFallbacks: defaults.restorationFallbacks,
}));
const patch = useCallback(
<K extends keyof ConflictModalSections>(
key: K,
value: ConflictModalSections[K],
) => {
markCreateFlowInteraction();
setSections((prev) => ({ ...prev, [key]: value }));
},
[markCreateFlowInteraction],
);
return (
<div className="flex flex-col gap-6">
<ModalTextAreaField
label={cm.sectionHeadings.corePrinciple}
value={sections.corePrinciple}
onChange={(v) => patch("corePrinciple", v)}
/>
<ApplicableScopeField
label={cm.sectionHeadings.applicableScope}
addLabel={cm.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={cm.sectionHeadings.processProtocol}
value={sections.processProtocol}
onChange={(v) => patch("processProtocol", v)}
/>
<ModalTextAreaField
label={cm.sectionHeadings.restorationFallbacks}
value={sections.restorationFallbacks}
onChange={(v) => patch("restorationFallbacks", v)}
/>
</div>
);
}
import { ConflictManagementEditFields } from "../../components/methodEditFields";
import { conflictManagementPresetFor } from "../../../../../lib/create/finalReviewChipPresets";
import type { ConflictManagementDetailEntry } from "../../types";
export function ConflictManagementScreen() {
const m = useMessages();
@@ -123,16 +42,11 @@ export function ConflictManagementScreen() {
const [expanded, setExpanded] = useState(false);
const [createModalOpen, setCreateModalOpen] = useState(false);
const [pendingCardId, setPendingCardId] = useState<string | null>(null);
const [pendingDraft, setPendingDraft] =
useState<ConflictManagementDetailEntry | null>(null);
const selectedIds = state.selectedConflictManagementIds ?? [];
const setSelectedIds = useCallback(
(next: string[]) => {
updateState({ selectedConflictManagementIds: next });
},
[updateState],
);
const { scoresBySlug, hasAnyFacets } =
useFacetRecommendations("conflictManagement");
const rankedMethods = useMemo(
@@ -180,55 +94,85 @@ export function ConflictManagementScreen() {
</>
);
const modalConfig = (() => {
if (!pendingCardId) {
return {
const modalConfig = pendingCardId
? (() => {
const method = methodById.get(pendingCardId);
return {
title: method?.label ?? cm.confirmModal.title,
description: method?.supportText ?? cm.confirmModal.description,
nextButtonText: cm.addApproach.nextButtonText,
};
})()
: {
title: cm.confirmModal.title,
description: cm.confirmModal.description,
nextButtonText: cm.confirmModal.nextButtonText,
showBackButton: false as const,
currentStep: undefined,
totalSteps: undefined,
};
}
const method = methodById.get(pendingCardId);
return {
title: method?.label ?? cm.confirmModal.title,
description: method?.supportText ?? cm.confirmModal.description,
nextButtonText: cm.addApproach.nextButtonText,
showBackButton: false as const,
currentStep: undefined,
totalSteps: undefined,
};
})();
const seedDraft = useCallback(
(id: string): ConflictManagementDetailEntry => {
const saved = state.conflictManagementDetailsById?.[id];
if (saved) {
return {
...saved,
applicableScope: [...saved.applicableScope],
selectedApplicableScope: [...saved.selectedApplicableScope],
};
}
return conflictManagementPresetFor(id);
},
[state.conflictManagementDetailsById],
);
const handleCardClick = useCallback(
(id: string) => {
markCreateFlowInteraction();
setPendingCardId(id);
setPendingDraft(seedDraft(id));
setCreateModalOpen(true);
},
[markCreateFlowInteraction, seedDraft],
);
const handleDraftChange = useCallback(
(next: ConflictManagementDetailEntry) => {
markCreateFlowInteraction();
setPendingDraft(next);
},
[markCreateFlowInteraction],
);
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({
selectedConflictManagementIds: selectedIds.includes(pendingCardId)
? selectedIds
: [...selectedIds, pendingCardId],
conflictManagementDetailsById: {
...(state.conflictManagementDetailsById ?? {}),
[pendingCardId]: pendingDraft,
},
});
handleCreateModalClose();
}, [
handleCreateModalClose,
markCreateFlowInteraction,
pendingCardId,
pendingDraft,
selectedIds,
state.conflictManagementDetailsById,
updateState,
]);
return (
<CreateFlowStepShell
@@ -270,15 +214,14 @@ export function ConflictManagementScreen() {
title={modalConfig.title}
description={modalConfig.description}
nextButtonText={modalConfig.nextButtonText}
showBackButton={modalConfig.showBackButton}
currentStep={modalConfig.currentStep}
totalSteps={modalConfig.totalSteps}
showBackButton={false}
backdropVariant="loginYellow"
>
{pendingCardId ? (
<AddConflictApproachModalContent
{pendingCardId && pendingDraft ? (
<ConflictManagementEditFields
key={pendingCardId}
approachCardId={pendingCardId}
value={pendingDraft}
onChange={handleDraftChange}
/>
) : null}
</Create>
@@ -5,10 +5,12 @@
* Registry: `CREATE_FLOW_SCREEN_REGISTRY["membership-methods"]`.
*
* Card click opens the Figma create modal (node `20858-13948`) with three
* editable sections — Eligibility & Philosophy, Joining Process, and
* Expectations & Removal. Section defaults come from
* `messages/en/create/customRule/membership.json` and will be replaced with DB-driven
* content.
* editable sections rendered by {@link MembershipMethodEditFields}. The same
* field set is reused on `/create/final-review` — see `FinalReviewChipEditModal`.
* Confirm persists both the chip selection and any user edits as a
* `membershipMethodDetailsById[id]` override; section defaults come from
* `messages/en/create/customRule/membership.json` and will be replaced with
* DB-driven content.
*/
import { useState, useCallback, useMemo } from "react";
@@ -29,61 +31,9 @@ import {
CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS,
CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS,
} from "../../components/createFlowLayoutTokens";
import ModalTextAreaField from "../../components/ModalTextAreaField";
const SECTION_FIELDS = [
"eligibility",
"joiningProcess",
"expectations",
] as const;
type SectionField = (typeof SECTION_FIELDS)[number];
function AddMembershipModalContent({
membershipCardId,
}: {
membershipCardId: string;
}) {
const { markCreateFlowInteraction } = useCreateFlow();
const m = useMessages();
const mem = m.create.customRule.membership;
const method = mem.methods.find((entry) => entry.id === membershipCardId);
const sections = method?.sections;
const defaults: Record<SectionField, string> = {
eligibility: sections?.eligibility ?? "",
joiningProcess: sections?.joiningProcess ?? "",
expectations: sections?.expectations ?? "",
};
const [sectionValues, setSectionValues] = useState<
Record<SectionField, string>
>(() => ({
eligibility: defaults.eligibility,
joiningProcess: defaults.joiningProcess,
expectations: defaults.expectations,
}));
const updateSection = useCallback(
(key: SectionField, value: string) => {
markCreateFlowInteraction();
setSectionValues((prev) => ({ ...prev, [key]: value }));
},
[markCreateFlowInteraction],
);
return (
<div className="flex flex-col gap-6">
{SECTION_FIELDS.map((field) => (
<ModalTextAreaField
key={field}
label={mem.sectionHeadings[field]}
rows={6}
value={sectionValues[field]}
onChange={(v) => updateSection(field, v)}
/>
))}
</div>
);
}
import { MembershipMethodEditFields } from "../../components/methodEditFields";
import { membershipPresetFor } from "../../../../../lib/create/finalReviewChipPresets";
import type { MembershipMethodDetailEntry } from "../../types";
export function MembershipMethodsScreen() {
const m = useMessages();
@@ -93,16 +43,11 @@ export function MembershipMethodsScreen() {
const [expanded, setExpanded] = useState(false);
const [createModalOpen, setCreateModalOpen] = useState(false);
const [pendingCardId, setPendingCardId] = useState<string | null>(null);
const [pendingDraft, setPendingDraft] =
useState<MembershipMethodDetailEntry | null>(null);
const selectedIds = state.selectedMembershipMethodIds ?? [];
const setSelectedIds = useCallback(
(next: string[]) => {
updateState({ selectedMembershipMethodIds: next });
},
[updateState],
);
const { scoresBySlug, hasAnyFacets } =
useFacetRecommendations("membership");
const rankedMethods = useMemo(
@@ -150,55 +95,81 @@ export function MembershipMethodsScreen() {
</>
);
const modalConfig = (() => {
if (!pendingCardId) {
return {
const modalConfig = pendingCardId
? (() => {
const method = methodById.get(pendingCardId);
return {
title: method?.label ?? mem.confirmModal.title,
description: method?.supportText ?? mem.confirmModal.description,
nextButtonText: mem.addPlatform.nextButtonText,
};
})()
: {
title: mem.confirmModal.title,
description: mem.confirmModal.description,
nextButtonText: mem.confirmModal.nextButtonText,
showBackButton: false as const,
currentStep: undefined,
totalSteps: undefined,
};
}
const method = methodById.get(pendingCardId);
return {
title: method?.label ?? mem.confirmModal.title,
description: method?.supportText ?? mem.confirmModal.description,
nextButtonText: mem.addPlatform.nextButtonText,
showBackButton: false as const,
currentStep: undefined,
totalSteps: undefined,
};
})();
const seedDraft = useCallback(
(id: string): MembershipMethodDetailEntry => {
const saved = state.membershipMethodDetailsById?.[id];
if (saved) {
return { ...saved };
}
return membershipPresetFor(id);
},
[state.membershipMethodDetailsById],
);
const handleCardClick = useCallback(
(id: string) => {
markCreateFlowInteraction();
setPendingCardId(id);
setPendingDraft(seedDraft(id));
setCreateModalOpen(true);
},
[markCreateFlowInteraction, seedDraft],
);
const handleDraftChange = useCallback(
(next: MembershipMethodDetailEntry) => {
markCreateFlowInteraction();
setPendingDraft(next);
},
[markCreateFlowInteraction],
);
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({
selectedMembershipMethodIds: selectedIds.includes(pendingCardId)
? selectedIds
: [...selectedIds, pendingCardId],
membershipMethodDetailsById: {
...(state.membershipMethodDetailsById ?? {}),
[pendingCardId]: pendingDraft,
},
});
handleCreateModalClose();
}, [
handleCreateModalClose,
markCreateFlowInteraction,
pendingCardId,
pendingDraft,
selectedIds,
state.membershipMethodDetailsById,
updateState,
]);
return (
<CreateFlowStepShell
@@ -240,15 +211,14 @@ export function MembershipMethodsScreen() {
title={modalConfig.title}
description={modalConfig.description}
nextButtonText={modalConfig.nextButtonText}
showBackButton={modalConfig.showBackButton}
currentStep={modalConfig.currentStep}
totalSteps={modalConfig.totalSteps}
showBackButton={false}
backdropVariant="loginYellow"
>
{pendingCardId ? (
<AddMembershipModalContent
{pendingCardId && pendingDraft ? (
<MembershipMethodEditFields
key={pendingCardId}
membershipCardId={pendingCardId}
value={pendingDraft}
onChange={handleDraftChange}
/>
) : null}
</Create>