434 lines
13 KiB
TypeScript
434 lines
13 KiB
TypeScript
"use client";
|
|
|
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
import {
|
|
useMessages,
|
|
useTranslation,
|
|
} from "../../../../contexts/MessagesContext";
|
|
import type { CustomMethodCardFieldBlock } from "../../../../../lib/create/customMethodCardFieldBlocks";
|
|
import { 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<CustomMethodCardWizardProps>(
|
|
({ isOpen, onClose, onFinalize, onPersistCustomUploadFile }) => {
|
|
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 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<AddCustomFieldType | null>(null);
|
|
const [draftFieldBlocks, setDraftFieldBlocks] = useState<
|
|
CustomMethodCardFieldBlock[]
|
|
>([]);
|
|
|
|
const [textBlockTitle, setTextBlockTitle] = useState("");
|
|
const [textPlaceholderBody, setTextPlaceholderBody] = useState("");
|
|
const [badgeBlockTitle, setBadgeBlockTitle] = useState("");
|
|
const [badgeOptions, setBadgeOptions] = useState<string[]>([]);
|
|
const [uploadBlockTitle, setUploadBlockTitle] = useState("");
|
|
const [uploadFileName, setUploadFileName] = useState<string | undefined>(
|
|
undefined,
|
|
);
|
|
const [uploadAssetUrl, setUploadAssetUrl] = useState<string | undefined>(
|
|
undefined,
|
|
);
|
|
const [uploadFieldBusy, setUploadFieldBusy] = useState(false);
|
|
const [uploadFieldError, setUploadFieldError] = useState<string | null>(
|
|
null,
|
|
);
|
|
const [proportionBlockTitle, setProportionBlockTitle] = useState("");
|
|
const [proportionDefault, setProportionDefault] = useState(50);
|
|
|
|
const fileInputRef = useRef<HTMLInputElement>(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);
|
|
setDraftFieldBlocks([]);
|
|
resetFieldTypeDrafts();
|
|
}, [resetFieldTypeDrafts]);
|
|
|
|
useEffect(() => {
|
|
if (!isOpen) {
|
|
reset();
|
|
}
|
|
}, [isOpen, reset]);
|
|
|
|
const dismiss = useCallback(() => {
|
|
reset();
|
|
onClose();
|
|
}, [onClose, reset]);
|
|
|
|
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_FIELD_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
|
|
? copy.fieldModals.addField
|
|
: wizardStep === 3
|
|
? copy.footerFinalize
|
|
: t("buttons.next");
|
|
|
|
const shellNextDisabled = fieldTypeModal
|
|
? !fieldModalStepValid
|
|
: !stepValid;
|
|
|
|
const handleShellClose = useCallback(() => {
|
|
if (fieldTypeModal) {
|
|
setFieldTypeModal(null);
|
|
return;
|
|
}
|
|
dismiss();
|
|
}, [dismiss, fieldTypeModal]);
|
|
|
|
const kebabMenuItems = useMemo<ModalHeaderMenuItem[]>(() => [], []);
|
|
|
|
const handleBack = useCallback(() => {
|
|
if (fieldTypeModal) {
|
|
setFieldTypeModal(null);
|
|
return;
|
|
}
|
|
if (wizardStep === 1) {
|
|
dismiss();
|
|
return;
|
|
}
|
|
setWizardStep((s) => (s === 2 ? 1 : 2));
|
|
}, [dismiss, fieldTypeModal, wizardStep]);
|
|
|
|
const handleSelectFieldType = useCallback((ft: AddCustomFieldType) => {
|
|
resetFieldTypeDrafts();
|
|
setFieldTypeModal(ft);
|
|
}, [resetFieldTypeDrafts]);
|
|
|
|
const handleFileChosen = useCallback(
|
|
async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
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 appendFieldBlock = useCallback(() => {
|
|
if (!fieldTypeModal || !fieldModalStepValid) return;
|
|
const id = 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) => [...prev, block]);
|
|
setFieldTypeModal(null);
|
|
}, [
|
|
badgeBlockTitle,
|
|
badgeOptions,
|
|
fieldModalStepValid,
|
|
fieldTypeModal,
|
|
proportionBlockTitle,
|
|
proportionDefault,
|
|
textBlockTitle,
|
|
textPlaceholderBody,
|
|
uploadBlockTitle,
|
|
uploadFileName,
|
|
uploadAssetUrl,
|
|
]);
|
|
|
|
const handleNext = useCallback(() => {
|
|
if (fieldTypeModal) {
|
|
appendFieldBlock();
|
|
return;
|
|
}
|
|
if (!stepValid) return;
|
|
if (wizardStep === 3) {
|
|
onFinalize({
|
|
title: titleTrim,
|
|
description: descriptionTrim,
|
|
fieldBlocks: draftFieldBlocks,
|
|
});
|
|
dismiss();
|
|
return;
|
|
}
|
|
setWizardStep((s) => (s === 1 ? 2 : 3));
|
|
}, [
|
|
appendFieldBlock,
|
|
descriptionTrim,
|
|
dismiss,
|
|
draftFieldBlocks,
|
|
fieldTypeModal,
|
|
onFinalize,
|
|
stepValid,
|
|
titleTrim,
|
|
wizardStep,
|
|
]);
|
|
|
|
return (
|
|
<CustomMethodCardWizardView
|
|
isOpen={isOpen}
|
|
onDismiss={handleShellClose}
|
|
wizardStep={wizardStep}
|
|
title={shellTitle}
|
|
description={shellDescription}
|
|
policyTitle={policyTitle}
|
|
policyDescription={policyDescription}
|
|
addFieldExpanded={addFieldExpanded}
|
|
copy={copy}
|
|
maxChars={CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS}
|
|
onPolicyTitleChange={setPolicyTitle}
|
|
onPolicyDescriptionChange={setPolicyDescription}
|
|
onPressAddCustomField={() => 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}
|
|
kebabMoreOptionsAriaLabel={menuCopy.triggerAriaLabel}
|
|
kebabMenuAriaLabel={menuCopy.menuAriaLabel}
|
|
kebabMenuItems={kebabMenuItems}
|
|
/>
|
|
);
|
|
},
|
|
);
|
|
|
|
CustomMethodCardWizardContainer.displayName = "CustomMethodCardWizard";
|
|
|
|
export default CustomMethodCardWizardContainer;
|