"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 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 { 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` — brand-new custom chip just created via the Add * value flow; modal opened with empty fields. Dismiss = drop the * chip entirely (it was never confirmed via the Add Value button). * - `editing` — chip already exists & is selected; modal reopened to * tweak meaning/signals. Dismiss = no-op (chip stays as-is). */ 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(null); const pendingEphemeralCoreDuplicateRef = useRef(null); const [coreValueOptions, setCoreValueOptions] = useState(() => buildCoreValueChipOptionsFromDraft( presets, state.coreValuesChipsSnapshot, state.selectedCoreValueIds, ), ); const [activeModalChipId, setActiveModalChipId] = useState( null, ); const [modalSession, setModalSession] = useState(null); const [draft, setDraft] = useState(EMPTY_DETAIL); const [addCustomWizardOpen, setAddCustomWizardOpen] = useState(false); const [wizardCustomizeChipId, setWizardCustomizeChipId] = useState< string | null >(null); const [wizardInitialValues, setWizardInitialValues] = useState(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]); const handleDuplicateCoreChip = useCallback(() => { if (!activeModalChipId || !modalSession) return; markCreateFlowInteraction(); const priorEphemeral = pendingEphemeralCoreDuplicateRef.current; let outcome: ReturnType | 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 chipId = wizardCustomizeChipId ?? activeModalChipId; if (!chipId) return; markCreateFlowInteraction(); pendingEphemeralCoreDuplicateRef.current = null; const trimmedTitle = title.trim(); const trimmedDescription = description.trim(); const nextDetails = { ...coreValueDetailsFromWizardFieldBlocks(fieldBlocks, draft), ...(trimmedDescription.length > 0 ? { supportText: trimmedDescription } : {}), }; replaceState((prev) => { const snap = [...(prev.coreValuesChipsSnapshot ?? [])]; const i = snap.findIndex((r) => r.id === chipId); if (i >= 0 && trimmedTitle.length > 0) { snap[i] = { ...snap[i], label: trimmedTitle }; } return { ...prev, coreValuesChipsSnapshot: snap, coreValueDetailsByChipId: { ...(prev.coreValueDetailsByChipId ?? {}), [chipId]: nextDetails, }, customMethodCardFieldBlocksById: { ...(prev.customMethodCardFieldBlocksById ?? {}), [chipId]: structuredClone(fieldBlocks), }, }; }); setDraft(nextDetails); setDraftFieldBlocks(structuredClone(fieldBlocks)); setAddCustomWizardOpen(false); setWizardCustomizeChipId(null); setWizardInitialValues(null); }, [ activeModalChipId, 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: () => { markCreateFlowInteraction(); setCoreValueOptions((prev) => { const next: ChipOption[] = [ ...prev, { id: crypto.randomUUID(), label: "", state: "custom" }, ]; queueMicrotask(() => syncCoreValuesToDraft(next)); return next; }); }, 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 = ( <> {cv.header.descriptionLead}{" "} {" "} {cv.header.descriptionTrail} ); const detailModal = cv.detailModal; return ( <> } > {detailModal && ( } showBackButton={false} showNextButton={showFooterPrimary} 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 ? ( { setDraftFieldBlocks(next); setDraft( coreValueDetailsFromWizardFieldBlocks(next, draft), ); } } /> ) : ( )} )} uploadCreateFlowFile(file, "customMethodAttachment") } /> {confirmDialog} ); }