Final review edit modals created
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import RuleCard from "../../../../components/cards/RuleCard";
|
||||
import type { Category } from "../../../../components/cards/RuleCard/RuleCard.types";
|
||||
import { TemplateChipDetailModal } from "../../../../components/cards/TemplateReviewCard/TemplateChipDetailModal";
|
||||
import { useMessages, useTranslation } from "../../../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||
@@ -11,22 +12,15 @@ import {
|
||||
CreateFlowLockupCardStepShell,
|
||||
} from "../../components/CreateFlowLockupCardStepShell";
|
||||
import {
|
||||
buildFinalReviewCategoriesFromState,
|
||||
type FinalReviewCategoryRow,
|
||||
buildFinalReviewCategoryRowsDetailed,
|
||||
type FinalReviewCategoryRowDetailed,
|
||||
} from "../../../../../lib/create/buildFinalReviewCategories";
|
||||
|
||||
function buildFinalReviewCategories(
|
||||
rows: readonly FinalReviewCategoryRow[],
|
||||
): Category[] {
|
||||
return rows.map((cat) => ({
|
||||
name: cat.name,
|
||||
chipOptions: cat.chips.map((label, idx) => ({
|
||||
id: `${cat.name}-${idx}`,
|
||||
label,
|
||||
state: "unselected" as const,
|
||||
})),
|
||||
}));
|
||||
}
|
||||
import type { TemplateChipDetail } from "../../../../../lib/create/templateReviewMapping";
|
||||
import {
|
||||
FinalReviewChipEditModal,
|
||||
type FinalReviewChipEditPatch,
|
||||
type FinalReviewChipEditTarget,
|
||||
} from "../../components/FinalReviewChipEditModal";
|
||||
|
||||
/**
|
||||
* `finalReview.json.categories` ships a demo ordering + localized names
|
||||
@@ -36,7 +30,7 @@ function buildFinalReviewCategories(
|
||||
* plain-custom flows, and fall back to the demo chips when state resolves
|
||||
* to nothing selected.
|
||||
*/
|
||||
function readFallbackCategoryNames(
|
||||
function readFallbackCategoryRows(
|
||||
categories: readonly { name: string; chips: readonly string[] }[],
|
||||
): {
|
||||
names: {
|
||||
@@ -46,7 +40,7 @@ function readFallbackCategoryNames(
|
||||
decisions: string;
|
||||
conflict: string;
|
||||
};
|
||||
rows: FinalReviewCategoryRow[];
|
||||
rows: FinalReviewCategoryRowDetailed[];
|
||||
} {
|
||||
const get = (i: number): string =>
|
||||
typeof categories[i]?.name === "string" ? categories[i].name : "";
|
||||
@@ -58,28 +52,158 @@ function readFallbackCategoryNames(
|
||||
decisions: get(3),
|
||||
conflict: get(4),
|
||||
},
|
||||
rows: categories.map((c) => ({ name: c.name, chips: [...c.chips] })),
|
||||
rows: categories.map((c) => ({
|
||||
name: c.name,
|
||||
groupKey: null,
|
||||
entries: [...c.chips].map((label) => ({
|
||||
label,
|
||||
groupKey: null,
|
||||
overrideKey: null,
|
||||
})),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function FinalReviewScreen() {
|
||||
const { state } = useCreateFlow();
|
||||
const { state, updateState, markCreateFlowInteraction } = useCreateFlow();
|
||||
const mdUp = useCreateFlowMdUp();
|
||||
const t = useTranslation("create.reviewAndComplete.finalReview");
|
||||
const m = useMessages();
|
||||
|
||||
const finalReviewCategories = useMemo(() => {
|
||||
const { names, rows: fallbackRows } = readFallbackCategoryNames(
|
||||
/**
|
||||
* Two modals coexist on this screen:
|
||||
*
|
||||
* - {@link FinalReviewChipEditModal} — editable Save-button version used
|
||||
* whenever the chip resolves to a stable `overrideKey` (core-value
|
||||
* chip id, or a method preset id). Writes through to
|
||||
* `{group}DetailsById` state fields on Save; close-without-save is a
|
||||
* no-op so any typed edits are discarded.
|
||||
* - {@link TemplateChipDetailModal} — read-only fallback for chips we
|
||||
* can't map to an override key (e.g. template body entries on the
|
||||
* "Use without changes" path where no preset matches the title).
|
||||
*
|
||||
* `activeEditTarget` drives the editable modal; `activeReadOnlyDetail`
|
||||
* drives the read-only modal; only one is ever non-null at a time.
|
||||
*/
|
||||
const [activeEditTarget, setActiveEditTarget] =
|
||||
useState<FinalReviewChipEditTarget | null>(null);
|
||||
const [activeReadOnlyDetail, setActiveReadOnlyDetail] =
|
||||
useState<TemplateChipDetail | null>(null);
|
||||
|
||||
const handleSave = useCallback(
|
||||
(patch: FinalReviewChipEditPatch) => {
|
||||
markCreateFlowInteraction();
|
||||
switch (patch.groupKey) {
|
||||
case "coreValues": {
|
||||
updateState({
|
||||
coreValueDetailsByChipId: {
|
||||
...(state.coreValueDetailsByChipId ?? {}),
|
||||
[patch.overrideKey]: patch.value,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
case "communication": {
|
||||
updateState({
|
||||
communicationMethodDetailsById: {
|
||||
...(state.communicationMethodDetailsById ?? {}),
|
||||
[patch.overrideKey]: patch.value,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
case "membership": {
|
||||
updateState({
|
||||
membershipMethodDetailsById: {
|
||||
...(state.membershipMethodDetailsById ?? {}),
|
||||
[patch.overrideKey]: patch.value,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
case "decisionApproaches": {
|
||||
updateState({
|
||||
decisionApproachDetailsById: {
|
||||
...(state.decisionApproachDetailsById ?? {}),
|
||||
[patch.overrideKey]: patch.value,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
case "conflictManagement": {
|
||||
updateState({
|
||||
conflictManagementDetailsById: {
|
||||
...(state.conflictManagementDetailsById ?? {}),
|
||||
[patch.overrideKey]: patch.value,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
[markCreateFlowInteraction, updateState, state],
|
||||
);
|
||||
|
||||
const { categories: finalReviewCategories, chipLookup } = useMemo(() => {
|
||||
const { names, rows: fallbackRows } = readFallbackCategoryRows(
|
||||
m.create.reviewAndComplete.finalReview.categories,
|
||||
);
|
||||
const derived = buildFinalReviewCategoriesFromState(state, names);
|
||||
// When a user lands on final review with nothing actually selected (e.g.
|
||||
// direct-nav during dev), keep the shipped demo chips rather than render
|
||||
// an empty card — matches prior behavior for that edge case.
|
||||
return buildFinalReviewCategories(
|
||||
derived.length > 0 ? derived : fallbackRows,
|
||||
);
|
||||
}, [m.create.reviewAndComplete.finalReview.categories, state]);
|
||||
const derived = buildFinalReviewCategoryRowsDetailed(state, names);
|
||||
const rowsToRender: readonly FinalReviewCategoryRowDetailed[] =
|
||||
derived.length > 0 ? derived : fallbackRows;
|
||||
|
||||
const lookup = new Map<
|
||||
string,
|
||||
{ target: FinalReviewChipEditTarget | null; readOnly: TemplateChipDetail }
|
||||
>();
|
||||
|
||||
const cats: Category[] = rowsToRender.map((row) => {
|
||||
const chipOptions = row.entries.map((entry, idx) => {
|
||||
const chipId = `${row.name}-${idx}`;
|
||||
const readOnly: TemplateChipDetail = {
|
||||
chipId,
|
||||
chipLabel: entry.label,
|
||||
categoryName: row.name,
|
||||
groupKey: entry.groupKey,
|
||||
body: "",
|
||||
};
|
||||
const target: FinalReviewChipEditTarget | null =
|
||||
entry.groupKey && entry.overrideKey
|
||||
? {
|
||||
overrideKey: entry.overrideKey,
|
||||
groupKey: entry.groupKey,
|
||||
chipLabel: entry.label,
|
||||
}
|
||||
: null;
|
||||
lookup.set(chipId, { target, readOnly });
|
||||
return {
|
||||
id: chipId,
|
||||
label: entry.label,
|
||||
state: "unselected" as const,
|
||||
};
|
||||
});
|
||||
return {
|
||||
name: row.name,
|
||||
chipOptions,
|
||||
onChipClick: (_categoryName: string, chipId: string) => {
|
||||
const hit = lookup.get(chipId);
|
||||
if (!hit) return;
|
||||
markCreateFlowInteraction();
|
||||
if (hit.target) {
|
||||
setActiveEditTarget(hit.target);
|
||||
} else {
|
||||
setActiveReadOnlyDetail(hit.readOnly);
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
return { categories: cats, chipLookup: lookup };
|
||||
}, [
|
||||
m.create.reviewAndComplete.finalReview.categories,
|
||||
state,
|
||||
markCreateFlowInteraction,
|
||||
]);
|
||||
void chipLookup;
|
||||
|
||||
const ruleCardTitle = useMemo(() => {
|
||||
const raw = typeof state.title === "string" ? state.title.trim() : "";
|
||||
@@ -109,6 +233,18 @@ export function FinalReviewScreen() {
|
||||
className={CREATE_FLOW_REVIEW_RULE_CARD_LAYOUT_CLASS}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
<FinalReviewChipEditModal
|
||||
isOpen={activeEditTarget !== null}
|
||||
onClose={() => setActiveEditTarget(null)}
|
||||
target={activeEditTarget}
|
||||
state={state}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
<TemplateChipDetailModal
|
||||
isOpen={activeReadOnlyDetail !== null}
|
||||
onClose={() => setActiveReadOnlyDetail(null)}
|
||||
detail={activeReadOnlyDetail}
|
||||
/>
|
||||
</CreateFlowLockupCardStepShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -3,14 +3,17 @@
|
||||
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import MultiSelect from "../../../../components/controls/MultiSelect";
|
||||
import type { ChipOption } from "../../../../components/controls/MultiSelect/MultiSelect.types";
|
||||
import TextArea from "../../../../components/controls/TextArea";
|
||||
import Create from "../../../../components/modals/Create";
|
||||
import ContentLockup from "../../../../components/type/ContentLockup";
|
||||
import { useMessages } from "../../../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import type { CommunityStructureChipSnapshotRow } from "../../types";
|
||||
import type {
|
||||
CommunityStructureChipSnapshotRow,
|
||||
CoreValueDetailEntry,
|
||||
} from "../../types";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
|
||||
import { CoreValueEditFields } from "../../components/methodEditFields";
|
||||
|
||||
const MAX_CORE_VALUES = 5;
|
||||
|
||||
@@ -96,6 +99,8 @@ function snapshotRowsToChipOptions(
|
||||
}));
|
||||
}
|
||||
|
||||
const EMPTY_DETAIL: CoreValueDetailEntry = { meaning: "", signals: "" };
|
||||
|
||||
/** Create Custom — Core Values (Figma `20264:68378`). Up to five selections; preset list + custom chips. */
|
||||
export function CoreValuesSelectScreen() {
|
||||
const m = useMessages();
|
||||
@@ -122,8 +127,7 @@ export function CoreValuesSelectScreen() {
|
||||
null,
|
||||
);
|
||||
const [modalSession, setModalSession] = useState<ModalSession | null>(null);
|
||||
const [draftMeaning, setDraftMeaning] = useState("");
|
||||
const [draftSignals, setDraftSignals] = useState("");
|
||||
const [draft, setDraft] = useState<CoreValueDetailEntry>(EMPTY_DETAIL);
|
||||
|
||||
useEffect(() => {
|
||||
const fromSnap = snapshotRowsToChipOptions(state.coreValuesChipsSnapshot);
|
||||
@@ -158,16 +162,16 @@ export function CoreValuesSelectScreen() {
|
||||
|
||||
/** Default meaning/signals from `coreValues.json` `values` for each preset label. */
|
||||
const getPresetTexts = useCallback(
|
||||
(valueLabel: string): { meaning: string; signals: string } => {
|
||||
(valueLabel: string): CoreValueDetailEntry => {
|
||||
const row = presets.find((p) => p.label === valueLabel);
|
||||
if (!row) return { meaning: "", signals: "" };
|
||||
if (!row) return EMPTY_DETAIL;
|
||||
return { meaning: row.meaning, signals: row.signals };
|
||||
},
|
||||
[presets],
|
||||
);
|
||||
|
||||
const getInitialTexts = useCallback(
|
||||
(chipId: string, valueLabel: string) => {
|
||||
(chipId: string, valueLabel: string): CoreValueDetailEntry => {
|
||||
const saved = state.coreValueDetailsByChipId?.[chipId];
|
||||
const preset = getPresetTexts(valueLabel);
|
||||
return {
|
||||
@@ -180,9 +184,7 @@ export function CoreValuesSelectScreen() {
|
||||
|
||||
const openModal = useCallback(
|
||||
(chipId: string, session: ModalSession, valueLabel: string) => {
|
||||
const initial = getInitialTexts(chipId, valueLabel);
|
||||
setDraftMeaning(initial.meaning);
|
||||
setDraftSignals(initial.signals);
|
||||
setDraft(getInitialTexts(chipId, valueLabel));
|
||||
setActiveModalChipId(chipId);
|
||||
setModalSession(session);
|
||||
markCreateFlowInteraction();
|
||||
@@ -190,6 +192,14 @@ export function CoreValuesSelectScreen() {
|
||||
[getInitialTexts, markCreateFlowInteraction],
|
||||
);
|
||||
|
||||
const handleDraftChange = useCallback(
|
||||
(next: CoreValueDetailEntry) => {
|
||||
markCreateFlowInteraction();
|
||||
setDraft(next);
|
||||
},
|
||||
[markCreateFlowInteraction],
|
||||
);
|
||||
|
||||
const handleModalDismiss = useCallback(() => {
|
||||
if (activeModalChipId && modalSession === "pending") {
|
||||
const next = coreValueOptions.map((opt) =>
|
||||
@@ -208,19 +218,17 @@ export function CoreValuesSelectScreen() {
|
||||
markCreateFlowInteraction();
|
||||
updateState({
|
||||
coreValueDetailsByChipId: {
|
||||
[activeModalChipId]: {
|
||||
meaning: draftMeaning,
|
||||
signals: draftSignals,
|
||||
},
|
||||
...(state.coreValueDetailsByChipId ?? {}),
|
||||
[activeModalChipId]: draft,
|
||||
},
|
||||
});
|
||||
setActiveModalChipId(null);
|
||||
setModalSession(null);
|
||||
}, [
|
||||
activeModalChipId,
|
||||
draftMeaning,
|
||||
draftSignals,
|
||||
draft,
|
||||
markCreateFlowInteraction,
|
||||
state.coreValueDetailsByChipId,
|
||||
updateState,
|
||||
]);
|
||||
|
||||
@@ -374,26 +382,7 @@ export function CoreValuesSelectScreen() {
|
||||
nextButtonText={detailModal.addValueButton}
|
||||
ariaLabel={modalChipLabel || "Core value details"}
|
||||
>
|
||||
<div className="flex flex-col gap-[var(--measures-spacing-600,24px)] pb-2">
|
||||
<TextArea
|
||||
label={detailModal.meaningLabel}
|
||||
showHelpIcon
|
||||
appearance="embedded"
|
||||
size="medium"
|
||||
value={draftMeaning}
|
||||
onChange={(e) => setDraftMeaning(e.target.value)}
|
||||
rows={4}
|
||||
/>
|
||||
<TextArea
|
||||
label={detailModal.signalsLabel}
|
||||
showHelpIcon
|
||||
appearance="embedded"
|
||||
size="medium"
|
||||
value={draftSignals}
|
||||
onChange={(e) => setDraftSignals(e.target.value)}
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
<CoreValueEditFields value={draft} onChange={handleDraftChange} />
|
||||
</Create>
|
||||
)}
|
||||
</CreateFlowTwoColumnSelectShell>
|
||||
|
||||
Reference in New Issue
Block a user