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
@@ -51,20 +51,20 @@ import type { MembershipMethodDetailEntry } from "../../types";
import CustomMethodCardModalBody from "../../components/CustomMethodCardModalBody";
import { buildCustomRuleModalKebabMenu } from "../../components/customRuleModalKebabMenu";
import { methodCardMetaWithCustomizeHeader } from "../../../../../lib/create/methodCardCustomizeMetaPatch";
import { buildMethodCardWizardInitialValues } from "../../../../../lib/create/methodCardWizardPrefill";
import type { MethodCardWizardInitialValues } from "../../../../../lib/create/methodCardWizardPrefill";
import {
captureMethodCardCustomizeSnapshot,
type MethodCardCustomizeSnapshot,
type MethodCardHeaderDraft,
} from "../../../../../lib/create/methodCardCustomizeSession";
import MethodCardCustomizeModalHeader from "../../components/MethodCardCustomizeModalHeader";
export function MembershipMethodsScreen() {
const m = useMessages();
const mem = m.create.customRule.membership;
const modalKebabMenu = m.create.customRule.modalKebabMenu;
const mdUp = useCreateFlowMdUp();
const { confirmDiscard, confirmDirtyCustomizeCancel, confirmDialog } =
useDiscardCustomizeConfirm();
const { confirmDiscard, confirmDialog } = useDiscardCustomizeConfirm();
const { state, updateState, replaceState, markCreateFlowInteraction } =
useCreateFlow();
const pendingEphemeralDuplicateIdRef = useRef<string | null>(null);
@@ -77,12 +77,14 @@ export function MembershipMethodsScreen() {
const [pendingDraft, setPendingDraft] =
useState<MembershipMethodDetailEntry | null>(null);
const [addCustomWizardOpen, setAddCustomWizardOpen] = useState(false);
const [modalEditUnlocked, setModalEditUnlocked] = useState(false);
const [wizardCustomizeCardId, setWizardCustomizeCardId] = useState<
string | null
>(null);
const [wizardInitialValues, setWizardInitialValues] =
useState<MethodCardWizardInitialValues | null>(null);
const [draftFieldBlocks, setDraftFieldBlocks] = useState<
CustomMethodCardFieldBlock[] | null
>(null);
const [customizeHeaderDraft, setCustomizeHeaderDraft] =
useState<MethodCardHeaderDraft | null>(null);
const selectedIds = state.selectedMembershipMethodIds ?? [];
@@ -105,6 +107,8 @@ export function MembershipMethodsScreen() {
const handleOpenAddWizard = useCallback(() => {
markCreateFlowInteraction();
setWizardCustomizeCardId(null);
setWizardInitialValues(null);
setAddCustomWizardOpen(true);
}, [markCreateFlowInteraction]);
@@ -142,15 +146,40 @@ export function MembershipMethodsScreen() {
const handleCardClick = useCallback(
(id: string) => {
markCreateFlowInteraction();
customizeSnapshotRef.current = null;
setModalEditUnlocked(false);
setDraftFieldBlocks(null);
setCustomizeHeaderDraft(null);
const draft = seedDraft(id);
const persistedBlocks = state.customMethodCardFieldBlocksById?.[id];
const initialBlocks =
Array.isArray(persistedBlocks) && persistedBlocks.length > 0
? structuredClone(persistedBlocks)
: null;
const method = methodById.get(id);
const meta = state.customMethodCardMetaById?.[id];
const headerDraft: MethodCardHeaderDraft = {
title: meta?.label ?? method?.label ?? mem.confirmModal.title,
description:
meta?.supportText ??
method?.supportText ??
mem.confirmModal.description,
};
customizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
draft,
initialBlocks,
headerDraft,
);
setPendingCardId(id);
setPendingDraft(seedDraft(id));
setPendingDraft(draft);
setDraftFieldBlocks(initialBlocks);
setCreateModalOpen(true);
},
[markCreateFlowInteraction, seedDraft],
[
mem.confirmModal.description,
mem.confirmModal.title,
markCreateFlowInteraction,
methodById,
seedDraft,
state.customMethodCardFieldBlocksById,
state.customMethodCardMetaById,
],
);
const handleDraftChange = useCallback(
@@ -163,9 +192,7 @@ export function MembershipMethodsScreen() {
const isSelectedCardModal =
pendingCardId !== null && selectedIds.includes(pendingCardId);
const fieldsLocked = !modalEditUnlocked;
const showMethodModalPrimary = !isSelectedCardModal || modalEditUnlocked;
const showMethodModalPrimary = true;
const customFacetDetailsMatchPreset = useMemo(() => {
if (!pendingCardId || !pendingDraft) return false;
@@ -187,7 +214,7 @@ export function MembershipMethodsScreen() {
methodId: pendingCardId,
meta: state.customMethodCardMetaById,
fieldBlocksById: state.customMethodCardFieldBlocksById,
modalEditUnlocked,
modalEditUnlocked: false,
draftFieldBlocks,
customFacetDetailsMatchPreset,
}),
@@ -195,7 +222,6 @@ export function MembershipMethodsScreen() {
[
customFacetDetailsMatchPreset,
draftFieldBlocks,
modalEditUnlocked,
pendingCardId,
state.customMethodCardFieldBlocksById,
state.customMethodCardMetaById,
@@ -205,11 +231,11 @@ export function MembershipMethodsScreen() {
const handleCreateModalClose = useCallback(async () => {
if (
!(await confirmDiscard(
modalEditUnlocked,
true,
customizeSnapshotRef.current,
pendingDraft,
draftFieldBlocks,
customizeHeaderDraft,
customizeSnapshotRef.current?.headerDraft ?? null,
))
) {
return;
@@ -237,53 +263,14 @@ export function MembershipMethodsScreen() {
setCreateModalOpen(false);
setPendingCardId(null);
setPendingDraft(null);
setModalEditUnlocked(false);
setDraftFieldBlocks(null);
setCustomizeHeaderDraft(null);
}, [
confirmDiscard,
customizeHeaderDraft,
draftFieldBlocks,
modalEditUnlocked,
pendingDraft,
replaceState,
]);
const handleCancelCustomize = useCallback(async () => {
if (!modalEditUnlocked) {
return;
}
const snap = customizeSnapshotRef.current;
if (!snap) {
customizeSnapshotRef.current = null;
setModalEditUnlocked(false);
setDraftFieldBlocks(null);
setCustomizeHeaderDraft(null);
return;
}
if (
!(await confirmDirtyCustomizeCancel(
snap,
pendingDraft,
draftFieldBlocks,
customizeHeaderDraft,
))
) {
return;
}
setPendingDraft(structuredClone(snap.pendingDraft));
setDraftFieldBlocks(null);
setModalEditUnlocked(false);
customizeSnapshotRef.current = null;
setCustomizeHeaderDraft(null);
}, [
confirmDirtyCustomizeCancel,
customizeHeaderDraft,
draftFieldBlocks,
modalEditUnlocked,
pendingDraft,
]);
const handleRemoveSelectedFromModal = useCallback(async () => {
if (!pendingCardId || !selectedIds.includes(pendingCardId)) {
return;
@@ -291,11 +278,11 @@ export function MembershipMethodsScreen() {
markCreateFlowInteraction();
if (
!(await confirmDiscard(
modalEditUnlocked,
true,
customizeSnapshotRef.current,
pendingDraft,
draftFieldBlocks,
customizeHeaderDraft,
customizeSnapshotRef.current?.headerDraft ?? null,
))
) {
return;
@@ -307,11 +294,9 @@ export function MembershipMethodsScreen() {
await handleCreateModalClose();
}, [
confirmDiscard,
customizeHeaderDraft,
draftFieldBlocks,
handleCreateModalClose,
markCreateFlowInteraction,
modalEditUnlocked,
pendingDraft,
pendingCardId,
selectedIds,
@@ -321,35 +306,36 @@ export function MembershipMethodsScreen() {
const handleCustomize = useCallback(() => {
markCreateFlowInteraction();
if (!pendingDraft || !pendingCardId) {
if (!pendingCardId) {
return;
}
const initialFieldBlocks =
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById)
? structuredClone(
state.customMethodCardFieldBlocksById?.[pendingCardId] ?? [],
)
: null;
const method = methodById.get(pendingCardId);
const meta = state.customMethodCardMetaById?.[pendingCardId];
const headerDraft: MethodCardHeaderDraft = {
title: meta?.label ?? method?.label ?? mem.confirmModal.title,
description:
meta?.supportText ??
method?.supportText ??
mem.confirmModal.description,
};
setCustomizeHeaderDraft(headerDraft);
customizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
pendingDraft,
initialFieldBlocks,
headerDraft,
setWizardInitialValues(
buildMethodCardWizardInitialValues({
cardId: pendingCardId,
fallbackTitle: method?.label ?? mem.confirmModal.title,
fallbackDescription:
method?.supportText ?? mem.confirmModal.description,
meta: state.customMethodCardMetaById,
persistedBlocks: state.customMethodCardFieldBlocksById,
draftFieldBlocks,
facetPrefill: pendingDraft
? {
group: "membership",
draft: pendingDraft,
headings: mem.sectionHeadings,
}
: undefined,
}),
);
setDraftFieldBlocks(initialFieldBlocks);
setModalEditUnlocked(true);
setWizardCustomizeCardId(pendingCardId);
setCreateModalOpen(false);
setAddCustomWizardOpen(true);
}, [
mem.confirmModal.description,
mem.confirmModal.title,
mem.sectionHeadings,
draftFieldBlocks,
markCreateFlowInteraction,
methodById,
pendingCardId,
@@ -374,9 +360,7 @@ export function MembershipMethodsScreen() {
() => membershipPresetFor(newId),
);
const blocksClone = structuredClone(
modalEditUnlocked &&
draftFieldBlocks !== null &&
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById)
draftFieldBlocks !== null
? draftFieldBlocks
: cloneMethodCardBlocksForDuplicate(
state.customMethodCardFieldBlocksById,
@@ -406,13 +390,12 @@ export function MembershipMethodsScreen() {
customizeSnapshotRef.current = null;
setPendingCardId(newId);
setPendingDraft(structuredClone(detailsClone));
setModalEditUnlocked(false);
setDraftFieldBlocks(null);
setCustomizeHeaderDraft(null);
setDraftFieldBlocks(
blocksClone.length > 0 ? structuredClone(blocksClone) : null,
);
}, [
draftFieldBlocks,
markCreateFlowInteraction,
modalEditUnlocked,
modalKebabMenu.duplicateTitleSuffix,
pendingCardId,
pendingDraft,
@@ -441,9 +424,7 @@ export function MembershipMethodsScreen() {
() => membershipPresetFor(newId),
);
const blocksClone = structuredClone(
modalEditUnlocked &&
draftFieldBlocks !== null &&
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById)
draftFieldBlocks !== null
? draftFieldBlocks
: cloneMethodCardBlocksForDuplicate(
state.customMethodCardFieldBlocksById,
@@ -473,14 +454,13 @@ export function MembershipMethodsScreen() {
customizeSnapshotRef.current = null;
setPendingCardId(newId);
setPendingDraft(structuredClone(detailsClone));
setModalEditUnlocked(false);
setDraftFieldBlocks(null);
setCustomizeHeaderDraft(null);
setDraftFieldBlocks(
blocksClone.length > 0 ? structuredClone(blocksClone) : null,
);
}, [
draftFieldBlocks,
markCreateFlowInteraction,
methodById,
modalEditUnlocked,
modalKebabMenu.duplicateTitleSuffix,
pendingCardId,
pendingDraft,
@@ -493,7 +473,7 @@ export function MembershipMethodsScreen() {
const kebabMenuItems = useMemo(
() =>
buildCustomRuleModalKebabMenu(modalKebabMenu, {
showCustomize: !modalEditUnlocked,
showCustomize: true,
onCustomize: handleCustomize,
onDuplicate:
(state.editingPublishedRuleId?.trim() ?? "") !== "" || !pendingCardId
@@ -513,7 +493,6 @@ export function MembershipMethodsScreen() {
handleDuplicatePrefabCard,
handleRemoveSelectedFromModal,
isSelectedCardModal,
modalEditUnlocked,
modalKebabMenu,
pendingCardId,
state.customMethodCardMetaById,
@@ -532,7 +511,7 @@ export function MembershipMethodsScreen() {
meta?.supportText ??
method?.supportText ??
mem.confirmModal.description,
nextButtonText: modalEditUnlocked
nextButtonText: isSelectedCardModal
? saveLabel
: mem.addPlatform.nextButtonText,
};
@@ -544,8 +523,14 @@ export function MembershipMethodsScreen() {
};
const handleCloseAddWizard = useCallback(() => {
const resumeCardId = wizardCustomizeCardId;
setAddCustomWizardOpen(false);
}, []);
setWizardCustomizeCardId(null);
setWizardInitialValues(null);
if (resumeCardId && pendingCardId === resumeCardId) {
setCreateModalOpen(true);
}
}, [pendingCardId, wizardCustomizeCardId]);
const handleFinalizeCustomCard = useCallback(
({
@@ -558,6 +543,42 @@ export function MembershipMethodsScreen() {
fieldBlocks: CustomMethodCardFieldBlock[];
}) => {
markCreateFlowInteraction();
const existingId = wizardCustomizeCardId;
if (existingId) {
updateState({
selectedMembershipMethodIds: moveFacetSelectionIdToFront(
selectedIds,
existingId,
),
customMethodCardMetaById: methodCardMetaWithCustomizeHeader(
state.customMethodCardMetaById,
existingId,
{ title, description },
),
...(pendingDraft
? {
membershipMethodDetailsById: {
...(state.membershipMethodDetailsById ?? {}),
[existingId]: pendingDraft,
},
}
: {}),
customMethodCardFieldBlocksById: {
...(state.customMethodCardFieldBlocksById ?? {}),
[existingId]: fieldBlocks,
},
});
if (pendingDraft) {
customizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
pendingDraft,
fieldBlocks,
{ title, description },
);
}
setDraftFieldBlocks(structuredClone(fieldBlocks));
setAddCustomWizardOpen(false);
return;
}
const id = crypto.randomUUID();
updateState({
selectedMembershipMethodIds: moveFacetSelectionIdToFront(
@@ -580,91 +601,29 @@ export function MembershipMethodsScreen() {
},
[
markCreateFlowInteraction,
pendingDraft,
selectedIds,
state.customMethodCardFieldBlocksById,
state.customMethodCardMetaById,
state.membershipMethodDetailsById,
updateState,
wizardCustomizeCardId,
],
);
const handleCreateModalPrimary = useCallback(() => {
if (!pendingCardId) {
handleCreateModalClose();
void handleCreateModalClose();
return;
}
markCreateFlowInteraction();
if (selectedIds.includes(pendingCardId)) {
if (modalEditUnlocked) {
if (!customizeHeaderDraft) {
return;
}
const nextMeta = methodCardMetaWithCustomizeHeader(
state.customMethodCardMetaById,
pendingCardId,
customizeHeaderDraft,
);
if (
pendingCardId &&
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById) &&
usesWizardFieldBlocksModalBody({
methodId: pendingCardId,
meta: state.customMethodCardMetaById,
fieldBlocksById: state.customMethodCardFieldBlocksById,
modalEditUnlocked,
draftFieldBlocks,
customFacetDetailsMatchPreset,
})
) {
updateState({
customMethodCardMetaById: nextMeta,
customMethodCardFieldBlocksById: {
...(state.customMethodCardFieldBlocksById ?? {}),
[pendingCardId]: structuredClone(draftFieldBlocks ?? []),
},
});
} else if (pendingDraft) {
updateState({
customMethodCardMetaById: nextMeta,
membershipMethodDetailsById: {
...(state.membershipMethodDetailsById ?? {}),
[pendingCardId]: pendingDraft,
},
});
}
customizeSnapshotRef.current = null;
setModalEditUnlocked(false);
setDraftFieldBlocks(null);
setCustomizeHeaderDraft(null);
return;
}
return;
}
const persistWizardBlocks =
modalUsesWizardFieldBlocksBody && draftFieldBlocks !== null;
if (modalEditUnlocked) {
if (!customizeHeaderDraft) {
return;
}
const nextMeta = methodCardMetaWithCustomizeHeader(
state.customMethodCardMetaById,
pendingCardId,
customizeHeaderDraft,
);
if (
pendingCardId &&
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById) &&
usesWizardFieldBlocksModalBody({
methodId: pendingCardId,
meta: state.customMethodCardMetaById,
fieldBlocksById: state.customMethodCardFieldBlocksById,
modalEditUnlocked,
draftFieldBlocks,
customFacetDetailsMatchPreset,
})
) {
if (selectedIds.includes(pendingCardId)) {
if (persistWizardBlocks) {
updateState({
customMethodCardMetaById: nextMeta,
customMethodCardFieldBlocksById: {
...(state.customMethodCardFieldBlocksById ?? {}),
[pendingCardId]: structuredClone(draftFieldBlocks ?? []),
@@ -672,22 +631,27 @@ export function MembershipMethodsScreen() {
});
} else if (pendingDraft) {
updateState({
customMethodCardMetaById: nextMeta,
membershipMethodDetailsById: {
...(state.membershipMethodDetailsById ?? {}),
[pendingCardId]: pendingDraft,
},
});
}
customizeSnapshotRef.current = null;
setModalEditUnlocked(false);
setDraftFieldBlocks(null);
setCustomizeHeaderDraft(null);
if (pendingDraft) {
customizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
pendingDraft,
persistWizardBlocks ? draftFieldBlocks : null,
customizeSnapshotRef.current?.headerDraft ?? {
title: "",
description: "",
},
);
}
return;
}
if (!pendingDraft) {
handleCreateModalClose();
void handleCreateModalClose();
return;
}
updateState({
@@ -699,15 +663,23 @@ export function MembershipMethodsScreen() {
...(state.membershipMethodDetailsById ?? {}),
[pendingCardId]: pendingDraft,
},
...(persistWizardBlocks
? {
customMethodCardFieldBlocksById: {
...(state.customMethodCardFieldBlocksById ?? {}),
[pendingCardId]: structuredClone(draftFieldBlocks ?? []),
},
}
: {}),
});
pendingEphemeralDuplicateIdRef.current = null;
handleCreateModalClose();
customizeSnapshotRef.current = null;
void handleCreateModalClose();
}, [
customizeHeaderDraft,
draftFieldBlocks,
handleCreateModalClose,
markCreateFlowInteraction,
modalEditUnlocked,
modalUsesWizardFieldBlocksBody,
pendingCardId,
pendingDraft,
selectedIds,
@@ -758,33 +730,10 @@ export function MembershipMethodsScreen() {
<Create
isOpen={createModalOpen}
onClose={handleCreateModalClose}
headerContent={
modalEditUnlocked && customizeHeaderDraft ? (
<MethodCardCustomizeModalHeader
titleLabel={modalKebabMenu.customizePolicyTitleLabel}
descriptionLabel={modalKebabMenu.customizePolicyDescriptionLabel}
titleValue={customizeHeaderDraft.title}
descriptionValue={customizeHeaderDraft.description}
onTitleChange={(title) =>
setCustomizeHeaderDraft((prev) =>
prev ? { ...prev, title } : null,
)
}
onDescriptionChange={(description) =>
setCustomizeHeaderDraft((prev) =>
prev ? { ...prev, description } : null,
)
}
/>
) : undefined
}
onNext={handleCreateModalPrimary}
title={modalConfig.title}
description={modalConfig.description}
nextButtonText={modalConfig.nextButtonText}
showBackButton={modalEditUnlocked}
onBack={handleCancelCustomize}
backButtonText={modalKebabMenu.cancelCustomize}
showNextButton={showMethodModalPrimary}
backdropVariant="blurredYellow"
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
@@ -797,14 +746,14 @@ export function MembershipMethodsScreen() {
cardId={pendingCardId}
blocksById={state.customMethodCardFieldBlocksById}
blocksOverride={
modalEditUnlocked && draftFieldBlocks !== null
? draftFieldBlocks
: undefined
draftFieldBlocks !== null ? draftFieldBlocks : undefined
}
policyMeta={state.customMethodCardMetaById?.[pendingCardId]}
showPolicyContentLockupWhenNoBlocks={!modalEditUnlocked}
showPolicyContentLockupWhenNoBlocks={
draftFieldBlocks === null || draftFieldBlocks.length === 0
}
onFieldBlocksChange={
fieldsLocked
draftFieldBlocks === null
? undefined
: (next) => setDraftFieldBlocks(next)
}
@@ -813,7 +762,6 @@ export function MembershipMethodsScreen() {
<MembershipMethodEditFields
value={pendingDraft}
onChange={handleDraftChange}
readOnly={fieldsLocked}
/>
)
) : null}
@@ -822,6 +770,7 @@ export function MembershipMethodsScreen() {
<CustomMethodCardWizard
isOpen={addCustomWizardOpen}
onClose={handleCloseAddWizard}
initialValues={wizardInitialValues}
onFinalize={handleFinalizeCustomCard}
onPersistCustomUploadFile={(file) =>
uploadCreateFlowFile(file, "customMethodAttachment")