Files
community-rule/app/(app)/create/components/FinalReviewChipEditModal.tsx
T
adilalloandCursor 20ea4a2f30 Give MultiSelect chips accessible names and start creation fields empty.
Catalog chips now announce Select or Deselect so they are named for assistive tech. Method and value modals open blank; Generate draft inserts the catalog copy that used to be prefilled, and Customize still keeps the section headings.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-29 15:59:00 -06:00

1454 lines
45 KiB
TypeScript

"use client";
/**
* Final-review chip modal: **Core values** and **method** facets share the
* kebab → **Duplicate** (values only when under the cap) / **Remove** pattern
* from the create-card facet modals (`Create` +
* {@link buildCustomRuleModalKebabMenu}), plus a footer **Remove** when the
* chip is already in the selection. Values and method chips also offer
* **Customize**, which opens {@link CustomMethodCardWizard} prefilled from the
* chip. Fields are editable on open; Save persists body edits without renaming.
*
* Template-only chips without an `overrideKey` never mount this component; they
* use {@link TemplateChipDetailModal} from the parent.
*
* @see CommunicationMethodsScreen — mental model for method modals.
*/
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import Create from "../../../components/modals/Create";
import ContentLockup from "../../../components/type/ContentLockup";
import { useMessages, useTranslation } from "../../../contexts/MessagesContext";
import {
CommunicationMethodEditFields,
ConflictManagementEditFields,
CoreValueEditFields,
DecisionApproachEditFields,
MembershipMethodEditFields,
} from "./methodEditFields";
import CustomMethodCardModalBody from "./CustomMethodCardModalBody";
import { buildCustomRuleModalKebabMenu } from "./customRuleModalKebabMenu";
import { useDiscardCustomizeConfirm } from "../hooks/useDiscardCustomizeConfirm";
import { useBeforeUnloadGuard } from "../../../hooks/useBeforeUnloadGuard";
import {
communicationPresetFor,
conflictManagementPresetFor,
coreValuePresetFor,
decisionApproachPresetFor,
emptyCommunicationDetails,
emptyConflictManagementDetails,
emptyDecisionApproachTextFields,
emptyMembershipDetails,
membershipPresetFor,
} from "../../../../lib/create/finalReviewChipPresets";
import { isCustomMethodCardId } from "../../../../lib/create/isCustomMethodCardId";
import { usesWizardFieldBlocksModalBody } from "../../../../lib/create/usesWizardFieldBlocksModalBody";
import type { CustomMethodCardFieldBlock } from "../../../../lib/create/customMethodCardFieldBlocks";
import {
CUSTOM_RULE_FACET_BY_GROUP,
isCatalogMethodCardId,
type TemplateFacetGroupKey,
} from "../../../../lib/create/customRuleFacets";
import type { MethodFacetGroupKey } from "../../../../lib/create/removeMethodCardFromFacetSelection";
import { removeMethodCardFromFacetSelection } from "../../../../lib/create/removeMethodCardFromFacetSelection";
import { mergePresetMethodsWithCustom } from "../../../../lib/create/mergePresetMethodsWithCustom";
import { moveFacetSelectionIdToFront } from "../../../../lib/create/methodCardSelectionOrder";
import {
buildMethodCardWizardInitialValues,
coreValueDetailsFromWizardFieldBlocks,
overlayFacetPrefillValues,
} from "../../../../lib/create/methodCardWizardPrefill";
import type {
MethodCardWizardFacetPrefill,
MethodCardWizardInitialValues,
} from "../../../../lib/create/methodCardWizardPrefill";
import { uploadCreateFlowFile } from "../../../../lib/create/uploadToServer";
import CustomMethodCardWizard from "./CustomMethodCardWizard";
import {
duplicateCoreValueChipInDraft,
removeCoreValueChipFromDraft,
} from "../../../../lib/create/coreValueChipFacet";
import {
captureMethodCardCustomizeSnapshot,
isMethodCardCustomizeSessionDirty,
type MethodCardCustomizeSnapshot,
type MethodCardHeaderDraft,
} from "../../../../lib/create/methodCardCustomizeSession";
import type {
CommunicationMethodDetailEntry,
ConflictManagementDetailEntry,
CoreValueDetailEntry,
CreateFlowState,
DecisionApproachDetailEntry,
MembershipMethodDetailEntry,
} from "../types";
export type FinalReviewChipEditTarget = {
/** Stable key for override lookup: preset id (methods) or chip id (core values). */
overrideKey: string;
/** Category group that decides which field set to render. */
groupKey: TemplateFacetGroupKey;
/** Display label shown at the top of the modal (localized chip label). */
chipLabel: string;
};
export type FinalReviewChipEditPatch =
| {
groupKey: "coreValues";
overrideKey: string;
value: CoreValueDetailEntry;
/** When set, updates the display label for this chip id in `coreValuesChipsSnapshot`. */
chipLabel?: string;
customMethodCardFieldBlocks?: CustomMethodCardFieldBlock[];
}
| {
groupKey: "communication";
overrideKey: string;
value: CommunicationMethodDetailEntry;
customMethodCardFieldBlocks?: CustomMethodCardFieldBlock[];
methodCardMeta?: { label: string; supportText: string };
}
| {
groupKey: "membership";
overrideKey: string;
value: MembershipMethodDetailEntry;
customMethodCardFieldBlocks?: CustomMethodCardFieldBlock[];
methodCardMeta?: { label: string; supportText: string };
}
| {
groupKey: "decisionApproaches";
overrideKey: string;
value: DecisionApproachDetailEntry;
customMethodCardFieldBlocks?: CustomMethodCardFieldBlock[];
methodCardMeta?: { label: string; supportText: string };
}
| {
groupKey: "conflictManagement";
overrideKey: string;
value: ConflictManagementDetailEntry;
customMethodCardFieldBlocks?: CustomMethodCardFieldBlock[];
methodCardMeta?: { label: string; supportText: string };
};
export interface FinalReviewChipEditModalProps {
isOpen: boolean;
onClose: () => void;
target: FinalReviewChipEditTarget | null;
state: CreateFlowState;
onSave: (_patch: FinalReviewChipEditPatch) => void;
replaceState: (_updater: (prev: CreateFlowState) => CreateFlowState) => void;
onInteract?: () => void;
/** After core-value **Duplicate**, re-point the open modal at the new chip id. */
onEditTargetChange?: (_next: FinalReviewChipEditTarget) => void;
}
type Draft =
| { groupKey: "coreValues"; value: CoreValueDetailEntry }
| { groupKey: "communication"; value: CommunicationMethodDetailEntry }
| { groupKey: "membership"; value: MembershipMethodDetailEntry }
| { groupKey: "decisionApproaches"; value: DecisionApproachDetailEntry }
| { groupKey: "conflictManagement"; value: ConflictManagementDetailEntry };
type MethodDetailDraft =
| CommunicationMethodDetailEntry
| MembershipMethodDetailEntry
| DecisionApproachDetailEntry
| ConflictManagementDetailEntry;
function methodDetailDraftForCustomizeSession(
draft: Draft | null,
): MethodDetailDraft | null {
if (!draft || draft.groupKey === "coreValues") return null;
return draft.value;
}
function isMethodFacetGroup(
k: TemplateFacetGroupKey,
): k is MethodFacetGroupKey {
return k !== "coreValues";
}
export function FinalReviewChipEditModal({
isOpen,
onClose,
target,
state,
onSave,
replaceState,
onInteract,
onEditTargetChange,
}: FinalReviewChipEditModalProps) {
const m = useMessages();
const cr = m.create.customRule;
const tCv = cr.coreValues;
const tComm = cr.communication;
const tMem = cr.membership;
const tDa = cr.decisionApproaches;
const tCm = cr.conflictManagement;
const modalKebabMenu = cr.modalKebabMenu;
const tModal = useTranslation(
"create.reviewAndComplete.finalReview.chipEditModal",
);
const { confirmDiscard, confirmDialog } = useDiscardCustomizeConfirm();
const [draft, setDraft] = useState<Draft | null>(null);
const [draftFieldBlocks, setDraftFieldBlocks] = useState<
CustomMethodCardFieldBlock[] | null
>(null);
const [customizeHeaderDraft, setCustomizeHeaderDraft] =
useState<MethodCardHeaderDraft | null>(null);
const [addCustomWizardOpen, setAddCustomWizardOpen] = useState(false);
const [wizardCustomizeCardId, setWizardCustomizeCardId] = useState<
string | null
>(null);
const [wizardInitialValues, setWizardInitialValues] =
useState<MethodCardWizardInitialValues | null>(null);
const initialSnapshotRef = useRef("");
const seededTargetRef = useRef<string | null>(null);
const customizeSnapshotRef = useRef<
| MethodCardCustomizeSnapshot<
| CommunicationMethodDetailEntry
| MembershipMethodDetailEntry
| DecisionApproachDetailEntry
| ConflictManagementDetailEntry
>
| null
>(null);
const coreCustomizeSnapshotRef =
useRef<MethodCardCustomizeSnapshot<CoreValueDetailEntry> | null>(null);
const pendingEphemeralCoreDuplicateRef = useRef<string | null>(null);
const methodById = useMemo(() => {
if (!target || !isMethodFacetGroup(target.groupKey)) {
return new Map<string, { id: string; label: string; supportText: string }>();
}
const facet = CUSTOM_RULE_FACET_BY_GROUP.get(target.groupKey)!;
const selectedIds = facet.selectionIds(state);
switch (target.groupKey) {
case "communication":
return new Map(
mergePresetMethodsWithCustom(
tComm.methods,
selectedIds,
state.customMethodCardMetaById,
).map((row) => [row.id, row]),
);
case "membership":
return new Map(
mergePresetMethodsWithCustom(
tMem.methods,
selectedIds,
state.customMethodCardMetaById,
).map((row) => [row.id, row]),
);
case "decisionApproaches":
return new Map(
mergePresetMethodsWithCustom(
tDa.methods,
selectedIds,
state.customMethodCardMetaById,
).map((row) => [row.id, row]),
);
case "conflictManagement":
return new Map(
mergePresetMethodsWithCustom(
tCm.methods,
selectedIds,
state.customMethodCardMetaById,
).map((row) => [row.id, row]),
);
}
}, [
target,
state.customMethodCardMetaById,
state.selectedCommunicationMethodIds,
state.selectedMembershipMethodIds,
state.selectedDecisionApproachIds,
state.selectedConflictManagementIds,
tComm.methods,
tMem.methods,
tDa.methods,
tCm.methods,
]);
const selectionIdsForTarget = useMemo(() => {
if (!target || !isMethodFacetGroup(target.groupKey)) return [];
return [...CUSTOM_RULE_FACET_BY_GROUP.get(target.groupKey)!.selectionIds(state)];
}, [
target,
state.selectedCommunicationMethodIds,
state.selectedMembershipMethodIds,
state.selectedDecisionApproachIds,
state.selectedConflictManagementIds,
]);
const isChipInSelection =
target && isMethodFacetGroup(target.groupKey)
? selectionIdsForTarget.includes(target.overrideKey)
: false;
useEffect(() => {
if (!isOpen || !target) return;
const sig = facetSeedSignature(target, state);
const targetKey = `${target.groupKey}:${target.overrideKey}:${sig}`;
if (seededTargetRef.current === targetKey) {
return;
}
const seed = seedDraftForTarget(target, state);
setDraft(seed);
initialSnapshotRef.current = JSON.stringify(seed.value);
if (target.groupKey === "coreValues") {
const persisted =
state.customMethodCardFieldBlocksById?.[target.overrideKey];
const initialBlocks =
Array.isArray(persisted) && persisted.length > 0
? structuredClone(persisted)
: null;
const headerDraft: MethodCardHeaderDraft = {
title: target.chipLabel,
description: "",
};
setCustomizeHeaderDraft(headerDraft);
coreCustomizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
seed.value as CoreValueDetailEntry,
initialBlocks,
headerDraft,
);
setDraftFieldBlocks(initialBlocks);
}
if (isMethodFacetGroup(target.groupKey) && seed.groupKey !== "coreValues") {
const persisted =
state.customMethodCardFieldBlocksById?.[target.overrideKey];
const initialBlocks =
Array.isArray(persisted) && persisted.length > 0
? structuredClone(persisted)
: isCustomMethodCardId(
target.overrideKey,
state.customMethodCardMetaById,
)
? structuredClone(persisted ?? [])
: null;
const method = methodById.get(target.overrideKey);
const meta = state.customMethodCardMetaById?.[target.overrideKey];
const headerDraft: MethodCardHeaderDraft = {
title: meta?.label ?? method?.label ?? target.chipLabel,
description: meta?.supportText ?? method?.supportText ?? "",
};
setCustomizeHeaderDraft(headerDraft);
customizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
seed.value as MethodDetailDraft,
initialBlocks,
headerDraft,
);
setDraftFieldBlocks(initialBlocks);
}
seededTargetRef.current = targetKey;
}, [isOpen, target, state, methodById]);
useEffect(() => {
if (!isOpen) seededTargetRef.current = null;
}, [isOpen]);
const coreCustomizeSaveDisabled = useMemo(() => {
const snap = coreCustomizeSnapshotRef.current;
if (!snap || !draft || draft.groupKey !== "coreValues") return true;
return !isMethodCardCustomizeSessionDirty(
snap,
draft.value,
draftFieldBlocks,
customizeHeaderDraft,
);
}, [customizeHeaderDraft, draft, draftFieldBlocks]);
const methodCustomizeSaveDisabled = useMemo(() => {
const snap = customizeSnapshotRef.current;
if (!snap) return true;
return !isMethodCardCustomizeSessionDirty(
snap,
methodDetailDraftForCustomizeSession(draft),
draftFieldBlocks,
customizeHeaderDraft,
);
}, [
customizeHeaderDraft,
draft,
draftFieldBlocks,
]);
useBeforeUnloadGuard(
isOpen &&
!addCustomWizardOpen &&
(!coreCustomizeSaveDisabled || !methodCustomizeSaveDisabled),
);
const modalUsesWizardFieldBlocksBody = Boolean(
target &&
(usesWizardFieldBlocksModalBody({
methodId: target.overrideKey,
meta: state.customMethodCardMetaById,
fieldBlocksById: state.customMethodCardFieldBlocksById,
modalEditUnlocked: true,
draftFieldBlocks,
}) ||
(isCustomMethodCardId(
target.overrideKey,
state.customMethodCardMetaById,
) &&
!isCatalogMethodCardId(target.overrideKey))),
);
const handleWizardFieldBlocksChange = useCallback(
(next: CustomMethodCardFieldBlock[]) => {
setDraftFieldBlocks(next);
setDraft((prev) => {
if (!prev || prev.groupKey !== "coreValues") {
return prev;
}
return {
groupKey: "coreValues",
value: coreValueDetailsFromWizardFieldBlocks(next, prev.value),
};
});
},
[],
);
const finalizeModalClose = useCallback(() => {
customizeSnapshotRef.current = null;
coreCustomizeSnapshotRef.current = null;
pendingEphemeralCoreDuplicateRef.current = null;
setDraftFieldBlocks(null);
setCustomizeHeaderDraft(null);
setAddCustomWizardOpen(false);
setWizardCustomizeCardId(null);
setWizardInitialValues(null);
onClose();
}, [onClose]);
const handleModalClose = useCallback(async () => {
if (
target &&
target.groupKey === "coreValues" &&
!(await confirmDiscard(
true,
coreCustomizeSnapshotRef.current,
draft?.groupKey === "coreValues" ? draft.value : null,
draftFieldBlocks,
customizeHeaderDraft,
))
) {
return;
}
if (
target &&
isMethodFacetGroup(target.groupKey) &&
!(await confirmDiscard(
true,
customizeSnapshotRef.current,
methodDetailDraftForCustomizeSession(draft),
draftFieldBlocks,
customizeHeaderDraft,
))
) {
return;
}
const ep = pendingEphemeralCoreDuplicateRef.current;
if (ep) {
replaceState((prev) => ({
...prev,
...removeCoreValueChipFromDraft(prev, ep),
}));
}
finalizeModalClose();
}, [
confirmDiscard,
customizeHeaderDraft,
draft,
draftFieldBlocks,
finalizeModalClose,
replaceState,
target,
]);
const handleCustomize = useCallback(() => {
onInteract?.();
if (!target) {
return;
}
if (target.groupKey === "coreValues") {
const value =
draft?.groupKey === "coreValues"
? draft.value
: coreValuePresetFor(target.overrideKey);
setWizardInitialValues(
buildMethodCardWizardInitialValues({
cardId: target.overrideKey,
fallbackTitle: target.chipLabel,
fallbackDescription:
value.supportText?.trim() || tCv.detailModal.subtitle,
meta: {},
persistedBlocks: state.customMethodCardFieldBlocksById,
draftFieldBlocks: null,
facetPrefill: {
group: "coreValues",
draft: value,
headings: {
meaning: tCv.detailModal.meaningLabel,
signals: tCv.detailModal.signalsLabel,
},
},
}),
);
setWizardCustomizeCardId(target.overrideKey);
setAddCustomWizardOpen(true);
return;
}
if (!isMethodFacetGroup(target.groupKey)) {
return;
}
const method = methodById.get(target.overrideKey);
const meta = state.customMethodCardMetaById?.[target.overrideKey];
let facetPrefill: MethodCardWizardFacetPrefill | undefined;
if (draft && draft.groupKey === "communication") {
facetPrefill = {
group: "communication",
draft: draft.value,
headings: tComm.sectionHeadings,
};
} else if (draft && draft.groupKey === "membership") {
facetPrefill = {
group: "membership",
draft: draft.value,
headings: tMem.sectionHeadings,
};
} else if (draft && draft.groupKey === "decisionApproaches") {
facetPrefill = {
group: "decisionApproaches",
draft: draft.value,
headings: tDa.sectionHeadings,
};
} else if (draft && draft.groupKey === "conflictManagement") {
facetPrefill = {
group: "conflictManagement",
draft: draft.value,
headings: tCm.sectionHeadings,
};
}
setWizardInitialValues(
buildMethodCardWizardInitialValues({
cardId: target.overrideKey,
fallbackTitle: method?.label ?? target.chipLabel,
fallbackDescription: method?.supportText ?? meta?.supportText ?? "",
meta: state.customMethodCardMetaById,
persistedBlocks: state.customMethodCardFieldBlocksById,
draftFieldBlocks,
facetPrefill,
}),
);
setWizardCustomizeCardId(target.overrideKey);
setAddCustomWizardOpen(true);
}, [
draft,
draftFieldBlocks,
methodById,
onInteract,
state.customMethodCardFieldBlocksById,
state.customMethodCardMetaById,
tCm.sectionHeadings,
tComm.sectionHeadings,
tCv.detailModal.meaningLabel,
tCv.detailModal.signalsLabel,
tCv.detailModal.subtitle,
tDa.sectionHeadings,
tMem.sectionHeadings,
target,
]);
const handleRemoveSelectedFromModal = useCallback(async () => {
if (!target || !isMethodFacetGroup(target.groupKey)) {
return;
}
const methodGroupKey = target.groupKey;
if (!selectionIdsForTarget.includes(target.overrideKey)) {
return;
}
onInteract?.();
if (
!(await confirmDiscard(
true,
customizeSnapshotRef.current,
methodDetailDraftForCustomizeSession(draft),
draftFieldBlocks,
customizeHeaderDraft,
))
) {
return;
}
customizeSnapshotRef.current = null;
replaceState((prev) => ({
...prev,
...removeMethodCardFromFacetSelection(
prev,
methodGroupKey,
target.overrideKey,
),
}));
finalizeModalClose();
}, [
confirmDiscard,
customizeHeaderDraft,
draft,
draftFieldBlocks,
finalizeModalClose,
onInteract,
replaceState,
selectionIdsForTarget,
target,
]);
const handleRemoveCoreValueFromModal = useCallback(async () => {
if (!target || target.groupKey !== "coreValues") {
return;
}
onInteract?.();
if (
!(await confirmDiscard(
true,
coreCustomizeSnapshotRef.current,
draft?.groupKey === "coreValues" ? draft.value : null,
draftFieldBlocks,
customizeHeaderDraft,
))
) {
return;
}
coreCustomizeSnapshotRef.current = null;
customizeSnapshotRef.current = null;
replaceState((prev) => ({
...prev,
...removeCoreValueChipFromDraft(prev, target.overrideKey),
}));
finalizeModalClose();
}, [
confirmDiscard,
customizeHeaderDraft,
draft,
draftFieldBlocks,
finalizeModalClose,
onInteract,
replaceState,
target,
]);
const handleDuplicateCoreValue = useCallback(async () => {
if (
!target ||
target.groupKey !== "coreValues" ||
draft?.groupKey !== "coreValues"
) {
return;
}
if ((state.editingPublishedRuleId?.trim() ?? "") !== "") {
return;
}
if ((state.selectedCoreValueIds ?? []).length >= 5) {
return;
}
if (
!(await confirmDiscard(
true,
coreCustomizeSnapshotRef.current,
draft.value,
draftFieldBlocks,
customizeHeaderDraft,
))
) {
return;
}
onInteract?.();
const priorEphemeral = pendingEphemeralCoreDuplicateRef.current;
let outcome: ReturnType<
typeof duplicateCoreValueChipInDraft
> | null = null;
replaceState((prev) => {
const base =
priorEphemeral != null
? { ...prev, ...removeCoreValueChipFromDraft(prev, priorEphemeral) }
: prev;
const res = duplicateCoreValueChipInDraft(
base,
target.overrideKey,
modalKebabMenu.duplicateTitleSuffix,
);
if (!res) {
return prev;
}
outcome = res;
return { ...base, ...res.patch };
});
if (!outcome) {
return;
}
customizeSnapshotRef.current = null;
coreCustomizeSnapshotRef.current = null;
setDraftFieldBlocks(null);
setCustomizeHeaderDraft(null);
pendingEphemeralCoreDuplicateRef.current = outcome.newId;
seededTargetRef.current = null;
setDraft({
groupKey: "coreValues",
value: structuredClone(draft.value),
});
onEditTargetChange?.({
overrideKey: outcome.newId,
groupKey: "coreValues",
chipLabel: outcome.newLabel,
});
}, [
confirmDiscard,
customizeHeaderDraft,
draft,
draftFieldBlocks,
modalKebabMenu.duplicateTitleSuffix,
onEditTargetChange,
onInteract,
replaceState,
state.editingPublishedRuleId,
state.selectedCoreValueIds,
target,
]);
const kebabMenuItems = useMemo(() => {
if (!target) return [];
if (target.groupKey === "coreValues") {
return buildCustomRuleModalKebabMenu(modalKebabMenu, {
showCustomize: true,
onCustomize: handleCustomize,
onDuplicate:
(state.editingPublishedRuleId?.trim() ?? "") !== "" ||
(state.selectedCoreValueIds ?? []).length >= 5
? undefined
: handleDuplicateCoreValue,
showRemove: true,
onRemove: handleRemoveCoreValueFromModal,
});
}
if (!isMethodFacetGroup(target.groupKey)) return [];
return buildCustomRuleModalKebabMenu(modalKebabMenu, {
showCustomize: true,
onCustomize: handleCustomize,
showRemove: isChipInSelection,
onRemove: handleRemoveSelectedFromModal,
});
}, [
handleCustomize,
handleDuplicateCoreValue,
handleRemoveCoreValueFromModal,
handleRemoveSelectedFromModal,
isChipInSelection,
modalKebabMenu,
state.editingPublishedRuleId,
state.selectedCoreValueIds,
target,
]);
const subtitle = useMemo(() => {
if (!target) return "";
return subtitleForTarget(
target,
{ tCv, tComm, tMem, tDa, tCm },
state.customMethodCardMetaById,
state.coreValueDetailsByChipId?.[target.overrideKey]?.supportText,
);
}, [
target,
tCv,
tComm,
tMem,
tDa,
tCm,
state.customMethodCardMetaById,
state.coreValueDetailsByChipId,
]);
const handleCoreSave = useCallback(() => {
if (!target || !draft || draft.groupKey !== "coreValues") {
return;
}
if (coreCustomizeSaveDisabled) {
return;
}
onInteract?.();
const existingBlocks =
state.customMethodCardFieldBlocksById?.[target.overrideKey];
onSave({
groupKey: "coreValues",
overrideKey: target.overrideKey,
value: structuredClone(draft.value),
...(existingBlocks && existingBlocks.length > 0
? {
customMethodCardFieldBlocks: overlayFacetPrefillValues(
existingBlocks,
{
group: "coreValues",
draft: draft.value,
headings: {
meaning: tCv.detailModal.meaningLabel,
signals: tCv.detailModal.signalsLabel,
},
},
),
}
: {}),
});
coreCustomizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
draft.value,
draftFieldBlocks,
customizeHeaderDraft ?? {
title: target.chipLabel,
description: "",
},
);
initialSnapshotRef.current = JSON.stringify(draft.value);
pendingEphemeralCoreDuplicateRef.current = null;
}, [
coreCustomizeSaveDisabled,
customizeHeaderDraft,
draft,
draftFieldBlocks,
onInteract,
onSave,
state.customMethodCardFieldBlocksById,
tCv.detailModal.meaningLabel,
tCv.detailModal.signalsLabel,
target,
]);
const handleMethodPrimary = useCallback(() => {
if (!target || !draft || !isMethodFacetGroup(target.groupKey)) return;
if (!isMethodFacetGroup(draft.groupKey)) return;
const facet = CUSTOM_RULE_FACET_BY_GROUP.get(target.groupKey)!;
const pendingId = target.overrideKey;
const sel = [...facet.selectionIds(state)];
onInteract?.();
const persistWizardBlocks = draftFieldBlocks !== null;
const blocksPayload = persistWizardBlocks
? structuredClone(draftFieldBlocks ?? [])
: undefined;
switch (draft.groupKey) {
case "communication":
onSave({
groupKey: "communication",
overrideKey: pendingId,
value: draft.value,
...(blocksPayload !== undefined
? { customMethodCardFieldBlocks: blocksPayload }
: {}),
});
break;
case "membership":
onSave({
groupKey: "membership",
overrideKey: pendingId,
value: draft.value,
...(blocksPayload !== undefined
? { customMethodCardFieldBlocks: blocksPayload }
: {}),
});
break;
case "decisionApproaches":
onSave({
groupKey: "decisionApproaches",
overrideKey: pendingId,
value: draft.value,
...(blocksPayload !== undefined
? { customMethodCardFieldBlocks: blocksPayload }
: {}),
});
break;
case "conflictManagement":
onSave({
groupKey: "conflictManagement",
overrideKey: pendingId,
value: draft.value,
...(blocksPayload !== undefined
? { customMethodCardFieldBlocks: blocksPayload }
: {}),
});
break;
}
if (!sel.includes(pendingId)) {
replaceState((prev) => ({
...prev,
[facet.selectedIdsStateKey]: moveFacetSelectionIdToFront(
[...facet.selectionIds(prev)],
pendingId,
),
}));
finalizeModalClose();
return;
}
const sessionDraft = methodDetailDraftForCustomizeSession(draft);
if (sessionDraft) {
customizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
sessionDraft,
draftFieldBlocks,
customizeHeaderDraft ?? { title: "", description: "" },
);
}
}, [
customizeHeaderDraft,
draft,
draftFieldBlocks,
finalizeModalClose,
onInteract,
onSave,
replaceState,
state,
target,
]);
const handleCloseAddWizard = useCallback(() => {
setAddCustomWizardOpen(false);
setWizardCustomizeCardId(null);
setWizardInitialValues(null);
}, []);
const handleFinalizeCustomCard = useCallback(
({
title,
description,
fieldBlocks,
}: {
title: string;
description: string;
fieldBlocks: CustomMethodCardFieldBlock[];
}) => {
if (!target || !draft) {
return;
}
if (target.groupKey === "coreValues" && draft.groupKey === "coreValues") {
const existingId = wizardCustomizeCardId ?? target.overrideKey;
onInteract?.();
const details = coreValueDetailsFromWizardFieldBlocks(
fieldBlocks,
draft.value,
);
const trimmedDescription = description.trim();
onSave({
groupKey: "coreValues",
overrideKey: existingId,
value: {
...details,
...(trimmedDescription.length > 0
? { supportText: trimmedDescription }
: {}),
},
chipLabel: title,
customMethodCardFieldBlocks: structuredClone(fieldBlocks),
});
setDraft({
groupKey: "coreValues",
value: {
...details,
...(trimmedDescription.length > 0
? { supportText: trimmedDescription }
: {}),
},
});
setDraftFieldBlocks(structuredClone(fieldBlocks));
coreCustomizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
{
...details,
...(trimmedDescription.length > 0
? { supportText: trimmedDescription }
: {}),
},
fieldBlocks,
{ title, description },
);
setAddCustomWizardOpen(false);
setWizardCustomizeCardId(null);
setWizardInitialValues(null);
return;
}
if (
!isMethodFacetGroup(target.groupKey) ||
!isMethodFacetGroup(draft.groupKey)
) {
return;
}
const existingId = wizardCustomizeCardId ?? target.overrideKey;
onInteract?.();
const meta = { label: title, supportText: description };
const blocks = structuredClone(fieldBlocks);
switch (draft.groupKey) {
case "communication":
onSave({
groupKey: "communication",
overrideKey: existingId,
value: draft.value,
methodCardMeta: meta,
customMethodCardFieldBlocks: blocks,
});
break;
case "membership":
onSave({
groupKey: "membership",
overrideKey: existingId,
value: draft.value,
methodCardMeta: meta,
customMethodCardFieldBlocks: blocks,
});
break;
case "decisionApproaches":
onSave({
groupKey: "decisionApproaches",
overrideKey: existingId,
value: draft.value,
methodCardMeta: meta,
customMethodCardFieldBlocks: blocks,
});
break;
case "conflictManagement":
onSave({
groupKey: "conflictManagement",
overrideKey: existingId,
value: draft.value,
methodCardMeta: meta,
customMethodCardFieldBlocks: blocks,
});
break;
}
customizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
draft.value,
blocks,
{ title, description },
);
setDraftFieldBlocks(blocks);
setAddCustomWizardOpen(false);
setWizardCustomizeCardId(null);
setWizardInitialValues(null);
},
[
draft,
onInteract,
onSave,
target,
wizardCustomizeCardId,
],
);
const handleNext = useCallback(() => {
if (!target || !draft) return;
if (target.groupKey === "coreValues") {
handleCoreSave();
} else {
handleMethodPrimary();
}
}, [draft, handleCoreSave, handleMethodPrimary, target]);
const nextButtonText = useMemo(() => {
if (!target) return tModal("saveButton");
if (target.groupKey === "coreValues") {
return tModal("saveButton");
}
if (!isChipInSelection) {
return addPrimaryLabelForMethodFacet(target.groupKey, cr);
}
return tModal("saveButton");
}, [cr, isChipInSelection, target, tModal]);
const headerContent = useMemo(() => {
if (!target) return undefined;
return (
<div className="bg-[var(--color-surface-default-primary)] px-[24px] py-[12px] shrink-0">
<ContentLockup
title={target.chipLabel}
description={subtitle}
variant="modal"
alignment="left"
/>
</div>
);
}, [subtitle, target]);
const showFooterRemove =
target != null &&
(target.groupKey === "coreValues" || isChipInSelection);
return (
<>
<Create
isOpen={isOpen && !addCustomWizardOpen}
onClose={handleModalClose}
onBack={handleModalClose}
showBackButton={!showFooterRemove}
showRemoveButton={showFooterRemove}
onRemove={
target?.groupKey === "coreValues"
? handleRemoveCoreValueFromModal
: handleRemoveSelectedFromModal
}
backdropVariant="blurredYellow"
headerContent={headerContent}
showNextButton={true}
onNext={handleNext}
nextButtonText={nextButtonText}
nextButtonDisabled={
target?.groupKey === "coreValues"
? coreCustomizeSaveDisabled
: methodCustomizeSaveDisabled
}
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
kebabMenuAriaLabel={modalKebabMenu.menuAriaLabel}
kebabMenuItems={
target && kebabMenuItems.length > 0 ? kebabMenuItems : undefined
}
ariaLabel={target?.chipLabel || "Edit chip details"}
>
<div className="flex flex-col gap-[var(--measures-spacing-600,24px)] pb-2">
{draft?.groupKey === "coreValues" &&
target &&
(modalUsesWizardFieldBlocksBody ? (
<CustomMethodCardModalBody
cardId={target.overrideKey}
blocksById={state.customMethodCardFieldBlocksById}
blocksOverride={
draftFieldBlocks !== null ? draftFieldBlocks : undefined
}
showPolicyContentLockupWhenNoBlocks={false}
onFieldBlocksChange={
draftFieldBlocks === null
? undefined
: handleWizardFieldBlocksChange
}
/>
) : (
<CoreValueEditFields
value={draft.value}
onChange={(value) => setDraft({ groupKey: "coreValues", value })}
generateDraft={coreValuePresetFor(target.overrideKey)}
/>
))}
{draft?.groupKey === "communication" &&
target &&
(modalUsesWizardFieldBlocksBody ? (
<CustomMethodCardModalBody
cardId={target.overrideKey}
blocksById={state.customMethodCardFieldBlocksById}
blocksOverride={
draftFieldBlocks !== null ? draftFieldBlocks : undefined
}
policyMeta={
state.customMethodCardMetaById?.[target.overrideKey]
}
showPolicyContentLockupWhenNoBlocks={false}
onFieldBlocksChange={
draftFieldBlocks === null
? undefined
: handleWizardFieldBlocksChange
}
/>
) : (
<CommunicationMethodEditFields
value={draft.value}
onChange={(value) =>
setDraft({ groupKey: "communication", value })
}
generateDraft={communicationPresetFor(target.overrideKey)}
/>
))}
{draft?.groupKey === "membership" &&
target &&
(modalUsesWizardFieldBlocksBody ? (
<CustomMethodCardModalBody
cardId={target.overrideKey}
blocksById={state.customMethodCardFieldBlocksById}
blocksOverride={
draftFieldBlocks !== null ? draftFieldBlocks : undefined
}
policyMeta={
state.customMethodCardMetaById?.[target.overrideKey]
}
showPolicyContentLockupWhenNoBlocks={false}
onFieldBlocksChange={
draftFieldBlocks === null
? undefined
: handleWizardFieldBlocksChange
}
/>
) : (
<MembershipMethodEditFields
value={draft.value}
onChange={(value) =>
setDraft({ groupKey: "membership", value })
}
generateDraft={membershipPresetFor(target.overrideKey)}
/>
))}
{draft?.groupKey === "decisionApproaches" &&
target &&
(modalUsesWizardFieldBlocksBody ? (
<CustomMethodCardModalBody
cardId={target.overrideKey}
blocksById={state.customMethodCardFieldBlocksById}
blocksOverride={
draftFieldBlocks !== null ? draftFieldBlocks : undefined
}
policyMeta={
state.customMethodCardMetaById?.[target.overrideKey]
}
showPolicyContentLockupWhenNoBlocks={false}
onFieldBlocksChange={
draftFieldBlocks === null
? undefined
: handleWizardFieldBlocksChange
}
/>
) : (
<DecisionApproachEditFields
value={draft.value}
onChange={(value) =>
setDraft({ groupKey: "decisionApproaches", value })
}
generateDraft={decisionApproachPresetFor(target.overrideKey)}
/>
))}
{draft?.groupKey === "conflictManagement" &&
target &&
(modalUsesWizardFieldBlocksBody ? (
<CustomMethodCardModalBody
cardId={target.overrideKey}
blocksById={state.customMethodCardFieldBlocksById}
blocksOverride={
draftFieldBlocks !== null ? draftFieldBlocks : undefined
}
policyMeta={
state.customMethodCardMetaById?.[target.overrideKey]
}
showPolicyContentLockupWhenNoBlocks={false}
onFieldBlocksChange={
draftFieldBlocks === null
? undefined
: handleWizardFieldBlocksChange
}
/>
) : (
<ConflictManagementEditFields
value={draft.value}
onChange={(value) =>
setDraft({ groupKey: "conflictManagement", value })
}
generateDraft={conflictManagementPresetFor(target.overrideKey)}
/>
))}
</div>
</Create>
<CustomMethodCardWizard
isOpen={addCustomWizardOpen}
onClose={handleCloseAddWizard}
initialValues={wizardInitialValues}
onFinalize={handleFinalizeCustomCard}
onPersistCustomUploadFile={(file) =>
uploadCreateFlowFile(file, "customMethodAttachment")
}
/>
{confirmDialog}
</>
);
}
// ---------- helpers ------------------------------------------------------
function facetSeedSignature(
target: FinalReviewChipEditTarget,
state: CreateFlowState,
): string {
const id = target.overrideKey;
switch (target.groupKey) {
case "coreValues":
return JSON.stringify({
details: state.coreValueDetailsByChipId?.[id],
row:
state.coreValuesChipsSnapshot?.find((r) => r.id === id) ?? null,
blocks: state.customMethodCardFieldBlocksById?.[id] ?? null,
});
case "communication":
return JSON.stringify({
meta: state.customMethodCardMetaById?.[id] ?? null,
details: state.communicationMethodDetailsById?.[id] ?? null,
blocks: state.customMethodCardFieldBlocksById?.[id] ?? null,
});
case "membership":
return JSON.stringify({
meta: state.customMethodCardMetaById?.[id] ?? null,
details: state.membershipMethodDetailsById?.[id] ?? null,
blocks: state.customMethodCardFieldBlocksById?.[id] ?? null,
});
case "decisionApproaches":
return JSON.stringify({
meta: state.customMethodCardMetaById?.[id] ?? null,
details: state.decisionApproachDetailsById?.[id] ?? null,
blocks: state.customMethodCardFieldBlocksById?.[id] ?? null,
});
case "conflictManagement":
return JSON.stringify({
meta: state.customMethodCardMetaById?.[id] ?? null,
details: state.conflictManagementDetailsById?.[id] ?? null,
blocks: state.customMethodCardFieldBlocksById?.[id] ?? null,
});
default: {
const _e: never = target.groupKey;
return String(_e);
}
}
}
function addPrimaryLabelForMethodFacet(
groupKey: MethodFacetGroupKey,
cr: ReturnType<typeof useMessages>["create"]["customRule"],
): string {
switch (groupKey) {
case "communication":
return cr.communication.addPlatform.nextButtonText;
case "membership":
return cr.membership.addPlatform.nextButtonText;
case "decisionApproaches":
return cr.decisionApproaches.addApproach.nextButtonText;
case "conflictManagement":
return cr.conflictManagement.addApproach.nextButtonText;
}
}
function seedDraftForTarget(
target: FinalReviewChipEditTarget,
state: CreateFlowState,
): Draft {
switch (target.groupKey) {
case "coreValues": {
const saved = state.coreValueDetailsByChipId?.[target.overrideKey];
if (saved) {
return {
groupKey: "coreValues",
value: {
meaning: saved.meaning,
signals: saved.signals,
...(saved.supportText ? { supportText: saved.supportText } : {}),
},
};
}
return {
groupKey: "coreValues",
value: { meaning: "", signals: "" },
};
}
case "communication": {
const saved = state.communicationMethodDetailsById?.[target.overrideKey];
return {
groupKey: "communication",
value: { ...(saved ?? emptyCommunicationDetails()) },
};
}
case "membership": {
const saved = state.membershipMethodDetailsById?.[target.overrideKey];
return {
groupKey: "membership",
value: { ...(saved ?? emptyMembershipDetails()) },
};
}
case "decisionApproaches": {
const saved = state.decisionApproachDetailsById?.[target.overrideKey];
const value = saved
? {
...saved,
applicableScope: [...saved.applicableScope],
selectedApplicableScope: [...saved.selectedApplicableScope],
}
: emptyDecisionApproachTextFields(
decisionApproachPresetFor(target.overrideKey),
);
return {
groupKey: "decisionApproaches",
value,
};
}
case "conflictManagement": {
const saved = state.conflictManagementDetailsById?.[target.overrideKey];
const value = saved
? {
...saved,
applicableScope: [...saved.applicableScope],
selectedApplicableScope: [...saved.selectedApplicableScope],
}
: emptyConflictManagementDetails();
return {
groupKey: "conflictManagement",
value,
};
}
}
}
type SubtitleMessages = {
tCv: ReturnType<typeof useMessages>["create"]["customRule"]["coreValues"];
tComm: ReturnType<typeof useMessages>["create"]["customRule"]["communication"];
tMem: ReturnType<typeof useMessages>["create"]["customRule"]["membership"];
tDa: ReturnType<
typeof useMessages
>["create"]["customRule"]["decisionApproaches"];
tCm: ReturnType<
typeof useMessages
>["create"]["customRule"]["conflictManagement"];
};
function subtitleForTarget(
target: FinalReviewChipEditTarget,
msgs: SubtitleMessages,
customMeta?: CreateFlowState["customMethodCardMetaById"],
coreValueSupportText?: string,
): string {
switch (target.groupKey) {
case "coreValues": {
const fromSaved = coreValueSupportText?.trim();
if (fromSaved) return fromSaved;
return msgs.tCv.detailModal.subtitle;
}
case "communication": {
const fromCustom = customMeta?.[target.overrideKey]?.supportText?.trim();
if (fromCustom) return fromCustom;
return findMethodSupportText(msgs.tComm.methods, target.overrideKey);
}
case "membership": {
const fromCustom = customMeta?.[target.overrideKey]?.supportText?.trim();
if (fromCustom) return fromCustom;
return findMethodSupportText(msgs.tMem.methods, target.overrideKey);
}
case "decisionApproaches": {
const fromCustom = customMeta?.[target.overrideKey]?.supportText?.trim();
if (fromCustom) return fromCustom;
return findMethodSupportText(msgs.tDa.methods, target.overrideKey);
}
case "conflictManagement": {
const fromCustom = customMeta?.[target.overrideKey]?.supportText?.trim();
if (fromCustom) return fromCustom;
return findMethodSupportText(msgs.tCm.methods, target.overrideKey);
}
}
}
function findMethodSupportText(
methods: readonly { id: string; supportText: string }[],
id: string,
): string {
for (const method of methods) {
if (method.id === id) return method.supportText;
}
return "";
}