Route kebab Customize through the prefilled policy wizard so existing methods and values can be renamed, reordered, and kept in that field order on the card after Finalize.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
adilallo
2026-08-21 13:51:37 -06:00
co-authored by Cursor
parent e9951f260b
commit 4006fe5c6c
31 changed files with 3062 additions and 1530 deletions
@@ -5,8 +5,12 @@ import {
useMessages,
useTranslation,
} from "../../../../contexts/MessagesContext";
import { useAsyncConfirm } from "../../../../hooks/useAsyncConfirm";
import type { CustomMethodCardFieldBlock } from "../../../../../lib/create/customMethodCardFieldBlocks";
import { CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS } from "../../../../../lib/create/customMethodCardWizardConstants";
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";
@@ -17,12 +21,13 @@ import type { CustomMethodCardWizardProps } from "./CustomMethodCardWizard.types
* `20066:14748`, `20094:48551`, `20066:14361`).
*/
const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
({ isOpen, onClose, onFinalize, onPersistCustomUploadFile }) => {
({ 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(
() => ({
@@ -68,6 +73,7 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
const [draftFieldBlocks, setDraftFieldBlocks] = useState<
CustomMethodCardFieldBlock[]
>([]);
const [editingBlockId, setEditingBlockId] = useState<string | null>(null);
const [textBlockTitle, setTextBlockTitle] = useState("");
const [textPlaceholderBody, setTextPlaceholderBody] = useState("");
@@ -88,6 +94,14 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
const [proportionDefault, setProportionDefault] = useState(50);
const fileInputRef = useRef<HTMLInputElement>(null);
const initialValuesRef = useRef(initialValues);
initialValuesRef.current = initialValues;
const openSnapshotRef = useRef<{
title: string;
description: string;
fieldBlocks: string;
} | null>(null);
const fieldModalSnapshotRef = useRef<string | null>(null);
const resetFieldTypeDrafts = useCallback(() => {
setTextBlockTitle("");
@@ -112,21 +126,112 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
setPolicyDescription("");
setAddFieldExpanded(false);
setFieldTypeModal(null);
setEditingBlockId(null);
setDraftFieldBlocks([]);
openSnapshotRef.current = null;
fieldModalSnapshotRef.current = null;
resetFieldTypeDrafts();
}, [resetFieldTypeDrafts]);
useEffect(() => {
if (!isOpen) {
reset();
return;
}
}, [isOpen, reset]);
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();
@@ -136,7 +241,7 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
titleTrim.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS;
const descriptionOk =
descriptionTrim.length > 0 &&
descriptionTrim.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS;
descriptionTrim.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_DESCRIPTION_CHARS;
if (wizardStep === 1) return titleOk;
if (wizardStep === 2) return descriptionOk;
return titleOk && descriptionOk;
@@ -213,7 +318,9 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
const shellDescription = fieldModalHeader?.description ?? headerDescription;
const nextLabel = fieldTypeModal
? copy.fieldModals.addField
? editingBlockId
? copy.fieldModals.saveField
: copy.fieldModals.addField
: wizardStep === 3
? copy.footerFinalize
: t("buttons.next");
@@ -222,33 +329,124 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
? !fieldModalStepValid
: !stepValid;
const handleShellClose = useCallback(() => {
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();
}, [dismiss, fieldTypeModal]);
}, [
confirmAbandonWizardEdits,
dismiss,
fieldModalDraftSignature,
fieldTypeModal,
]);
const kebabMenuItems = useMemo<ModalHeaderMenuItem[]>(() => [], []);
const handleBack = useCallback(() => {
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));
}, [dismiss, fieldTypeModal, wizardStep]);
}, [
confirmAbandonWizardEdits,
dismiss,
fieldModalDraftSignature,
fieldTypeModal,
wizardStep,
]);
const handleSelectFieldType = useCallback((ft: AddCustomFieldType) => {
setEditingBlockId(null);
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<HTMLInputElement>) => {
const file = e.target.files?.[0];
@@ -285,9 +483,9 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
);
}, []);
const appendFieldBlock = useCallback(() => {
const commitFieldBlock = useCallback(() => {
if (!fieldTypeModal || !fieldModalStepValid) return;
const id = crypto.randomUUID();
const id = editingBlockId ?? crypto.randomUUID();
let block: CustomMethodCardFieldBlock;
switch (fieldTypeModal) {
case "text":
@@ -325,11 +523,19 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
defaultPercent: proportionDefault,
};
}
setDraftFieldBlocks((prev) => [...prev, block]);
setDraftFieldBlocks((prev) => {
if (editingBlockId) {
return prev.map((row) => (row.id === editingBlockId ? block : row));
}
return [...prev, block];
});
setFieldTypeModal(null);
setEditingBlockId(null);
fieldModalSnapshotRef.current = null;
}, [
badgeBlockTitle,
badgeOptions,
editingBlockId,
fieldModalStepValid,
fieldTypeModal,
proportionBlockTitle,
@@ -343,7 +549,7 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
const handleNext = useCallback(() => {
if (fieldTypeModal) {
appendFieldBlock();
commitFieldBlock();
return;
}
if (!stepValid) return;
@@ -358,7 +564,7 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
}
setWizardStep((s) => (s === 1 ? 2 : 3));
}, [
appendFieldBlock,
commitFieldBlock,
descriptionTrim,
dismiss,
draftFieldBlocks,
@@ -370,6 +576,7 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
]);
return (
<>
<CustomMethodCardWizardView
isOpen={isOpen}
onDismiss={handleShellClose}
@@ -380,7 +587,8 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
policyDescription={policyDescription}
addFieldExpanded={addFieldExpanded}
copy={copy}
maxChars={CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS}
maxTitleChars={CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS}
maxDescriptionChars={CUSTOM_METHOD_CARD_WIZARD_MAX_DESCRIPTION_CHARS}
onPolicyTitleChange={setPolicyTitle}
onPolicyDescriptionChange={setPolicyDescription}
onPressAddCustomField={() => setAddFieldExpanded(true)}
@@ -420,10 +628,13 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
stepper={!fieldTypeModal}
draftFieldBlocks={draftFieldBlocks}
onDraftFieldBlocksReorder={setDraftFieldBlocks}
onEditFieldBlock={handleEditFieldBlock}
kebabMoreOptionsAriaLabel={menuCopy.triggerAriaLabel}
kebabMenuAriaLabel={menuCopy.menuAriaLabel}
kebabMenuItems={kebabMenuItems}
/>
{confirmDialog}
</>
);
},
);