Files
community-rule/app/(app)/create/components/FinalReviewChipEditModal.tsx
T

1421 lines
44 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}). 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,
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]);
return (
<>
<Create
isOpen={isOpen && !addCustomWizardOpen}
onClose={handleModalClose}
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 })}
/>
))}
{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 })
}
/>
))}
{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 })
}
/>
))}
{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 })
}
/>
))}
{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 })
}
/>
))}
</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];
const preset = coreValuePresetFor(target.overrideKey);
return {
groupKey: "coreValues",
value: {
meaning: saved?.meaning ?? preset.meaning,
signals: saved?.signals ?? preset.signals,
...(saved?.supportText ? { supportText: saved.supportText } : {}),
},
};
}
case "communication": {
const saved =
state.communicationMethodDetailsById?.[target.overrideKey] ??
communicationPresetFor(target.overrideKey);
return { groupKey: "communication", value: { ...saved } };
}
case "membership": {
const saved =
state.membershipMethodDetailsById?.[target.overrideKey] ??
membershipPresetFor(target.overrideKey);
return { groupKey: "membership", value: { ...saved } };
}
case "decisionApproaches": {
const saved =
state.decisionApproachDetailsById?.[target.overrideKey] ??
decisionApproachPresetFor(target.overrideKey);
return {
groupKey: "decisionApproaches",
value: {
...saved,
applicableScope: [...saved.applicableScope],
selectedApplicableScope: [...saved.selectedApplicableScope],
},
};
}
case "conflictManagement": {
const saved =
state.conflictManagementDetailsById?.[target.overrideKey] ??
conflictManagementPresetFor(target.overrideKey);
return {
groupKey: "conflictManagement",
value: {
...saved,
applicableScope: [...saved.applicableScope],
selectedApplicableScope: [...saved.selectedApplicableScope],
},
};
}
}
}
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 "";
}