810 lines
26 KiB
TypeScript
810 lines
26 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
|
|
import MultiSelect from "../../../../components/controls/MultiSelect";
|
|
import type { ChipOption } from "../../../../components/controls/MultiSelect/MultiSelect.types";
|
|
import Create from "../../../../components/modals/Create";
|
|
import ContentLockup from "../../../../components/type/ContentLockup";
|
|
import { useMessages } from "../../../../contexts/MessagesContext";
|
|
import { buildCoreValueChipOptionsFromDraft } from "../../../../../lib/create/coreValueChipOptionsFromDraft";
|
|
import { useCreateFlow } from "../../context/CreateFlowContext";
|
|
import { useAsyncConfirm } from "../../../../hooks/useAsyncConfirm";
|
|
import { useBeforeUnloadGuard } from "../../../../hooks/useBeforeUnloadGuard";
|
|
import type {
|
|
CommunityStructureChipSnapshotRow,
|
|
CoreValueDetailEntry,
|
|
} from "../../types";
|
|
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
|
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
|
|
import { CoreValueEditFields } from "../../components/methodEditFields";
|
|
import CustomMethodCardModalBody from "../../components/CustomMethodCardModalBody";
|
|
import CustomMethodCardWizard from "../../components/CustomMethodCardWizard";
|
|
import { usesWizardFieldBlocksModalBody } from "../../../../../lib/create/usesWizardFieldBlocksModalBody";
|
|
import { buildCustomRuleModalKebabMenu } from "../../components/customRuleModalKebabMenu";
|
|
import {
|
|
duplicateCoreValueChipInDraft,
|
|
MAX_SELECTED_CORE_VALUES,
|
|
removeCoreValueChipFromDraft,
|
|
} from "../../../../../lib/create/coreValueChipFacet";
|
|
import { methodCardMetaWithCustomizeHeader } from "../../../../../lib/create/methodCardCustomizeMetaPatch";
|
|
import { moveFacetSelectionIdToFront } from "../../../../../lib/create/methodCardSelectionOrder";
|
|
import {
|
|
buildMethodCardWizardInitialValues,
|
|
coreValueDetailsFromWizardFieldBlocks,
|
|
overlayFacetPrefillValues,
|
|
type MethodCardWizardInitialValues,
|
|
} from "../../../../../lib/create/methodCardWizardPrefill";
|
|
import type { CustomMethodCardFieldBlock } from "../../../../../lib/create/customMethodCardFieldBlocks";
|
|
import { uploadCreateFlowFile } from "../../../../../lib/create/uploadToServer";
|
|
|
|
const MAX_CORE_VALUES = MAX_SELECTED_CORE_VALUES;
|
|
|
|
/**
|
|
* Why three sessions, not two:
|
|
*
|
|
* - `pending` — preset chip just selected; modal opened to capture
|
|
* meaning/signals. Close (X) confirms, then unselects the chip.
|
|
* - `customPending` — leftover inline custom-chip drafts (older sessions).
|
|
* Dismiss = drop the chip entirely.
|
|
* - `editing` — chip already exists & is selected; modal reopened to
|
|
* tweak meaning/signals. Dismiss = no-op (chip stays as-is).
|
|
*
|
|
* **Add value** (and the header "add" link) opens an empty custom-policy
|
|
* wizard. Finalize writes a selected chip named from the wizard title.
|
|
*/
|
|
type ModalSession = "pending" | "customPending" | "editing";
|
|
|
|
/** Row in `coreValues.json` `values` — string (legacy) or `{ label, meaning, signals }`. */
|
|
type CoreValuePresetJson =
|
|
| string
|
|
| { label: string; meaning?: string; signals?: string };
|
|
|
|
type CoreValuePreset = {
|
|
label: string;
|
|
meaning: string;
|
|
signals: string;
|
|
};
|
|
|
|
function normalizeCoreValuePresets(
|
|
values: readonly CoreValuePresetJson[],
|
|
): CoreValuePreset[] {
|
|
return values.map((v) => {
|
|
if (typeof v === "string") {
|
|
return { label: v, meaning: "", signals: "" };
|
|
}
|
|
return {
|
|
label: v.label,
|
|
meaning: typeof v.meaning === "string" ? v.meaning : "",
|
|
signals: typeof v.signals === "string" ? v.signals : "",
|
|
};
|
|
});
|
|
}
|
|
|
|
function selectedIdsFromOptions(options: ChipOption[]): string[] {
|
|
return options
|
|
.filter((o) => o.state === "selected")
|
|
.map((o) => o.id);
|
|
}
|
|
|
|
function chipOptionsToSnapshotRows(
|
|
options: ChipOption[],
|
|
): CommunityStructureChipSnapshotRow[] {
|
|
return options.map((o) => ({
|
|
id: o.id,
|
|
label: o.label,
|
|
...(o.state !== undefined ? { state: o.state } : {}),
|
|
}));
|
|
}
|
|
|
|
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();
|
|
const cv = m.create.customRule.coreValues;
|
|
const modalKebabMenu = m.create.customRule.modalKebabMenu;
|
|
const presets = useMemo(
|
|
() => normalizeCoreValuePresets(cv.values as CoreValuePresetJson[]),
|
|
[cv.values],
|
|
);
|
|
|
|
const { requestConfirm, confirmDialog } = useAsyncConfirm();
|
|
const { markCreateFlowInteraction, updateState, replaceState, state } =
|
|
useCreateFlow();
|
|
|
|
const initialDraftRef = useRef<CoreValueDetailEntry | null>(null);
|
|
const pendingEphemeralCoreDuplicateRef = useRef<string | null>(null);
|
|
|
|
const [coreValueOptions, setCoreValueOptions] = useState<ChipOption[]>(() =>
|
|
buildCoreValueChipOptionsFromDraft(
|
|
presets,
|
|
state.coreValuesChipsSnapshot,
|
|
state.selectedCoreValueIds,
|
|
),
|
|
);
|
|
|
|
const [activeModalChipId, setActiveModalChipId] = useState<string | null>(
|
|
null,
|
|
);
|
|
const [modalSession, setModalSession] = useState<ModalSession | null>(null);
|
|
const [draft, setDraft] = useState<CoreValueDetailEntry>(EMPTY_DETAIL);
|
|
const [addCustomWizardOpen, setAddCustomWizardOpen] = useState(false);
|
|
const [wizardCustomizeChipId, setWizardCustomizeChipId] = useState<
|
|
string | null
|
|
>(null);
|
|
const [wizardInitialValues, setWizardInitialValues] =
|
|
useState<MethodCardWizardInitialValues | null>(null);
|
|
const [draftFieldBlocks, setDraftFieldBlocks] = useState<
|
|
CustomMethodCardFieldBlock[] | null
|
|
>(null);
|
|
|
|
useEffect(() => {
|
|
setCoreValueOptions(
|
|
buildCoreValueChipOptionsFromDraft(
|
|
presets,
|
|
state.coreValuesChipsSnapshot,
|
|
state.selectedCoreValueIds,
|
|
),
|
|
);
|
|
}, [
|
|
presets,
|
|
state.coreValuesChipsSnapshot,
|
|
state.selectedCoreValueIds,
|
|
]);
|
|
|
|
/** Sync chips to create-flow draft. Never call `updateState` from inside a `setCoreValueOptions` updater — defer with `queueMicrotask`. */
|
|
const syncCoreValuesToDraft = useCallback(
|
|
(next: ChipOption[]) => {
|
|
updateState({
|
|
selectedCoreValueIds: selectedIdsFromOptions(next),
|
|
coreValuesChipsSnapshot: chipOptionsToSnapshotRows(next),
|
|
});
|
|
},
|
|
[updateState],
|
|
);
|
|
|
|
const persistCoreValues = useCallback(
|
|
(next: ChipOption[]) => {
|
|
markCreateFlowInteraction();
|
|
setCoreValueOptions(next);
|
|
syncCoreValuesToDraft(next);
|
|
},
|
|
[markCreateFlowInteraction, syncCoreValuesToDraft],
|
|
);
|
|
|
|
/** Default meaning/signals from `coreValues.json` `values` for each preset label. */
|
|
const getPresetTexts = useCallback(
|
|
(valueLabel: string): CoreValueDetailEntry => {
|
|
const row = presets.find((p) => p.label === valueLabel);
|
|
if (!row) return EMPTY_DETAIL;
|
|
return { meaning: row.meaning, signals: row.signals };
|
|
},
|
|
[presets],
|
|
);
|
|
|
|
const getInitialTexts = useCallback(
|
|
(chipId: string, valueLabel: string): CoreValueDetailEntry => {
|
|
const saved = state.coreValueDetailsByChipId?.[chipId];
|
|
const preset = getPresetTexts(valueLabel);
|
|
return {
|
|
meaning: saved?.meaning ?? preset.meaning,
|
|
signals: saved?.signals ?? preset.signals,
|
|
...(saved?.supportText ? { supportText: saved.supportText } : {}),
|
|
};
|
|
},
|
|
[state.coreValueDetailsByChipId, getPresetTexts],
|
|
);
|
|
|
|
const openModal = useCallback(
|
|
(
|
|
chipId: string,
|
|
session: ModalSession,
|
|
valueLabel: string,
|
|
seedDetail?: CoreValueDetailEntry,
|
|
) => {
|
|
const initial = seedDetail ?? getInitialTexts(chipId, valueLabel);
|
|
initialDraftRef.current = { ...initial };
|
|
const persisted = state.customMethodCardFieldBlocksById?.[chipId];
|
|
setDraftFieldBlocks(
|
|
Array.isArray(persisted) && persisted.length > 0
|
|
? structuredClone(persisted)
|
|
: null,
|
|
);
|
|
setDraft(initial);
|
|
setActiveModalChipId(chipId);
|
|
setModalSession(session);
|
|
markCreateFlowInteraction();
|
|
},
|
|
[
|
|
getInitialTexts,
|
|
markCreateFlowInteraction,
|
|
state.customMethodCardFieldBlocksById,
|
|
],
|
|
);
|
|
|
|
const handleDraftChange = useCallback(
|
|
(next: CoreValueDetailEntry) => {
|
|
markCreateFlowInteraction();
|
|
setDraft(next);
|
|
},
|
|
[markCreateFlowInteraction],
|
|
);
|
|
|
|
const finalizeModalDismiss = useCallback(() => {
|
|
pendingEphemeralCoreDuplicateRef.current = null;
|
|
initialDraftRef.current = null;
|
|
setActiveModalChipId(null);
|
|
setModalSession(null);
|
|
setDraftFieldBlocks(null);
|
|
}, []);
|
|
|
|
const confirmLeaveWithoutSaving = useCallback(async () => {
|
|
const initial = initialDraftRef.current;
|
|
const fieldsDirty =
|
|
initial != null &&
|
|
(draft.meaning !== initial.meaning || draft.signals !== initial.signals);
|
|
if (!fieldsDirty) {
|
|
return true;
|
|
}
|
|
const isPendingAdd =
|
|
modalSession === "pending" || modalSession === "customPending";
|
|
return requestConfirm({
|
|
title: cv.detailModal.discardTitle,
|
|
description: isPendingAdd
|
|
? cv.detailModal.discardPendingDescription
|
|
: cv.detailModal.discardEditsDescription,
|
|
proceedText: cv.detailModal.discardProceed,
|
|
cancelText: cv.detailModal.discardKeepEditing,
|
|
});
|
|
}, [cv.detailModal, draft, modalSession, requestConfirm]);
|
|
|
|
useBeforeUnloadGuard(
|
|
activeModalChipId != null &&
|
|
!addCustomWizardOpen &&
|
|
initialDraftRef.current != null &&
|
|
(draft.meaning !== initialDraftRef.current.meaning ||
|
|
draft.signals !== initialDraftRef.current.signals),
|
|
);
|
|
|
|
const handleDuplicateCoreChip = useCallback(() => {
|
|
if (!activeModalChipId || !modalSession) return;
|
|
markCreateFlowInteraction();
|
|
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,
|
|
activeModalChipId,
|
|
modalKebabMenu.duplicateTitleSuffix,
|
|
);
|
|
if (!res) {
|
|
return base;
|
|
}
|
|
outcome = res;
|
|
return { ...base, ...res.patch };
|
|
});
|
|
if (!outcome) {
|
|
return;
|
|
}
|
|
pendingEphemeralCoreDuplicateRef.current = outcome.newId;
|
|
openModal(
|
|
outcome.newId,
|
|
"editing",
|
|
outcome.newLabel,
|
|
structuredClone(draft),
|
|
);
|
|
}, [
|
|
activeModalChipId,
|
|
draft,
|
|
markCreateFlowInteraction,
|
|
modalKebabMenu.duplicateTitleSuffix,
|
|
modalSession,
|
|
openModal,
|
|
replaceState,
|
|
]);
|
|
|
|
const handleRemoveFromKebab = useCallback(() => {
|
|
markCreateFlowInteraction();
|
|
|
|
const ep = pendingEphemeralCoreDuplicateRef.current;
|
|
if (ep && activeModalChipId === ep) {
|
|
replaceState((prev) => ({
|
|
...prev,
|
|
...removeCoreValueChipFromDraft(prev, ep),
|
|
}));
|
|
finalizeModalDismiss();
|
|
return;
|
|
}
|
|
|
|
if (modalSession === "pending") {
|
|
const next = coreValueOptions.map((opt) =>
|
|
opt.id === activeModalChipId
|
|
? { ...opt, state: "unselected" as const }
|
|
: opt,
|
|
);
|
|
persistCoreValues(next);
|
|
} else if (modalSession === "customPending") {
|
|
const next = coreValueOptions.filter((opt) => opt.id !== activeModalChipId);
|
|
persistCoreValues(next);
|
|
} else if (modalSession === "editing" && activeModalChipId) {
|
|
const nextFiltered = coreValueOptions.filter(
|
|
(opt) => opt.id !== activeModalChipId,
|
|
);
|
|
markCreateFlowInteraction();
|
|
replaceState((prev) => ({
|
|
...prev,
|
|
...removeCoreValueChipFromDraft(prev, activeModalChipId),
|
|
}));
|
|
setCoreValueOptions(nextFiltered);
|
|
}
|
|
finalizeModalDismiss();
|
|
}, [
|
|
activeModalChipId,
|
|
coreValueOptions,
|
|
finalizeModalDismiss,
|
|
markCreateFlowInteraction,
|
|
modalSession,
|
|
persistCoreValues,
|
|
replaceState,
|
|
]);
|
|
|
|
const handleModalDismiss = useCallback(async () => {
|
|
if (!(await confirmLeaveWithoutSaving())) {
|
|
return;
|
|
}
|
|
|
|
const ep = pendingEphemeralCoreDuplicateRef.current;
|
|
if (ep) {
|
|
replaceState((prev) => ({
|
|
...prev,
|
|
...removeCoreValueChipFromDraft(prev, ep),
|
|
}));
|
|
}
|
|
|
|
if (modalSession === "pending" && activeModalChipId) {
|
|
const next = coreValueOptions.map((opt) =>
|
|
opt.id === activeModalChipId
|
|
? { ...opt, state: "unselected" as const }
|
|
: opt,
|
|
);
|
|
persistCoreValues(next);
|
|
} else if (modalSession === "customPending" && activeModalChipId) {
|
|
const next = coreValueOptions.filter(
|
|
(opt) => opt.id !== activeModalChipId,
|
|
);
|
|
persistCoreValues(next);
|
|
}
|
|
|
|
finalizeModalDismiss();
|
|
}, [
|
|
activeModalChipId,
|
|
confirmLeaveWithoutSaving,
|
|
coreValueOptions,
|
|
finalizeModalDismiss,
|
|
modalSession,
|
|
persistCoreValues,
|
|
replaceState,
|
|
]);
|
|
|
|
const handleModalConfirm = useCallback(() => {
|
|
if (!activeModalChipId || !modalSession) return;
|
|
markCreateFlowInteraction();
|
|
pendingEphemeralCoreDuplicateRef.current = null;
|
|
const existingBlocks =
|
|
draftFieldBlocks && draftFieldBlocks.length > 0
|
|
? draftFieldBlocks
|
|
: state.customMethodCardFieldBlocksById?.[activeModalChipId];
|
|
updateState({
|
|
coreValueDetailsByChipId: {
|
|
...(state.coreValueDetailsByChipId ?? {}),
|
|
[activeModalChipId]: draft,
|
|
},
|
|
...(existingBlocks && existingBlocks.length > 0
|
|
? {
|
|
customMethodCardFieldBlocksById: {
|
|
...(state.customMethodCardFieldBlocksById ?? {}),
|
|
[activeModalChipId]: overlayFacetPrefillValues(existingBlocks, {
|
|
group: "coreValues",
|
|
draft,
|
|
headings: {
|
|
meaning: cv.detailModal.meaningLabel,
|
|
signals: cv.detailModal.signalsLabel,
|
|
},
|
|
}),
|
|
},
|
|
}
|
|
: {}),
|
|
});
|
|
finalizeModalDismiss();
|
|
}, [
|
|
activeModalChipId,
|
|
cv.detailModal.meaningLabel,
|
|
cv.detailModal.signalsLabel,
|
|
draft,
|
|
draftFieldBlocks,
|
|
finalizeModalDismiss,
|
|
markCreateFlowInteraction,
|
|
modalSession,
|
|
state.coreValueDetailsByChipId,
|
|
state.customMethodCardFieldBlocksById,
|
|
updateState,
|
|
]);
|
|
|
|
const modalChipLabel =
|
|
coreValueOptions.find((o) => o.id === activeModalChipId)?.label ?? "";
|
|
|
|
const modalUsesWizardFieldBlocksBody = Boolean(
|
|
activeModalChipId &&
|
|
usesWizardFieldBlocksModalBody({
|
|
methodId: activeModalChipId,
|
|
meta: {},
|
|
fieldBlocksById: state.customMethodCardFieldBlocksById,
|
|
modalEditUnlocked: true,
|
|
draftFieldBlocks,
|
|
}),
|
|
);
|
|
|
|
const showFooterPrimary = Boolean(modalSession);
|
|
|
|
const handleCustomize = useCallback(() => {
|
|
if (!activeModalChipId) return;
|
|
markCreateFlowInteraction();
|
|
setWizardInitialValues(
|
|
buildMethodCardWizardInitialValues({
|
|
cardId: activeModalChipId,
|
|
fallbackTitle: modalChipLabel,
|
|
fallbackDescription:
|
|
draft.supportText?.trim() || cv.detailModal.subtitle,
|
|
meta: {},
|
|
persistedBlocks: state.customMethodCardFieldBlocksById,
|
|
draftFieldBlocks,
|
|
facetPrefill: {
|
|
group: "coreValues",
|
|
draft,
|
|
headings: {
|
|
meaning: cv.detailModal.meaningLabel,
|
|
signals: cv.detailModal.signalsLabel,
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
setWizardCustomizeChipId(activeModalChipId);
|
|
setAddCustomWizardOpen(true);
|
|
}, [
|
|
activeModalChipId,
|
|
cv.detailModal.meaningLabel,
|
|
cv.detailModal.signalsLabel,
|
|
cv.detailModal.subtitle,
|
|
draft,
|
|
draftFieldBlocks,
|
|
markCreateFlowInteraction,
|
|
modalChipLabel,
|
|
state.customMethodCardFieldBlocksById,
|
|
]);
|
|
|
|
const handleCloseAddWizard = useCallback(() => {
|
|
setAddCustomWizardOpen(false);
|
|
setWizardCustomizeChipId(null);
|
|
setWizardInitialValues(null);
|
|
}, []);
|
|
|
|
const handleFinalizeCustomCard = useCallback(
|
|
({
|
|
title,
|
|
description,
|
|
fieldBlocks,
|
|
}: {
|
|
title: string;
|
|
description: string;
|
|
fieldBlocks: CustomMethodCardFieldBlock[];
|
|
}) => {
|
|
const trimmedTitle = title.trim();
|
|
const trimmedDescription = description.trim();
|
|
if (!trimmedTitle) return;
|
|
markCreateFlowInteraction();
|
|
pendingEphemeralCoreDuplicateRef.current = null;
|
|
|
|
const existingId = wizardCustomizeChipId;
|
|
if (!existingId) {
|
|
const id = crypto.randomUUID();
|
|
const nextDetails = {
|
|
...coreValueDetailsFromWizardFieldBlocks(fieldBlocks, EMPTY_DETAIL),
|
|
...(trimmedDescription.length > 0
|
|
? { supportText: trimmedDescription }
|
|
: {}),
|
|
};
|
|
replaceState((prev) => {
|
|
const sel = prev.selectedCoreValueIds ?? [];
|
|
if (sel.length >= MAX_CORE_VALUES) {
|
|
return prev;
|
|
}
|
|
const snap = [...(prev.coreValuesChipsSnapshot ?? [])];
|
|
snap.push({
|
|
id,
|
|
label: trimmedTitle,
|
|
state: "selected",
|
|
});
|
|
return {
|
|
...prev,
|
|
selectedCoreValueIds: moveFacetSelectionIdToFront(sel, id),
|
|
coreValuesChipsSnapshot: snap,
|
|
coreValueDetailsByChipId: {
|
|
...(prev.coreValueDetailsByChipId ?? {}),
|
|
[id]: nextDetails,
|
|
},
|
|
customMethodCardFieldBlocksById: {
|
|
...(prev.customMethodCardFieldBlocksById ?? {}),
|
|
[id]: structuredClone(fieldBlocks),
|
|
},
|
|
customMethodCardMetaById: methodCardMetaWithCustomizeHeader(
|
|
prev.customMethodCardMetaById,
|
|
id,
|
|
{ title: trimmedTitle, description: trimmedDescription },
|
|
),
|
|
};
|
|
});
|
|
setAddCustomWizardOpen(false);
|
|
setWizardCustomizeChipId(null);
|
|
setWizardInitialValues(null);
|
|
return;
|
|
}
|
|
|
|
const nextDetails = {
|
|
...coreValueDetailsFromWizardFieldBlocks(fieldBlocks, draft),
|
|
...(trimmedDescription.length > 0
|
|
? { supportText: trimmedDescription }
|
|
: {}),
|
|
};
|
|
replaceState((prev) => {
|
|
const snap = [...(prev.coreValuesChipsSnapshot ?? [])];
|
|
const i = snap.findIndex((r) => r.id === existingId);
|
|
if (i >= 0) {
|
|
snap[i] = { ...snap[i], label: trimmedTitle };
|
|
}
|
|
return {
|
|
...prev,
|
|
coreValuesChipsSnapshot: snap,
|
|
coreValueDetailsByChipId: {
|
|
...(prev.coreValueDetailsByChipId ?? {}),
|
|
[existingId]: nextDetails,
|
|
},
|
|
customMethodCardFieldBlocksById: {
|
|
...(prev.customMethodCardFieldBlocksById ?? {}),
|
|
[existingId]: structuredClone(fieldBlocks),
|
|
},
|
|
};
|
|
});
|
|
setDraft(nextDetails);
|
|
setDraftFieldBlocks(structuredClone(fieldBlocks));
|
|
setAddCustomWizardOpen(false);
|
|
setWizardCustomizeChipId(null);
|
|
setWizardInitialValues(null);
|
|
},
|
|
[draft, markCreateFlowInteraction, replaceState, wizardCustomizeChipId],
|
|
);
|
|
|
|
const kebabMenuItems = useMemo(() => {
|
|
if (!modalSession || !activeModalChipId) return [];
|
|
const selectedCount = coreValueOptions.filter(
|
|
(o) => o.state === "selected",
|
|
).length;
|
|
return buildCustomRuleModalKebabMenu(modalKebabMenu, {
|
|
showCustomize: true,
|
|
onCustomize: handleCustomize,
|
|
onDuplicate:
|
|
(state.editingPublishedRuleId?.trim() ?? "") !== "" ||
|
|
selectedCount >= MAX_CORE_VALUES
|
|
? undefined
|
|
: handleDuplicateCoreChip,
|
|
showRemove: modalSession === "editing",
|
|
onRemove: handleRemoveFromKebab,
|
|
});
|
|
}, [
|
|
activeModalChipId,
|
|
coreValueOptions,
|
|
handleCustomize,
|
|
handleDuplicateCoreChip,
|
|
handleRemoveFromKebab,
|
|
modalKebabMenu,
|
|
modalSession,
|
|
state.editingPublishedRuleId,
|
|
]);
|
|
const handleChipClick = (chipId: string) => {
|
|
const target = coreValueOptions.find((o) => o.id === chipId);
|
|
if (!target || target.state === "custom") return;
|
|
|
|
const selectedCount = coreValueOptions.filter(
|
|
(o) => o.state === "selected",
|
|
).length;
|
|
|
|
if (target.state === "selected") {
|
|
openModal(chipId, "editing", target.label);
|
|
return;
|
|
}
|
|
|
|
if (selectedCount >= MAX_CORE_VALUES) return;
|
|
|
|
const next: ChipOption[] = coreValueOptions.map((opt) =>
|
|
opt.id === chipId
|
|
? { ...opt, state: "selected" as const }
|
|
: opt,
|
|
);
|
|
persistCoreValues(next);
|
|
openModal(chipId, "pending", target.label);
|
|
};
|
|
|
|
const addHandlers = {
|
|
onAddClick: () => {
|
|
const selectedCount = coreValueOptions.filter(
|
|
(o) => o.state === "selected",
|
|
).length;
|
|
if (selectedCount >= MAX_CORE_VALUES) return;
|
|
markCreateFlowInteraction();
|
|
setWizardCustomizeChipId(null);
|
|
setWizardInitialValues(null);
|
|
setAddCustomWizardOpen(true);
|
|
},
|
|
onCustomChipConfirm: (chipId: string, value: string) => {
|
|
markCreateFlowInteraction();
|
|
setCoreValueOptions((prev) => {
|
|
const withLabel = prev.map((opt) =>
|
|
opt.id === chipId
|
|
? { ...opt, label: value, state: "unselected" as const }
|
|
: opt,
|
|
);
|
|
const selectedCount = withLabel.filter(
|
|
(o) => o.state === "selected",
|
|
).length;
|
|
const canSelect = selectedCount < MAX_CORE_VALUES;
|
|
const next = canSelect
|
|
? withLabel.map((opt) =>
|
|
opt.id === chipId
|
|
? { ...opt, state: "selected" as const }
|
|
: opt,
|
|
)
|
|
: withLabel;
|
|
|
|
queueMicrotask(() => {
|
|
syncCoreValuesToDraft(next);
|
|
// Both branches treat the chip as a brand-new draft until the
|
|
// user confirms via Add Value — dismissal removes it.
|
|
openModal(chipId, "customPending", value);
|
|
});
|
|
return next;
|
|
});
|
|
},
|
|
onCustomChipClose: (chipId: string) => {
|
|
markCreateFlowInteraction();
|
|
setCoreValueOptions((prev) => {
|
|
const next = prev.filter((o) => o.id !== chipId);
|
|
queueMicrotask(() => syncCoreValuesToDraft(next));
|
|
return next;
|
|
});
|
|
},
|
|
};
|
|
|
|
const description = (
|
|
<>
|
|
<span className="leading-[1.3] text-[color:var(--color-content-default-tertiary,#b4b4b4)]">
|
|
{cv.header.descriptionLead}{" "}
|
|
</span>
|
|
<button
|
|
type="button"
|
|
onClick={addHandlers.onAddClick}
|
|
className="cursor-pointer font-normal leading-[1.3] text-[color:var(--color-content-default-tertiary,#b4b4b4)] underline decoration-solid underline-offset-[3px] hover:opacity-90"
|
|
>
|
|
{cv.header.addLink}
|
|
</button>
|
|
<span className="leading-[1.3] text-[color:var(--color-content-default-tertiary,#b4b4b4)]">
|
|
{" "}
|
|
{cv.header.descriptionTrail}
|
|
</span>
|
|
</>
|
|
);
|
|
|
|
const detailModal = cv.detailModal;
|
|
|
|
return (
|
|
<>
|
|
<CreateFlowTwoColumnSelectShell
|
|
lgVerticalAlign="start"
|
|
header={
|
|
<CreateFlowHeaderLockup
|
|
title={cv.header.title}
|
|
description={description}
|
|
justification="left"
|
|
/>
|
|
}
|
|
>
|
|
<MultiSelect
|
|
formHeader={false}
|
|
size="m"
|
|
options={coreValueOptions}
|
|
onChipClick={handleChipClick}
|
|
onAddClick={addHandlers.onAddClick}
|
|
onCustomChipConfirm={addHandlers.onCustomChipConfirm}
|
|
onCustomChipClose={addHandlers.onCustomChipClose}
|
|
addButton
|
|
addButtonText={cv.multiSelect.addButtonText}
|
|
/>
|
|
|
|
{detailModal && (
|
|
<Create
|
|
isOpen={activeModalChipId !== null && !addCustomWizardOpen}
|
|
onClose={handleModalDismiss}
|
|
backdropVariant="blurredYellow"
|
|
headerContent={
|
|
<div className="bg-[var(--color-surface-default-primary)] px-[24px] py-[12px] shrink-0">
|
|
<ContentLockup
|
|
title={modalChipLabel}
|
|
description={
|
|
draft.supportText?.trim() || detailModal.subtitle
|
|
}
|
|
variant="modal"
|
|
alignment="left"
|
|
/>
|
|
</div>
|
|
}
|
|
showBackButton={false}
|
|
showNextButton={showFooterPrimary}
|
|
showRemoveButton={modalSession === "editing"}
|
|
onRemove={handleRemoveFromKebab}
|
|
onNext={handleModalConfirm}
|
|
nextButtonText={
|
|
modalSession === "editing"
|
|
? modalKebabMenu.saveEdits
|
|
: detailModal.addValueButton
|
|
}
|
|
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
|
|
kebabMenuAriaLabel={modalKebabMenu.menuAriaLabel}
|
|
kebabMenuItems={
|
|
kebabMenuItems.length > 0 ? kebabMenuItems : undefined
|
|
}
|
|
ariaLabel={modalChipLabel || "Core value details"}
|
|
>
|
|
{modalUsesWizardFieldBlocksBody && activeModalChipId ? (
|
|
<CustomMethodCardModalBody
|
|
cardId={activeModalChipId}
|
|
blocksById={state.customMethodCardFieldBlocksById}
|
|
blocksOverride={
|
|
draftFieldBlocks !== null ? draftFieldBlocks : undefined
|
|
}
|
|
showPolicyContentLockupWhenNoBlocks={false}
|
|
onFieldBlocksChange={
|
|
draftFieldBlocks === null
|
|
? undefined
|
|
: (next) => {
|
|
setDraftFieldBlocks(next);
|
|
setDraft(
|
|
coreValueDetailsFromWizardFieldBlocks(next, draft),
|
|
);
|
|
}
|
|
}
|
|
/>
|
|
) : (
|
|
<CoreValueEditFields
|
|
value={draft}
|
|
onChange={handleDraftChange}
|
|
/>
|
|
)}
|
|
</Create>
|
|
)}
|
|
</CreateFlowTwoColumnSelectShell>
|
|
<CustomMethodCardWizard
|
|
isOpen={addCustomWizardOpen}
|
|
onClose={handleCloseAddWizard}
|
|
initialValues={wizardInitialValues}
|
|
onFinalize={handleFinalizeCustomCard}
|
|
onPersistCustomUploadFile={(file) =>
|
|
uploadCreateFlowFile(file, "customMethodAttachment")
|
|
}
|
|
/>
|
|
{confirmDialog}
|
|
</>
|
|
);
|
|
}
|