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
@@ -53,20 +53,20 @@ import type { CommunicationMethodDetailEntry } 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 CommunicationMethodsScreen() {
const m = useMessages();
const comm = m.create.customRule.communication;
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);
@@ -79,12 +79,14 @@ export function CommunicationMethodsScreen() {
const [pendingDraft, setPendingDraft] =
useState<CommunicationMethodDetailEntry | 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.selectedCommunicationMethodIds ?? [];
@@ -107,6 +109,8 @@ export function CommunicationMethodsScreen() {
const handleOpenAddWizard = useCallback(() => {
markCreateFlowInteraction();
setWizardCustomizeCardId(null);
setWizardInitialValues(null);
setAddCustomWizardOpen(true);
}, [markCreateFlowInteraction]);
@@ -144,15 +148,40 @@ export function CommunicationMethodsScreen() {
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 ?? comm.confirmModal.title,
description:
meta?.supportText ??
method?.supportText ??
comm.confirmModal.description,
};
customizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
draft,
initialBlocks,
headerDraft,
);
setPendingCardId(id);
setPendingDraft(seedDraft(id));
setPendingDraft(draft);
setDraftFieldBlocks(initialBlocks);
setCreateModalOpen(true);
},
[markCreateFlowInteraction, seedDraft],
[
comm.confirmModal.description,
comm.confirmModal.title,
markCreateFlowInteraction,
methodById,
seedDraft,
state.customMethodCardFieldBlocksById,
state.customMethodCardMetaById,
],
);
const handleDraftChange = useCallback(
@@ -165,9 +194,7 @@ export function CommunicationMethodsScreen() {
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;
@@ -189,7 +216,7 @@ export function CommunicationMethodsScreen() {
methodId: pendingCardId,
meta: state.customMethodCardMetaById,
fieldBlocksById: state.customMethodCardFieldBlocksById,
modalEditUnlocked,
modalEditUnlocked: false,
draftFieldBlocks,
customFacetDetailsMatchPreset,
}),
@@ -197,7 +224,6 @@ export function CommunicationMethodsScreen() {
[
customFacetDetailsMatchPreset,
draftFieldBlocks,
modalEditUnlocked,
pendingCardId,
state.customMethodCardFieldBlocksById,
state.customMethodCardMetaById,
@@ -207,11 +233,11 @@ export function CommunicationMethodsScreen() {
const handleCreateModalClose = useCallback(async () => {
if (
!(await confirmDiscard(
modalEditUnlocked,
true,
customizeSnapshotRef.current,
pendingDraft,
draftFieldBlocks,
customizeHeaderDraft,
customizeSnapshotRef.current?.headerDraft ?? null,
))
) {
return;
@@ -239,53 +265,14 @@ export function CommunicationMethodsScreen() {
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;
@@ -293,11 +280,11 @@ export function CommunicationMethodsScreen() {
markCreateFlowInteraction();
if (
!(await confirmDiscard(
modalEditUnlocked,
true,
customizeSnapshotRef.current,
pendingDraft,
draftFieldBlocks,
customizeHeaderDraft,
customizeSnapshotRef.current?.headerDraft ?? null,
))
) {
return;
@@ -313,11 +300,9 @@ export function CommunicationMethodsScreen() {
await handleCreateModalClose();
}, [
confirmDiscard,
customizeHeaderDraft,
draftFieldBlocks,
handleCreateModalClose,
markCreateFlowInteraction,
modalEditUnlocked,
pendingDraft,
pendingCardId,
selectedIds,
@@ -327,37 +312,36 @@ export function CommunicationMethodsScreen() {
const handleCustomize = useCallback(() => {
markCreateFlowInteraction();
if (!pendingDraft || !pendingCardId) {
if (!pendingCardId) {
return;
}
const persistedBlocks =
state.customMethodCardFieldBlocksById?.[pendingCardId] ?? [];
const initialFieldBlocks =
persistedBlocks.length > 0
? structuredClone(persistedBlocks)
: isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById)
? []
: null;
const method = methodById.get(pendingCardId);
const meta = state.customMethodCardMetaById?.[pendingCardId];
const headerDraft: MethodCardHeaderDraft = {
title: meta?.label ?? method?.label ?? comm.confirmModal.title,
description:
meta?.supportText ??
method?.supportText ??
comm.confirmModal.description,
};
setCustomizeHeaderDraft(headerDraft);
customizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
pendingDraft,
initialFieldBlocks,
headerDraft,
setWizardInitialValues(
buildMethodCardWizardInitialValues({
cardId: pendingCardId,
fallbackTitle: method?.label ?? comm.confirmModal.title,
fallbackDescription:
method?.supportText ?? comm.confirmModal.description,
meta: state.customMethodCardMetaById,
persistedBlocks: state.customMethodCardFieldBlocksById,
draftFieldBlocks,
facetPrefill: pendingDraft
? {
group: "communication",
draft: pendingDraft,
headings: comm.sectionHeadings,
}
: undefined,
}),
);
setDraftFieldBlocks(initialFieldBlocks);
setModalEditUnlocked(true);
setWizardCustomizeCardId(pendingCardId);
setCreateModalOpen(false);
setAddCustomWizardOpen(true);
}, [
comm.confirmModal.description,
comm.confirmModal.title,
comm.sectionHeadings,
draftFieldBlocks,
markCreateFlowInteraction,
methodById,
pendingCardId,
@@ -382,9 +366,7 @@ export function CommunicationMethodsScreen() {
() => communicationPresetFor(newId),
);
const blocksClone = structuredClone(
modalEditUnlocked &&
draftFieldBlocks !== null &&
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById)
draftFieldBlocks !== null
? draftFieldBlocks
: cloneMethodCardBlocksForDuplicate(
state.customMethodCardFieldBlocksById,
@@ -414,16 +396,15 @@ export function CommunicationMethodsScreen() {
customizeSnapshotRef.current = null;
setPendingCardId(newId);
setPendingDraft(structuredClone(detailsClone));
setModalEditUnlocked(false);
setDraftFieldBlocks(null);
setCustomizeHeaderDraft(null);
setDraftFieldBlocks(
blocksClone.length > 0 ? structuredClone(blocksClone) : null,
);
}, [
markCreateFlowInteraction,
modalKebabMenu.duplicateTitleSuffix,
pendingCardId,
pendingDraft,
draftFieldBlocks,
modalEditUnlocked,
state.communicationMethodDetailsById,
state.customMethodCardFieldBlocksById,
state.customMethodCardMetaById,
@@ -449,9 +430,7 @@ export function CommunicationMethodsScreen() {
() => communicationPresetFor(newId),
);
const blocksClone = structuredClone(
modalEditUnlocked &&
draftFieldBlocks !== null &&
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById)
draftFieldBlocks !== null
? draftFieldBlocks
: cloneMethodCardBlocksForDuplicate(
state.customMethodCardFieldBlocksById,
@@ -481,14 +460,13 @@ export function CommunicationMethodsScreen() {
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,
@@ -501,7 +479,7 @@ export function CommunicationMethodsScreen() {
const kebabMenuItems = useMemo(
() =>
buildCustomRuleModalKebabMenu(modalKebabMenu, {
showCustomize: !modalEditUnlocked,
showCustomize: true,
onCustomize: handleCustomize,
onDuplicate:
(state.editingPublishedRuleId?.trim() ?? "") !== "" || !pendingCardId
@@ -521,7 +499,6 @@ export function CommunicationMethodsScreen() {
handleDuplicatePrefabCard,
handleRemoveSelectedFromModal,
isSelectedCardModal,
modalEditUnlocked,
modalKebabMenu,
pendingCardId,
state.customMethodCardMetaById,
@@ -540,7 +517,7 @@ export function CommunicationMethodsScreen() {
meta?.supportText ??
method?.supportText ??
comm.confirmModal.description,
nextButtonText: modalEditUnlocked
nextButtonText: isSelectedCardModal
? saveLabel
: comm.addPlatform.nextButtonText,
};
@@ -552,8 +529,14 @@ export function CommunicationMethodsScreen() {
};
const handleCloseAddWizard = useCallback(() => {
const resumeCardId = wizardCustomizeCardId;
setAddCustomWizardOpen(false);
}, []);
setWizardCustomizeCardId(null);
setWizardInitialValues(null);
if (resumeCardId && pendingCardId === resumeCardId) {
setCreateModalOpen(true);
}
}, [pendingCardId, wizardCustomizeCardId]);
const handleFinalizeCustomCard = useCallback(
({
@@ -566,6 +549,42 @@ export function CommunicationMethodsScreen() {
fieldBlocks: CustomMethodCardFieldBlock[];
}) => {
markCreateFlowInteraction();
const existingId = wizardCustomizeCardId;
if (existingId) {
updateState({
selectedCommunicationMethodIds: moveFacetSelectionIdToFront(
selectedIds,
existingId,
),
customMethodCardMetaById: methodCardMetaWithCustomizeHeader(
state.customMethodCardMetaById,
existingId,
{ title, description },
),
...(pendingDraft
? {
communicationMethodDetailsById: {
...(state.communicationMethodDetailsById ?? {}),
[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({
selectedCommunicationMethodIds: moveFacetSelectionIdToFront(
@@ -588,91 +607,29 @@ export function CommunicationMethodsScreen() {
},
[
markCreateFlowInteraction,
pendingDraft,
selectedIds,
state.communicationMethodDetailsById,
state.customMethodCardFieldBlocksById,
state.customMethodCardMetaById,
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,
communicationMethodDetailsById: {
...(state.communicationMethodDetailsById ?? {}),
[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 ?? []),
@@ -680,22 +637,27 @@ export function CommunicationMethodsScreen() {
});
} else if (pendingDraft) {
updateState({
customMethodCardMetaById: nextMeta,
communicationMethodDetailsById: {
...(state.communicationMethodDetailsById ?? {}),
[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({
@@ -707,15 +669,23 @@ export function CommunicationMethodsScreen() {
...(state.communicationMethodDetailsById ?? {}),
[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,
@@ -766,33 +736,10 @@ export function CommunicationMethodsScreen() {
<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}
@@ -805,14 +752,14 @@ export function CommunicationMethodsScreen() {
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)
}
@@ -821,7 +768,6 @@ export function CommunicationMethodsScreen() {
<CommunicationMethodEditFields
value={pendingDraft}
onChange={handleDraftChange}
readOnly={fieldsLocked}
/>
)
) : null}
@@ -830,6 +776,7 @@ export function CommunicationMethodsScreen() {
<CustomMethodCardWizard
isOpen={addCustomWizardOpen}
onClose={handleCloseAddWizard}
initialValues={wizardInitialValues}
onFinalize={handleFinalizeCustomCard}
onPersistCustomUploadFile={(file) =>
uploadCreateFlowFile(file, "customMethodAttachment")