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:
+226
-15
@@ -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}
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user