"use client"; import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useMessages, useTranslation, } from "../../../../contexts/MessagesContext"; import { useAsyncConfirm } from "../../../../hooks/useAsyncConfirm"; import type { CustomMethodCardFieldBlock } from "../../../../../lib/create/customMethodCardFieldBlocks"; import { CUSTOM_METHOD_CARD_WIZARD_MAX_DESCRIPTION_CHARS, CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS, } from "../../../../../lib/create/customMethodCardWizardConstants"; import type { AddCustomFieldType } from "../../../../components/controls/AddCustomField/AddCustomField.types"; import type { ModalHeaderMenuItem } from "../../../../components/modals/ModalHeader/ModalHeader.types"; import { CustomMethodCardWizardView } from "./CustomMethodCardWizard.view"; import type { CustomMethodCardWizardProps } from "./CustomMethodCardWizard.types"; /** * Shared 3-step add-custom-method-card flow (Figma Modal / Create — nodes * `20066:14748`, `20094:48551`, `20066:14361`). */ const CustomMethodCardWizardContainer = memo( ({ isOpen, onClose, onFinalize, onPersistCustomUploadFile, initialValues }) => { const m = useMessages(); const t = useTranslation("common"); const tUpload = useTranslation("create.upload"); const w = m.create.customRule.customMethodCardWizard; const menuCopy = m.create.customRule.modalKebabMenu; const { requestConfirm, confirmDialog } = useAsyncConfirm(); const copy = useMemo( () => ({ step1: w.steps["1"], step2: w.steps["2"], step3: w.steps["3"], step3BlocksList: w.step3BlocksList, fieldTypeLabels: { text: w.addCustomField.fieldTypes.text, badges: w.addCustomField.fieldTypes.badges, upload: w.addCustomField.fieldTypes.upload, proportion: w.addCustomField.fieldTypes.proportion, }, footerFinalize: w.footer.finalize, fieldModals: w.fieldModals, }), [ w.addCustomField.fieldTypes, w.fieldModals, w.footer.finalize, w.step3BlocksList, w.steps, ], ); const fieldBodiesCopy = useMemo( () => ({ requiredHint: copy.fieldModals.requiredHint, text: copy.fieldModals.text, badges: copy.fieldModals.badges, upload: copy.fieldModals.upload, proportion: copy.fieldModals.proportion, }), [copy.fieldModals], ); const [wizardStep, setWizardStep] = useState<1 | 2 | 3>(1); const [policyTitle, setPolicyTitle] = useState(""); const [policyDescription, setPolicyDescription] = useState(""); const [addFieldExpanded, setAddFieldExpanded] = useState(false); const [fieldTypeModal, setFieldTypeModal] = useState(null); const [draftFieldBlocks, setDraftFieldBlocks] = useState< CustomMethodCardFieldBlock[] >([]); const [editingBlockId, setEditingBlockId] = useState(null); const [textBlockTitle, setTextBlockTitle] = useState(""); const [textPlaceholderBody, setTextPlaceholderBody] = useState(""); const [badgeBlockTitle, setBadgeBlockTitle] = useState(""); const [badgeOptions, setBadgeOptions] = useState([]); const [uploadBlockTitle, setUploadBlockTitle] = useState(""); const [uploadFileName, setUploadFileName] = useState( undefined, ); const [uploadAssetUrl, setUploadAssetUrl] = useState( undefined, ); const [uploadFieldBusy, setUploadFieldBusy] = useState(false); const [uploadFieldError, setUploadFieldError] = useState( null, ); const [proportionBlockTitle, setProportionBlockTitle] = useState(""); const [proportionDefault, setProportionDefault] = useState(50); const fileInputRef = useRef(null); const initialValuesRef = useRef(initialValues); initialValuesRef.current = initialValues; const openSnapshotRef = useRef<{ title: string; description: string; fieldBlocks: string; } | null>(null); const fieldModalSnapshotRef = useRef(null); const resetFieldTypeDrafts = useCallback(() => { setTextBlockTitle(""); setTextPlaceholderBody(""); setBadgeBlockTitle(""); setBadgeOptions([]); setUploadBlockTitle(""); setUploadFileName(undefined); setUploadAssetUrl(undefined); setUploadFieldBusy(false); setUploadFieldError(null); setProportionBlockTitle(""); setProportionDefault(50); if (fileInputRef.current) { fileInputRef.current.value = ""; } }, []); const reset = useCallback(() => { setWizardStep(1); setPolicyTitle(""); setPolicyDescription(""); setAddFieldExpanded(false); setFieldTypeModal(null); setEditingBlockId(null); setDraftFieldBlocks([]); openSnapshotRef.current = null; fieldModalSnapshotRef.current = null; resetFieldTypeDrafts(); }, [resetFieldTypeDrafts]); useEffect(() => { if (!isOpen) { reset(); return; } const init = initialValuesRef.current; setWizardStep(1); setPolicyTitle(init?.title ?? ""); setPolicyDescription(init?.description ?? ""); setAddFieldExpanded(false); setFieldTypeModal(null); setEditingBlockId(null); setDraftFieldBlocks( init?.fieldBlocks ? structuredClone(init.fieldBlocks) : [], ); openSnapshotRef.current = { title: init?.title ?? "", description: init?.description ?? "", fieldBlocks: JSON.stringify(init?.fieldBlocks ?? []), }; fieldModalSnapshotRef.current = null; resetFieldTypeDrafts(); }, [isOpen, reset, resetFieldTypeDrafts]); const dismiss = useCallback(() => { reset(); onClose(); }, [onClose, reset]); const fieldModalDraftSignature = useCallback(() => { return JSON.stringify({ fieldTypeModal, textBlockTitle, textPlaceholderBody, badgeBlockTitle, badgeOptions, uploadBlockTitle, uploadFileName, uploadAssetUrl, proportionBlockTitle, proportionDefault, }); }, [ badgeBlockTitle, badgeOptions, fieldTypeModal, proportionBlockTitle, proportionDefault, textBlockTitle, textPlaceholderBody, uploadAssetUrl, uploadBlockTitle, uploadFileName, ]); const isWizardSessionDirty = useCallback(() => { const snap = openSnapshotRef.current; if (!snap) return false; if (policyTitle !== snap.title || policyDescription !== snap.description) { return true; } if (JSON.stringify(draftFieldBlocks) !== snap.fieldBlocks) { return true; } if ( fieldTypeModal && fieldModalSnapshotRef.current != null && fieldModalDraftSignature() !== fieldModalSnapshotRef.current ) { return true; } return false; }, [ draftFieldBlocks, fieldModalDraftSignature, fieldTypeModal, policyDescription, policyTitle, ]); const confirmAbandonWizardEdits = useCallback(async () => { if (!isWizardSessionDirty()) { return true; } return requestConfirm({ title: menuCopy.discardUnsavedCustomizeChangesTitle, description: menuCopy.discardUnsavedCustomizeChangesDescription, proceedText: menuCopy.discardUnsavedCustomizeChangesProceed, cancelText: menuCopy.discardUnsavedCustomizeChangesCancel, }); }, [ isWizardSessionDirty, menuCopy.discardUnsavedCustomizeChangesCancel, menuCopy.discardUnsavedCustomizeChangesDescription, menuCopy.discardUnsavedCustomizeChangesProceed, menuCopy.discardUnsavedCustomizeChangesTitle, requestConfirm, ]); const titleTrim = policyTitle.trim(); const descriptionTrim = policyDescription.trim(); const stepValid = useMemo(() => { const titleOk = titleTrim.length > 0 && titleTrim.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS; const descriptionOk = descriptionTrim.length > 0 && descriptionTrim.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_DESCRIPTION_CHARS; if (wizardStep === 1) return titleOk; if (wizardStep === 2) return descriptionOk; return titleOk && descriptionOk; }, [ descriptionTrim.length, titleTrim.length, wizardStep, ]); const fieldModalStepValid = useMemo(() => { if (!fieldTypeModal) return false; if (fieldTypeModal === "text") { const t0 = textBlockTitle.trim(); return ( t0.length > 0 && t0.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS ); } if (fieldTypeModal === "badges") { const t0 = badgeBlockTitle.trim(); return ( t0.length > 0 && t0.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS ); } if (fieldTypeModal === "upload") { const t0 = uploadBlockTitle.trim(); const titleOk = t0.length > 0 && t0.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS; if (!titleOk) return false; if (onPersistCustomUploadFile) { return Boolean(uploadAssetUrl?.trim()); } return true; } const t0 = proportionBlockTitle.trim(); return ( t0.length > 0 && t0.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS && proportionDefault >= 1 && proportionDefault <= 100 ); }, [ badgeBlockTitle, fieldTypeModal, proportionBlockTitle, proportionDefault, textBlockTitle, uploadBlockTitle, uploadAssetUrl, onPersistCustomUploadFile, ]); const headerTitle = wizardStep === 1 ? copy.step1.title : wizardStep === 2 ? copy.step2.title : copy.step3.title; const headerDescription = wizardStep === 1 ? copy.step1.description : wizardStep === 2 ? copy.step2.description : copy.step3.description; const fieldModalHeader = fieldTypeModal ? copy.fieldModals[fieldTypeModal] : null; const shellTitle = fieldModalHeader?.title ?? headerTitle; const shellDescription = fieldModalHeader?.description ?? headerDescription; const nextLabel = fieldTypeModal ? editingBlockId ? copy.fieldModals.saveField : copy.fieldModals.addField : wizardStep === 3 ? copy.footerFinalize : t("buttons.next"); const shellNextDisabled = fieldTypeModal ? !fieldModalStepValid : !stepValid; const handleShellClose = useCallback(async () => { if (fieldTypeModal) { if ( fieldModalSnapshotRef.current != null && fieldModalDraftSignature() !== fieldModalSnapshotRef.current && !(await confirmAbandonWizardEdits()) ) { return; } setFieldTypeModal(null); setEditingBlockId(null); fieldModalSnapshotRef.current = null; return; } if (!(await confirmAbandonWizardEdits())) { return; } dismiss(); }, [ confirmAbandonWizardEdits, dismiss, fieldModalDraftSignature, fieldTypeModal, ]); const kebabMenuItems = useMemo(() => [], []); const handleBack = useCallback(async () => { if (fieldTypeModal) { if ( fieldModalSnapshotRef.current != null && fieldModalDraftSignature() !== fieldModalSnapshotRef.current && !(await confirmAbandonWizardEdits()) ) { return; } setFieldTypeModal(null); setEditingBlockId(null); fieldModalSnapshotRef.current = null; return; } if (wizardStep === 1) { if (!(await confirmAbandonWizardEdits())) { return; } dismiss(); return; } setWizardStep((s) => (s === 2 ? 1 : 2)); }, [ confirmAbandonWizardEdits, dismiss, fieldModalDraftSignature, fieldTypeModal, wizardStep, ]); const handleSelectFieldType = useCallback((ft: AddCustomFieldType) => { setEditingBlockId(null); setAddFieldExpanded(false); resetFieldTypeDrafts(); setFieldTypeModal(ft); fieldModalSnapshotRef.current = JSON.stringify({ fieldTypeModal: ft, textBlockTitle: "", textPlaceholderBody: "", badgeBlockTitle: "", badgeOptions: [], uploadBlockTitle: "", uploadFileName: undefined, uploadAssetUrl: undefined, proportionBlockTitle: "", proportionDefault: 50, }); }, [resetFieldTypeDrafts]); const handleEditFieldBlock = useCallback( (block: CustomMethodCardFieldBlock) => { resetFieldTypeDrafts(); setEditingBlockId(block.id); setAddFieldExpanded(false); switch (block.kind) { case "text": setTextBlockTitle(block.blockTitle); setTextPlaceholderBody(block.placeholderText); break; case "badges": setBadgeBlockTitle(block.blockTitle); setBadgeOptions([...block.options]); break; case "upload": setUploadBlockTitle(block.blockTitle); setUploadFileName(block.fileName); setUploadAssetUrl(block.assetUrl); break; case "proportion": setProportionBlockTitle(block.blockTitle); setProportionDefault(block.defaultPercent); break; } setFieldTypeModal(block.kind); fieldModalSnapshotRef.current = JSON.stringify({ fieldTypeModal: block.kind, textBlockTitle: block.kind === "text" ? block.blockTitle : "", textPlaceholderBody: block.kind === "text" ? block.placeholderText : "", badgeBlockTitle: block.kind === "badges" ? block.blockTitle : "", badgeOptions: block.kind === "badges" ? [...block.options] : [], uploadBlockTitle: block.kind === "upload" ? block.blockTitle : "", uploadFileName: block.kind === "upload" ? block.fileName : undefined, uploadAssetUrl: block.kind === "upload" ? block.assetUrl : undefined, proportionBlockTitle: block.kind === "proportion" ? block.blockTitle : "", proportionDefault: block.kind === "proportion" ? block.defaultPercent : 50, }); }, [resetFieldTypeDrafts], ); const handleFileChosen = useCallback( async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; setUploadFileName(file?.name); setUploadAssetUrl(undefined); setUploadFieldError(null); if (!file || !onPersistCustomUploadFile) return; setUploadFieldBusy(true); try { const { url } = await onPersistCustomUploadFile(file); setUploadAssetUrl(url); } catch { setUploadFieldError(tUpload("errors.generic")); } finally { setUploadFieldBusy(false); } }, [onPersistCustomUploadFile, tUpload], ); const handleClearPendingUpload = useCallback(() => { setUploadFileName(undefined); setUploadAssetUrl(undefined); setUploadFieldError(null); setUploadFieldBusy(false); if (fileInputRef.current) { fileInputRef.current.value = ""; } }, []); const handleBadgeAddOption = useCallback((label: string) => { setBadgeOptions((prev) => prev.includes(label) ? prev : [...prev, label], ); }, []); const commitFieldBlock = useCallback(() => { if (!fieldTypeModal || !fieldModalStepValid) return; const id = editingBlockId ?? crypto.randomUUID(); let block: CustomMethodCardFieldBlock; switch (fieldTypeModal) { case "text": block = { kind: "text", id, blockTitle: textBlockTitle.trim(), placeholderText: textPlaceholderBody, }; break; case "badges": block = { kind: "badges", id, blockTitle: badgeBlockTitle.trim(), options: [...badgeOptions], }; break; case "upload": block = { kind: "upload", id, blockTitle: uploadBlockTitle.trim(), fileName: uploadFileName, ...(uploadAssetUrl?.trim() ? { assetUrl: uploadAssetUrl.trim() } : {}), }; break; default: block = { kind: "proportion", id, blockTitle: proportionBlockTitle.trim(), defaultPercent: proportionDefault, }; } setDraftFieldBlocks((prev) => { if (editingBlockId) { return prev.map((row) => (row.id === editingBlockId ? block : row)); } return [...prev, block]; }); setFieldTypeModal(null); setEditingBlockId(null); setAddFieldExpanded(false); fieldModalSnapshotRef.current = null; }, [ badgeBlockTitle, badgeOptions, editingBlockId, fieldModalStepValid, fieldTypeModal, proportionBlockTitle, proportionDefault, textBlockTitle, textPlaceholderBody, uploadBlockTitle, uploadFileName, uploadAssetUrl, ]); const handleNext = useCallback(() => { if (fieldTypeModal) { commitFieldBlock(); return; } if (!stepValid) return; if (wizardStep === 3) { onFinalize({ title: titleTrim, description: descriptionTrim, fieldBlocks: draftFieldBlocks, }); dismiss(); return; } setWizardStep((s) => (s === 1 ? 2 : 3)); }, [ commitFieldBlock, descriptionTrim, dismiss, draftFieldBlocks, fieldTypeModal, onFinalize, stepValid, titleTrim, wizardStep, ]); return ( <> setAddFieldExpanded(true)} onSelectFieldType={handleSelectFieldType} fieldTypeModal={fieldTypeModal} fieldBodiesCopy={fieldBodiesCopy} fieldBodiesProps={{ textBlockTitle, textPlaceholderBody, onTextBlockTitleChange: setTextBlockTitle, onTextPlaceholderBodyChange: setTextPlaceholderBody, badgeBlockTitle, badgeOptions, onBadgeBlockTitleChange: setBadgeBlockTitle, onBadgeAddOption: handleBadgeAddOption, uploadBlockTitle, onUploadBlockTitleChange: setUploadBlockTitle, fileInputRef, onFileChosen: handleFileChosen, onClearPendingUpload: handleClearPendingUpload, uploadAssetPreviewUrl: uploadAssetUrl, uploadPersisting: Boolean(fieldTypeModal === "upload" && uploadFieldBusy), uploadBusyHint: tUpload("uploading"), uploadErrorMessage: fieldTypeModal === "upload" ? uploadFieldError : null, proportionBlockTitle, proportionDefault, onProportionBlockTitleChange: setProportionBlockTitle, onProportionDefaultChange: setProportionDefault, }} nextDisabled={shellNextDisabled} nextLabel={nextLabel} showBackButton onBack={handleBack} onNext={handleNext} stepper={!fieldTypeModal} draftFieldBlocks={draftFieldBlocks} onDraftFieldBlocksReorder={setDraftFieldBlocks} onEditFieldBlock={handleEditFieldBlock} kebabMoreOptionsAriaLabel={menuCopy.triggerAriaLabel} kebabMenuAriaLabel={menuCopy.menuAriaLabel} kebabMenuItems={kebabMenuItems} /> {confirmDialog} ); }, ); CustomMethodCardWizardContainer.displayName = "CustomMethodCardWizard"; export default CustomMethodCardWizardContainer;