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}
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { RefObject } from "react";
|
||||
import type { AddCustomFieldType } from "../../../../components/controls/AddCustomField/AddCustomField.types";
|
||||
import type { ModalHeaderMenuItem } from "../../../../components/modals/ModalHeader/ModalHeader.types";
|
||||
import type { CustomMethodCardFieldBlock } from "../../../../../lib/create/customMethodCardFieldBlocks";
|
||||
import type { MethodCardWizardInitialValues } from "../../../../../lib/create/methodCardWizardPrefill";
|
||||
|
||||
export interface CustomMethodCardWizardFieldBodiesCopy {
|
||||
requiredHint: string;
|
||||
@@ -47,6 +48,7 @@ export interface CustomMethodCardWizardCopy {
|
||||
footerFinalize: string;
|
||||
fieldModals: {
|
||||
addField: string;
|
||||
saveField: string;
|
||||
requiredHint: string;
|
||||
text: CustomMethodCardWizardFieldBodiesCopy["text"] & {
|
||||
title: string;
|
||||
@@ -70,6 +72,8 @@ export interface CustomMethodCardWizardCopy {
|
||||
export interface CustomMethodCardWizardProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
/** When set, seeds title, description, and field blocks; wizard still starts at step 1. */
|
||||
initialValues?: MethodCardWizardInitialValues | null;
|
||||
/** Called when the user completes step 3; parent assigns id and persists state. */
|
||||
onFinalize: (payload: {
|
||||
title: string;
|
||||
@@ -123,7 +127,8 @@ export interface CustomMethodCardWizardViewProps {
|
||||
policyDescription: string;
|
||||
addFieldExpanded: boolean;
|
||||
copy: CustomMethodCardWizardCopy;
|
||||
maxChars: number;
|
||||
maxTitleChars: number;
|
||||
maxDescriptionChars: number;
|
||||
onPolicyTitleChange: (v: string) => void;
|
||||
onPolicyDescriptionChange: (v: string) => void;
|
||||
onPressAddCustomField: () => void;
|
||||
@@ -136,6 +141,7 @@ export interface CustomMethodCardWizardViewProps {
|
||||
>;
|
||||
draftFieldBlocks: CustomMethodCardFieldBlock[];
|
||||
onDraftFieldBlocksReorder: (_next: CustomMethodCardFieldBlock[]) => void;
|
||||
onEditFieldBlock: (_block: CustomMethodCardFieldBlock) => void;
|
||||
nextDisabled: boolean;
|
||||
nextLabel: string;
|
||||
showBackButton: boolean;
|
||||
|
||||
@@ -19,7 +19,8 @@ function CustomMethodCardWizardViewComponent({
|
||||
policyDescription,
|
||||
addFieldExpanded,
|
||||
copy,
|
||||
maxChars,
|
||||
maxTitleChars,
|
||||
maxDescriptionChars,
|
||||
onPolicyTitleChange,
|
||||
onPolicyDescriptionChange,
|
||||
onPressAddCustomField,
|
||||
@@ -35,6 +36,7 @@ function CustomMethodCardWizardViewComponent({
|
||||
stepper,
|
||||
draftFieldBlocks,
|
||||
onDraftFieldBlocksReorder,
|
||||
onEditFieldBlock,
|
||||
kebabMoreOptionsAriaLabel,
|
||||
kebabMenuAriaLabel,
|
||||
kebabMenuItems,
|
||||
@@ -71,7 +73,7 @@ function CustomMethodCardWizardViewComponent({
|
||||
placeholder={copy.step1.fieldPlaceholder}
|
||||
value={policyTitle}
|
||||
onChange={onPolicyTitleChange}
|
||||
maxLength={maxChars}
|
||||
maxLength={maxTitleChars}
|
||||
/>
|
||||
) : null}
|
||||
{!fieldTypeModal && wizardStep === 2 ? (
|
||||
@@ -80,9 +82,9 @@ function CustomMethodCardWizardViewComponent({
|
||||
formHeader={false}
|
||||
placeholder={copy.step2.fieldPlaceholder}
|
||||
value={policyDescription}
|
||||
maxLength={maxChars}
|
||||
maxLength={maxDescriptionChars}
|
||||
onChange={(e) => onPolicyDescriptionChange(e.target.value)}
|
||||
textHint={`${policyDescription.length}/${maxChars}`}
|
||||
textHint={`${policyDescription.length}/${maxDescriptionChars}`}
|
||||
className="w-full"
|
||||
rows={4}
|
||||
/>
|
||||
@@ -96,6 +98,7 @@ function CustomMethodCardWizardViewComponent({
|
||||
dragHandleAriaLabel={copy.step3BlocksList.dragHandleAriaLabel}
|
||||
listLabel={copy.step3BlocksList.listLabel}
|
||||
onBlocksReorder={onDraftFieldBlocksReorder}
|
||||
onEditBlock={onEditFieldBlock}
|
||||
/>
|
||||
) : null}
|
||||
<AddCustomField
|
||||
|
||||
+22
-3
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { memo, useCallback, useState, type DragEvent } from "react";
|
||||
import { memo, useCallback, useRef, useState, type DragEvent } from "react";
|
||||
import { reorderCustomMethodCardFieldBlocks } from "../../../../../lib/create/reorderCustomMethodCardFieldBlocks";
|
||||
import { CustomMethodCardWizardBlocksListView } from "./CustomMethodCardWizardBlocksList.view";
|
||||
import type { CustomMethodCardWizardBlocksListProps } from "./CustomMethodCardWizardBlocksList.types";
|
||||
@@ -11,19 +11,33 @@ function CustomMethodCardWizardBlocksListContainerComponent({
|
||||
dragHandleAriaLabel,
|
||||
listLabel,
|
||||
onBlocksReorder,
|
||||
onEditBlock,
|
||||
}: CustomMethodCardWizardBlocksListProps) {
|
||||
const [draggingIndex, setDraggingIndex] = useState<number | null>(null);
|
||||
const [overIndex, setOverIndex] = useState<number | null>(null);
|
||||
const draggingIndexRef = useRef<number | null>(null);
|
||||
const dragFromHandleRef = useRef(false);
|
||||
|
||||
const clearDragUi = useCallback(() => {
|
||||
draggingIndexRef.current = null;
|
||||
dragFromHandleRef.current = false;
|
||||
setDraggingIndex(null);
|
||||
setOverIndex(null);
|
||||
}, []);
|
||||
|
||||
const handleHandlePointerDown = useCallback(() => {
|
||||
dragFromHandleRef.current = true;
|
||||
}, []);
|
||||
|
||||
const handleDragStart = useCallback(
|
||||
(index: number) => (e: DragEvent) => {
|
||||
if (!dragFromHandleRef.current) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
e.dataTransfer.effectAllowed = "move";
|
||||
e.dataTransfer.setData("text/plain", String(index));
|
||||
draggingIndexRef.current = index;
|
||||
setDraggingIndex(index);
|
||||
},
|
||||
[],
|
||||
@@ -40,8 +54,11 @@ function CustomMethodCardWizardBlocksListContainerComponent({
|
||||
const handleDrop = useCallback(
|
||||
(index: number) => (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
const from = Number.parseInt(e.dataTransfer.getData("text/plain"), 10);
|
||||
if (Number.isNaN(from)) {
|
||||
const fromData = Number.parseInt(e.dataTransfer.getData("text/plain"), 10);
|
||||
const from = Number.isNaN(fromData)
|
||||
? draggingIndexRef.current
|
||||
: fromData;
|
||||
if (from == null || Number.isNaN(from)) {
|
||||
clearDragUi();
|
||||
return;
|
||||
}
|
||||
@@ -66,6 +83,8 @@ function CustomMethodCardWizardBlocksListContainerComponent({
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleDrop}
|
||||
onDragEnd={clearDragUi}
|
||||
onHandlePointerDown={handleHandlePointerDown}
|
||||
onEditBlock={onEditBlock}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+2
@@ -8,6 +8,7 @@ export interface CustomMethodCardWizardBlocksListProps {
|
||||
dragHandleAriaLabel: string;
|
||||
listLabel: string;
|
||||
onBlocksReorder: (_next: CustomMethodCardFieldBlock[]) => void;
|
||||
onEditBlock: (_block: CustomMethodCardFieldBlock) => void;
|
||||
}
|
||||
|
||||
export interface CustomMethodCardWizardBlocksListViewProps
|
||||
@@ -18,4 +19,5 @@ export interface CustomMethodCardWizardBlocksListViewProps
|
||||
onDragOver: (_index: number) => (_e: DragEvent) => void;
|
||||
onDrop: (_index: number) => (_e: DragEvent) => void;
|
||||
onDragEnd: () => void;
|
||||
onHandlePointerDown: () => void;
|
||||
}
|
||||
|
||||
+18
-7
@@ -38,6 +38,8 @@ function CustomMethodCardWizardBlocksListViewComponent({
|
||||
onDragOver,
|
||||
onDrop,
|
||||
onDragEnd,
|
||||
onHandlePointerDown,
|
||||
onEditBlock,
|
||||
}: CustomMethodCardWizardBlocksListViewProps) {
|
||||
return (
|
||||
<ul
|
||||
@@ -53,6 +55,7 @@ function CustomMethodCardWizardBlocksListViewComponent({
|
||||
return (
|
||||
<li
|
||||
key={block.id}
|
||||
draggable
|
||||
aria-grabbed={isDragging ? true : undefined}
|
||||
className={`group flex min-h-[52px] items-stretch gap-2 rounded-[var(--measures-radius-medium,8px)] border-2 bg-[var(--color-surface-default-secondary)] py-2 pl-1 pr-3 transition-[border-color,box-shadow,opacity] ${
|
||||
isDropTarget
|
||||
@@ -61,19 +64,26 @@ function CustomMethodCardWizardBlocksListViewComponent({
|
||||
? "relative z-20 border-[var(--color-border-default-primary)] opacity-60 shadow-[0_4px_12px_rgba(0,0,0,0.12)]"
|
||||
: "border-[var(--color-border-default-primary)] hover:border-[var(--color-content-default-secondary)]"
|
||||
}`}
|
||||
onDragStart={onDragStart(index)}
|
||||
onDragOver={onDragOver(index)}
|
||||
onDrop={onDrop(index)}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
draggable
|
||||
onDragStart={onDragStart(index)}
|
||||
onDragEnd={onDragEnd}
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={dragHandleAriaLabel}
|
||||
className="flex shrink-0 cursor-grab touch-manipulation items-center self-stretch rounded-[var(--measures-radius-200,8px)] border-0 bg-transparent px-1 text-[var(--color-content-default-secondary)] transition-colors active:cursor-grabbing focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--color-border-invert-primary)] group-hover:text-[var(--color-content-default-primary)]"
|
||||
onPointerDown={onHandlePointerDown}
|
||||
className="flex shrink-0 cursor-grab touch-manipulation items-center self-stretch rounded-[var(--measures-radius-200,8px)] px-1 text-[var(--color-content-default-secondary)] transition-colors active:cursor-grabbing focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--color-border-invert-primary)] group-hover:text-[var(--color-content-default-primary)]"
|
||||
>
|
||||
<DragHandleGlyph />
|
||||
</button>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={title}
|
||||
onClick={() => onEditBlock(block)}
|
||||
className="flex min-w-0 flex-1 items-center gap-2 self-stretch rounded-[var(--measures-radius-200,8px)] border-0 bg-transparent px-0 text-left cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--color-border-invert-primary)]"
|
||||
>
|
||||
<span className="flex h-8 w-8 shrink-0 items-center justify-center self-center">
|
||||
<Icon
|
||||
name={ADD_CUSTOM_FIELD_TYPE_ICONS[kind]}
|
||||
@@ -89,6 +99,7 @@ function CustomMethodCardWizardBlocksListViewComponent({
|
||||
{typeLabel}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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")
|
||||
|
||||
@@ -50,20 +50,20 @@ import type { ConflictManagementDetailEntry } 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 ConflictManagementScreen() {
|
||||
const m = useMessages();
|
||||
const cm = m.create.customRule.conflictManagement;
|
||||
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);
|
||||
@@ -76,12 +76,14 @@ export function ConflictManagementScreen() {
|
||||
const [pendingDraft, setPendingDraft] =
|
||||
useState<ConflictManagementDetailEntry | 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.selectedConflictManagementIds ?? [];
|
||||
|
||||
@@ -104,6 +106,8 @@ export function ConflictManagementScreen() {
|
||||
|
||||
const handleOpenAddWizard = useCallback(() => {
|
||||
markCreateFlowInteraction();
|
||||
setWizardCustomizeCardId(null);
|
||||
setWizardInitialValues(null);
|
||||
setAddCustomWizardOpen(true);
|
||||
}, [markCreateFlowInteraction]);
|
||||
|
||||
@@ -145,15 +149,40 @@ export function ConflictManagementScreen() {
|
||||
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 ?? cm.confirmModal.title,
|
||||
description:
|
||||
meta?.supportText ??
|
||||
method?.supportText ??
|
||||
cm.confirmModal.description,
|
||||
};
|
||||
customizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
|
||||
draft,
|
||||
initialBlocks,
|
||||
headerDraft,
|
||||
);
|
||||
setPendingCardId(id);
|
||||
setPendingDraft(seedDraft(id));
|
||||
setPendingDraft(draft);
|
||||
setDraftFieldBlocks(initialBlocks);
|
||||
setCreateModalOpen(true);
|
||||
},
|
||||
[markCreateFlowInteraction, seedDraft],
|
||||
[
|
||||
cm.confirmModal.description,
|
||||
cm.confirmModal.title,
|
||||
markCreateFlowInteraction,
|
||||
methodById,
|
||||
seedDraft,
|
||||
state.customMethodCardFieldBlocksById,
|
||||
state.customMethodCardMetaById,
|
||||
],
|
||||
);
|
||||
|
||||
const handleDraftChange = useCallback(
|
||||
@@ -166,9 +195,7 @@ export function ConflictManagementScreen() {
|
||||
|
||||
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;
|
||||
@@ -190,7 +217,7 @@ export function ConflictManagementScreen() {
|
||||
methodId: pendingCardId,
|
||||
meta: state.customMethodCardMetaById,
|
||||
fieldBlocksById: state.customMethodCardFieldBlocksById,
|
||||
modalEditUnlocked,
|
||||
modalEditUnlocked: false,
|
||||
draftFieldBlocks,
|
||||
customFacetDetailsMatchPreset,
|
||||
}),
|
||||
@@ -198,7 +225,6 @@ export function ConflictManagementScreen() {
|
||||
[
|
||||
customFacetDetailsMatchPreset,
|
||||
draftFieldBlocks,
|
||||
modalEditUnlocked,
|
||||
pendingCardId,
|
||||
state.customMethodCardFieldBlocksById,
|
||||
state.customMethodCardMetaById,
|
||||
@@ -208,11 +234,11 @@ export function ConflictManagementScreen() {
|
||||
const handleCreateModalClose = useCallback(async () => {
|
||||
if (
|
||||
!(await confirmDiscard(
|
||||
modalEditUnlocked,
|
||||
true,
|
||||
customizeSnapshotRef.current,
|
||||
pendingDraft,
|
||||
draftFieldBlocks,
|
||||
customizeHeaderDraft,
|
||||
customizeSnapshotRef.current?.headerDraft ?? null,
|
||||
))
|
||||
) {
|
||||
return;
|
||||
@@ -240,53 +266,14 @@ export function ConflictManagementScreen() {
|
||||
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;
|
||||
@@ -294,11 +281,11 @@ export function ConflictManagementScreen() {
|
||||
markCreateFlowInteraction();
|
||||
if (
|
||||
!(await confirmDiscard(
|
||||
modalEditUnlocked,
|
||||
true,
|
||||
customizeSnapshotRef.current,
|
||||
pendingDraft,
|
||||
draftFieldBlocks,
|
||||
customizeHeaderDraft,
|
||||
customizeSnapshotRef.current?.headerDraft ?? null,
|
||||
))
|
||||
) {
|
||||
return;
|
||||
@@ -314,11 +301,9 @@ export function ConflictManagementScreen() {
|
||||
await handleCreateModalClose();
|
||||
}, [
|
||||
confirmDiscard,
|
||||
customizeHeaderDraft,
|
||||
draftFieldBlocks,
|
||||
handleCreateModalClose,
|
||||
markCreateFlowInteraction,
|
||||
modalEditUnlocked,
|
||||
pendingDraft,
|
||||
pendingCardId,
|
||||
selectedIds,
|
||||
@@ -328,35 +313,36 @@ export function ConflictManagementScreen() {
|
||||
|
||||
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 ?? cm.confirmModal.title,
|
||||
description:
|
||||
meta?.supportText ??
|
||||
method?.supportText ??
|
||||
cm.confirmModal.description,
|
||||
};
|
||||
setCustomizeHeaderDraft(headerDraft);
|
||||
customizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
|
||||
pendingDraft,
|
||||
initialFieldBlocks,
|
||||
headerDraft,
|
||||
setWizardInitialValues(
|
||||
buildMethodCardWizardInitialValues({
|
||||
cardId: pendingCardId,
|
||||
fallbackTitle: method?.label ?? cm.confirmModal.title,
|
||||
fallbackDescription:
|
||||
method?.supportText ?? cm.confirmModal.description,
|
||||
meta: state.customMethodCardMetaById,
|
||||
persistedBlocks: state.customMethodCardFieldBlocksById,
|
||||
draftFieldBlocks,
|
||||
facetPrefill: pendingDraft
|
||||
? {
|
||||
group: "conflictManagement",
|
||||
draft: pendingDraft,
|
||||
headings: cm.sectionHeadings,
|
||||
}
|
||||
: undefined,
|
||||
}),
|
||||
);
|
||||
setDraftFieldBlocks(initialFieldBlocks);
|
||||
setModalEditUnlocked(true);
|
||||
setWizardCustomizeCardId(pendingCardId);
|
||||
setCreateModalOpen(false);
|
||||
setAddCustomWizardOpen(true);
|
||||
}, [
|
||||
cm.confirmModal.description,
|
||||
cm.confirmModal.title,
|
||||
cm.sectionHeadings,
|
||||
draftFieldBlocks,
|
||||
markCreateFlowInteraction,
|
||||
methodById,
|
||||
pendingCardId,
|
||||
@@ -381,9 +367,7 @@ export function ConflictManagementScreen() {
|
||||
() => conflictManagementPresetFor(newId),
|
||||
);
|
||||
const blocksClone = structuredClone(
|
||||
modalEditUnlocked &&
|
||||
draftFieldBlocks !== null &&
|
||||
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById)
|
||||
draftFieldBlocks !== null
|
||||
? draftFieldBlocks
|
||||
: cloneMethodCardBlocksForDuplicate(
|
||||
state.customMethodCardFieldBlocksById,
|
||||
@@ -413,13 +397,12 @@ export function ConflictManagementScreen() {
|
||||
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,
|
||||
@@ -448,9 +431,7 @@ export function ConflictManagementScreen() {
|
||||
() => conflictManagementPresetFor(newId),
|
||||
);
|
||||
const blocksClone = structuredClone(
|
||||
modalEditUnlocked &&
|
||||
draftFieldBlocks !== null &&
|
||||
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById)
|
||||
draftFieldBlocks !== null
|
||||
? draftFieldBlocks
|
||||
: cloneMethodCardBlocksForDuplicate(
|
||||
state.customMethodCardFieldBlocksById,
|
||||
@@ -480,14 +461,13 @@ export function ConflictManagementScreen() {
|
||||
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,
|
||||
@@ -500,7 +480,7 @@ export function ConflictManagementScreen() {
|
||||
const kebabMenuItems = useMemo(
|
||||
() =>
|
||||
buildCustomRuleModalKebabMenu(modalKebabMenu, {
|
||||
showCustomize: !modalEditUnlocked,
|
||||
showCustomize: true,
|
||||
onCustomize: handleCustomize,
|
||||
onDuplicate:
|
||||
(state.editingPublishedRuleId?.trim() ?? "") !== "" || !pendingCardId
|
||||
@@ -520,7 +500,6 @@ export function ConflictManagementScreen() {
|
||||
handleDuplicatePrefabCard,
|
||||
handleRemoveSelectedFromModal,
|
||||
isSelectedCardModal,
|
||||
modalEditUnlocked,
|
||||
modalKebabMenu,
|
||||
pendingCardId,
|
||||
state.customMethodCardMetaById,
|
||||
@@ -539,7 +518,7 @@ export function ConflictManagementScreen() {
|
||||
meta?.supportText ??
|
||||
method?.supportText ??
|
||||
cm.confirmModal.description,
|
||||
nextButtonText: modalEditUnlocked
|
||||
nextButtonText: isSelectedCardModal
|
||||
? saveLabel
|
||||
: cm.addApproach.nextButtonText,
|
||||
};
|
||||
@@ -551,8 +530,14 @@ export function ConflictManagementScreen() {
|
||||
};
|
||||
|
||||
const handleCloseAddWizard = useCallback(() => {
|
||||
const resumeCardId = wizardCustomizeCardId;
|
||||
setAddCustomWizardOpen(false);
|
||||
}, []);
|
||||
setWizardCustomizeCardId(null);
|
||||
setWizardInitialValues(null);
|
||||
if (resumeCardId && pendingCardId === resumeCardId) {
|
||||
setCreateModalOpen(true);
|
||||
}
|
||||
}, [pendingCardId, wizardCustomizeCardId]);
|
||||
|
||||
const handleFinalizeCustomCard = useCallback(
|
||||
({
|
||||
@@ -565,6 +550,42 @@ export function ConflictManagementScreen() {
|
||||
fieldBlocks: CustomMethodCardFieldBlock[];
|
||||
}) => {
|
||||
markCreateFlowInteraction();
|
||||
const existingId = wizardCustomizeCardId;
|
||||
if (existingId) {
|
||||
updateState({
|
||||
selectedConflictManagementIds: moveFacetSelectionIdToFront(
|
||||
selectedIds,
|
||||
existingId,
|
||||
),
|
||||
customMethodCardMetaById: methodCardMetaWithCustomizeHeader(
|
||||
state.customMethodCardMetaById,
|
||||
existingId,
|
||||
{ title, description },
|
||||
),
|
||||
...(pendingDraft
|
||||
? {
|
||||
conflictManagementDetailsById: {
|
||||
...(state.conflictManagementDetailsById ?? {}),
|
||||
[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({
|
||||
selectedConflictManagementIds: moveFacetSelectionIdToFront(
|
||||
@@ -587,91 +608,29 @@ export function ConflictManagementScreen() {
|
||||
},
|
||||
[
|
||||
markCreateFlowInteraction,
|
||||
pendingDraft,
|
||||
selectedIds,
|
||||
state.conflictManagementDetailsById,
|
||||
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,
|
||||
conflictManagementDetailsById: {
|
||||
...(state.conflictManagementDetailsById ?? {}),
|
||||
[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 ?? []),
|
||||
@@ -679,22 +638,27 @@ export function ConflictManagementScreen() {
|
||||
});
|
||||
} else if (pendingDraft) {
|
||||
updateState({
|
||||
customMethodCardMetaById: nextMeta,
|
||||
conflictManagementDetailsById: {
|
||||
...(state.conflictManagementDetailsById ?? {}),
|
||||
[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({
|
||||
@@ -706,15 +670,23 @@ export function ConflictManagementScreen() {
|
||||
...(state.conflictManagementDetailsById ?? {}),
|
||||
[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,
|
||||
@@ -765,33 +737,10 @@ export function ConflictManagementScreen() {
|
||||
<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}
|
||||
@@ -804,14 +753,14 @@ export function ConflictManagementScreen() {
|
||||
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)
|
||||
}
|
||||
@@ -820,7 +769,6 @@ export function ConflictManagementScreen() {
|
||||
<ConflictManagementEditFields
|
||||
value={pendingDraft}
|
||||
onChange={handleDraftChange}
|
||||
readOnly={fieldsLocked}
|
||||
/>
|
||||
)
|
||||
) : null}
|
||||
@@ -829,6 +777,7 @@ export function ConflictManagementScreen() {
|
||||
<CustomMethodCardWizard
|
||||
isOpen={addCustomWizardOpen}
|
||||
onClose={handleCloseAddWizard}
|
||||
initialValues={wizardInitialValues}
|
||||
onFinalize={handleFinalizeCustomCard}
|
||||
onPersistCustomUploadFile={(file) =>
|
||||
uploadCreateFlowFile(file, "customMethodAttachment")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -98,10 +98,10 @@ export function FinalReviewScreen({
|
||||
* Two modals coexist on this screen:
|
||||
*
|
||||
* - {@link FinalReviewChipEditModal} — core values + method chips: kebab
|
||||
* Customize / Remove; values also offer Duplicate under the five-chip cap.
|
||||
* Save respects the same unlock/dirty rules as the facet create modals;
|
||||
* writes `{group}DetailsById`, snapshot label (values), `customMethodCardMetaById`,
|
||||
* and field blocks on Save.
|
||||
* Remove; values also offer Duplicate under the five-chip cap; method chips
|
||||
* offer Customize (prefilled custom-policy wizard). Fields are editable on
|
||||
* open. Save writes `{group}DetailsById` and field blocks; wizard Finalize
|
||||
* also writes `customMethodCardMetaById`.
|
||||
* - {@link TemplateChipDetailModal} — read-only fallback for chips we
|
||||
* can't map to an override key (e.g. template body entries on the
|
||||
* "Use without changes" path where no preset matches the title).
|
||||
|
||||
@@ -51,20 +51,20 @@ import type { DecisionApproachDetailEntry } 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 DecisionApproachesScreen() {
|
||||
const m = useMessages();
|
||||
const da = m.create.customRule.decisionApproaches;
|
||||
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);
|
||||
@@ -80,12 +80,14 @@ export function DecisionApproachesScreen() {
|
||||
const [pendingDraft, setPendingDraft] =
|
||||
useState<DecisionApproachDetailEntry | 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.selectedDecisionApproachIds ?? [];
|
||||
|
||||
@@ -117,6 +119,8 @@ export function DecisionApproachesScreen() {
|
||||
|
||||
const handleOpenAddWizard = useCallback(() => {
|
||||
markCreateFlowInteraction();
|
||||
setWizardCustomizeCardId(null);
|
||||
setWizardInitialValues(null);
|
||||
setAddCustomWizardOpen(true);
|
||||
}, [markCreateFlowInteraction]);
|
||||
|
||||
@@ -158,15 +162,40 @@ export function DecisionApproachesScreen() {
|
||||
const handleCardSelect = 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 ?? da.confirmModal.title,
|
||||
description:
|
||||
meta?.supportText ??
|
||||
method?.supportText ??
|
||||
da.confirmModal.description,
|
||||
};
|
||||
customizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
|
||||
draft,
|
||||
initialBlocks,
|
||||
headerDraft,
|
||||
);
|
||||
setPendingCardId(id);
|
||||
setPendingDraft(seedDraft(id));
|
||||
setPendingDraft(draft);
|
||||
setDraftFieldBlocks(initialBlocks);
|
||||
setCreateModalOpen(true);
|
||||
},
|
||||
[markCreateFlowInteraction, seedDraft],
|
||||
[
|
||||
da.confirmModal.description,
|
||||
da.confirmModal.title,
|
||||
markCreateFlowInteraction,
|
||||
methodById,
|
||||
seedDraft,
|
||||
state.customMethodCardFieldBlocksById,
|
||||
state.customMethodCardMetaById,
|
||||
],
|
||||
);
|
||||
|
||||
const handleDraftChange = useCallback(
|
||||
@@ -179,9 +208,7 @@ export function DecisionApproachesScreen() {
|
||||
|
||||
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;
|
||||
@@ -203,7 +230,7 @@ export function DecisionApproachesScreen() {
|
||||
methodId: pendingCardId,
|
||||
meta: state.customMethodCardMetaById,
|
||||
fieldBlocksById: state.customMethodCardFieldBlocksById,
|
||||
modalEditUnlocked,
|
||||
modalEditUnlocked: false,
|
||||
draftFieldBlocks,
|
||||
customFacetDetailsMatchPreset,
|
||||
}),
|
||||
@@ -211,7 +238,6 @@ export function DecisionApproachesScreen() {
|
||||
[
|
||||
customFacetDetailsMatchPreset,
|
||||
draftFieldBlocks,
|
||||
modalEditUnlocked,
|
||||
pendingCardId,
|
||||
state.customMethodCardFieldBlocksById,
|
||||
state.customMethodCardMetaById,
|
||||
@@ -221,11 +247,11 @@ export function DecisionApproachesScreen() {
|
||||
const handleCreateModalClose = useCallback(async () => {
|
||||
if (
|
||||
!(await confirmDiscard(
|
||||
modalEditUnlocked,
|
||||
true,
|
||||
customizeSnapshotRef.current,
|
||||
pendingDraft,
|
||||
draftFieldBlocks,
|
||||
customizeHeaderDraft,
|
||||
customizeSnapshotRef.current?.headerDraft ?? null,
|
||||
))
|
||||
) {
|
||||
return;
|
||||
@@ -253,53 +279,14 @@ export function DecisionApproachesScreen() {
|
||||
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;
|
||||
@@ -307,11 +294,11 @@ export function DecisionApproachesScreen() {
|
||||
markCreateFlowInteraction();
|
||||
if (
|
||||
!(await confirmDiscard(
|
||||
modalEditUnlocked,
|
||||
true,
|
||||
customizeSnapshotRef.current,
|
||||
pendingDraft,
|
||||
draftFieldBlocks,
|
||||
customizeHeaderDraft,
|
||||
customizeSnapshotRef.current?.headerDraft ?? null,
|
||||
))
|
||||
) {
|
||||
return;
|
||||
@@ -327,11 +314,9 @@ export function DecisionApproachesScreen() {
|
||||
await handleCreateModalClose();
|
||||
}, [
|
||||
confirmDiscard,
|
||||
customizeHeaderDraft,
|
||||
draftFieldBlocks,
|
||||
handleCreateModalClose,
|
||||
markCreateFlowInteraction,
|
||||
modalEditUnlocked,
|
||||
pendingDraft,
|
||||
pendingCardId,
|
||||
selectedIds,
|
||||
@@ -341,35 +326,36 @@ export function DecisionApproachesScreen() {
|
||||
|
||||
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 ?? da.confirmModal.title,
|
||||
description:
|
||||
meta?.supportText ??
|
||||
method?.supportText ??
|
||||
da.confirmModal.description,
|
||||
};
|
||||
setCustomizeHeaderDraft(headerDraft);
|
||||
customizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
|
||||
pendingDraft,
|
||||
initialFieldBlocks,
|
||||
headerDraft,
|
||||
setWizardInitialValues(
|
||||
buildMethodCardWizardInitialValues({
|
||||
cardId: pendingCardId,
|
||||
fallbackTitle: method?.label ?? da.confirmModal.title,
|
||||
fallbackDescription:
|
||||
method?.supportText ?? da.confirmModal.description,
|
||||
meta: state.customMethodCardMetaById,
|
||||
persistedBlocks: state.customMethodCardFieldBlocksById,
|
||||
draftFieldBlocks,
|
||||
facetPrefill: pendingDraft
|
||||
? {
|
||||
group: "decisionApproaches",
|
||||
draft: pendingDraft,
|
||||
headings: da.sectionHeadings,
|
||||
}
|
||||
: undefined,
|
||||
}),
|
||||
);
|
||||
setDraftFieldBlocks(initialFieldBlocks);
|
||||
setModalEditUnlocked(true);
|
||||
setWizardCustomizeCardId(pendingCardId);
|
||||
setCreateModalOpen(false);
|
||||
setAddCustomWizardOpen(true);
|
||||
}, [
|
||||
da.confirmModal.description,
|
||||
da.confirmModal.title,
|
||||
da.sectionHeadings,
|
||||
draftFieldBlocks,
|
||||
markCreateFlowInteraction,
|
||||
methodById,
|
||||
pendingCardId,
|
||||
@@ -394,9 +380,7 @@ export function DecisionApproachesScreen() {
|
||||
() => decisionApproachPresetFor(newId),
|
||||
);
|
||||
const blocksClone = structuredClone(
|
||||
modalEditUnlocked &&
|
||||
draftFieldBlocks !== null &&
|
||||
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById)
|
||||
draftFieldBlocks !== null
|
||||
? draftFieldBlocks
|
||||
: cloneMethodCardBlocksForDuplicate(
|
||||
state.customMethodCardFieldBlocksById,
|
||||
@@ -426,13 +410,12 @@ export function DecisionApproachesScreen() {
|
||||
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,
|
||||
@@ -461,9 +444,7 @@ export function DecisionApproachesScreen() {
|
||||
() => decisionApproachPresetFor(newId),
|
||||
);
|
||||
const blocksClone = structuredClone(
|
||||
modalEditUnlocked &&
|
||||
draftFieldBlocks !== null &&
|
||||
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById)
|
||||
draftFieldBlocks !== null
|
||||
? draftFieldBlocks
|
||||
: cloneMethodCardBlocksForDuplicate(
|
||||
state.customMethodCardFieldBlocksById,
|
||||
@@ -493,14 +474,13 @@ export function DecisionApproachesScreen() {
|
||||
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,
|
||||
@@ -513,7 +493,7 @@ export function DecisionApproachesScreen() {
|
||||
const kebabMenuItems = useMemo(
|
||||
() =>
|
||||
buildCustomRuleModalKebabMenu(modalKebabMenu, {
|
||||
showCustomize: !modalEditUnlocked,
|
||||
showCustomize: true,
|
||||
onCustomize: handleCustomize,
|
||||
onDuplicate:
|
||||
(state.editingPublishedRuleId?.trim() ?? "") !== "" || !pendingCardId
|
||||
@@ -533,7 +513,6 @@ export function DecisionApproachesScreen() {
|
||||
handleDuplicatePrefabCard,
|
||||
handleRemoveSelectedFromModal,
|
||||
isSelectedCardModal,
|
||||
modalEditUnlocked,
|
||||
modalKebabMenu,
|
||||
pendingCardId,
|
||||
state.customMethodCardMetaById,
|
||||
@@ -547,8 +526,14 @@ export function DecisionApproachesScreen() {
|
||||
}, [markCreateFlowInteraction]);
|
||||
|
||||
const handleCloseAddWizard = useCallback(() => {
|
||||
const resumeCardId = wizardCustomizeCardId;
|
||||
setAddCustomWizardOpen(false);
|
||||
}, []);
|
||||
setWizardCustomizeCardId(null);
|
||||
setWizardInitialValues(null);
|
||||
if (resumeCardId && pendingCardId === resumeCardId) {
|
||||
setCreateModalOpen(true);
|
||||
}
|
||||
}, [pendingCardId, wizardCustomizeCardId]);
|
||||
|
||||
const handleFinalizeCustomCard = useCallback(
|
||||
({
|
||||
@@ -561,6 +546,42 @@ export function DecisionApproachesScreen() {
|
||||
fieldBlocks: CustomMethodCardFieldBlock[];
|
||||
}) => {
|
||||
markCreateFlowInteraction();
|
||||
const existingId = wizardCustomizeCardId;
|
||||
if (existingId) {
|
||||
updateState({
|
||||
selectedDecisionApproachIds: moveFacetSelectionIdToFront(
|
||||
selectedIds,
|
||||
existingId,
|
||||
),
|
||||
customMethodCardMetaById: methodCardMetaWithCustomizeHeader(
|
||||
state.customMethodCardMetaById,
|
||||
existingId,
|
||||
{ title, description },
|
||||
),
|
||||
...(pendingDraft
|
||||
? {
|
||||
decisionApproachDetailsById: {
|
||||
...(state.decisionApproachDetailsById ?? {}),
|
||||
[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({
|
||||
selectedDecisionApproachIds: moveFacetSelectionIdToFront(
|
||||
@@ -583,91 +604,29 @@ export function DecisionApproachesScreen() {
|
||||
},
|
||||
[
|
||||
markCreateFlowInteraction,
|
||||
pendingDraft,
|
||||
selectedIds,
|
||||
state.customMethodCardFieldBlocksById,
|
||||
state.customMethodCardMetaById,
|
||||
state.decisionApproachDetailsById,
|
||||
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,
|
||||
decisionApproachDetailsById: {
|
||||
...(state.decisionApproachDetailsById ?? {}),
|
||||
[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 ?? []),
|
||||
@@ -675,22 +634,27 @@ export function DecisionApproachesScreen() {
|
||||
});
|
||||
} else if (pendingDraft) {
|
||||
updateState({
|
||||
customMethodCardMetaById: nextMeta,
|
||||
decisionApproachDetailsById: {
|
||||
...(state.decisionApproachDetailsById ?? {}),
|
||||
[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({
|
||||
@@ -702,15 +666,23 @@ export function DecisionApproachesScreen() {
|
||||
...(state.decisionApproachDetailsById ?? {}),
|
||||
[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,
|
||||
@@ -729,7 +701,7 @@ export function DecisionApproachesScreen() {
|
||||
meta?.supportText ??
|
||||
method?.supportText ??
|
||||
da.confirmModal.description,
|
||||
nextButtonText: modalEditUnlocked
|
||||
nextButtonText: isSelectedCardModal
|
||||
? saveLabel
|
||||
: da.addApproach.nextButtonText,
|
||||
};
|
||||
@@ -802,33 +774,10 @@ export function DecisionApproachesScreen() {
|
||||
<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}
|
||||
@@ -841,14 +790,14 @@ export function DecisionApproachesScreen() {
|
||||
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)
|
||||
}
|
||||
@@ -857,7 +806,6 @@ export function DecisionApproachesScreen() {
|
||||
<DecisionApproachEditFields
|
||||
value={pendingDraft}
|
||||
onChange={handleDraftChange}
|
||||
readOnly={fieldsLocked}
|
||||
/>
|
||||
)
|
||||
) : null}
|
||||
@@ -866,6 +814,7 @@ export function DecisionApproachesScreen() {
|
||||
<CustomMethodCardWizard
|
||||
isOpen={addCustomWizardOpen}
|
||||
onClose={handleCloseAddWizard}
|
||||
initialValues={wizardInitialValues}
|
||||
onFinalize={handleFinalizeCustomCard}
|
||||
onPersistCustomUploadFile={(file) =>
|
||||
uploadCreateFlowFile(file, "customMethodAttachment")
|
||||
|
||||
@@ -16,13 +16,23 @@ import type {
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
|
||||
import { CoreValueEditFields } from "../../components/methodEditFields";
|
||||
import CustomMethodCardModalBody from "../../components/CustomMethodCardModalBody";
|
||||
import CustomMethodCardWizard from "../../components/CustomMethodCardWizard";
|
||||
import { usesWizardFieldBlocksModalBody } from "../../../../../lib/create/usesWizardFieldBlocksModalBody";
|
||||
import { buildCustomRuleModalKebabMenu } from "../../components/customRuleModalKebabMenu";
|
||||
import {
|
||||
duplicateCoreValueChipInDraft,
|
||||
MAX_SELECTED_CORE_VALUES,
|
||||
removeCoreValueChipFromDraft,
|
||||
} from "../../../../../lib/create/coreValueChipFacet";
|
||||
import { omitIdFromStringRecord } from "../../../../../lib/create/duplicateMethodCardModalDraft";
|
||||
import {
|
||||
buildMethodCardWizardInitialValues,
|
||||
coreValueDetailsFromWizardFieldBlocks,
|
||||
overlayFacetPrefillValues,
|
||||
type MethodCardWizardInitialValues,
|
||||
} from "../../../../../lib/create/methodCardWizardPrefill";
|
||||
import type { CustomMethodCardFieldBlock } from "../../../../../lib/create/customMethodCardFieldBlocks";
|
||||
import { uploadCreateFlowFile } from "../../../../../lib/create/uploadToServer";
|
||||
|
||||
const MAX_CORE_VALUES = MAX_SELECTED_CORE_VALUES;
|
||||
|
||||
@@ -113,6 +123,15 @@ export function CoreValuesSelectScreen() {
|
||||
);
|
||||
const [modalSession, setModalSession] = useState<ModalSession | null>(null);
|
||||
const [draft, setDraft] = useState<CoreValueDetailEntry>(EMPTY_DETAIL);
|
||||
const [addCustomWizardOpen, setAddCustomWizardOpen] = useState(false);
|
||||
const [wizardCustomizeChipId, setWizardCustomizeChipId] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [wizardInitialValues, setWizardInitialValues] =
|
||||
useState<MethodCardWizardInitialValues | null>(null);
|
||||
const [draftFieldBlocks, setDraftFieldBlocks] = useState<
|
||||
CustomMethodCardFieldBlock[] | null
|
||||
>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setCoreValueOptions(
|
||||
@@ -165,6 +184,7 @@ export function CoreValuesSelectScreen() {
|
||||
return {
|
||||
meaning: saved?.meaning ?? preset.meaning,
|
||||
signals: saved?.signals ?? preset.signals,
|
||||
...(saved?.supportText ? { supportText: saved.supportText } : {}),
|
||||
};
|
||||
},
|
||||
[state.coreValueDetailsByChipId, getPresetTexts],
|
||||
@@ -179,12 +199,22 @@ export function CoreValuesSelectScreen() {
|
||||
) => {
|
||||
const initial = seedDetail ?? getInitialTexts(chipId, valueLabel);
|
||||
initialDraftRef.current = { ...initial };
|
||||
const persisted = state.customMethodCardFieldBlocksById?.[chipId];
|
||||
setDraftFieldBlocks(
|
||||
Array.isArray(persisted) && persisted.length > 0
|
||||
? structuredClone(persisted)
|
||||
: null,
|
||||
);
|
||||
setDraft(initial);
|
||||
setActiveModalChipId(chipId);
|
||||
setModalSession(session);
|
||||
markCreateFlowInteraction();
|
||||
},
|
||||
[getInitialTexts, markCreateFlowInteraction],
|
||||
[
|
||||
getInitialTexts,
|
||||
markCreateFlowInteraction,
|
||||
state.customMethodCardFieldBlocksById,
|
||||
],
|
||||
);
|
||||
|
||||
const handleDraftChange = useCallback(
|
||||
@@ -200,19 +230,19 @@ export function CoreValuesSelectScreen() {
|
||||
initialDraftRef.current = null;
|
||||
setActiveModalChipId(null);
|
||||
setModalSession(null);
|
||||
setDraftFieldBlocks(null);
|
||||
}, []);
|
||||
|
||||
const confirmLeaveWithoutSaving = useCallback(async () => {
|
||||
const isPendingAdd =
|
||||
modalSession === "pending" || modalSession === "customPending";
|
||||
const initial = initialDraftRef.current;
|
||||
const editingDirty =
|
||||
modalSession === "editing" &&
|
||||
const fieldsDirty =
|
||||
initial != null &&
|
||||
(draft.meaning !== initial.meaning || draft.signals !== initial.signals);
|
||||
if (!isPendingAdd && !editingDirty) {
|
||||
if (!fieldsDirty) {
|
||||
return true;
|
||||
}
|
||||
const isPendingAdd =
|
||||
modalSession === "pending" || modalSession === "customPending";
|
||||
return requestConfirm({
|
||||
title: cv.detailModal.discardTitle,
|
||||
description: isPendingAdd
|
||||
@@ -294,11 +324,7 @@ export function CoreValuesSelectScreen() {
|
||||
markCreateFlowInteraction();
|
||||
replaceState((prev) => ({
|
||||
...prev,
|
||||
selectedCoreValueIds: selectedIdsFromOptions(nextFiltered),
|
||||
coreValuesChipsSnapshot:
|
||||
chipOptionsToSnapshotRows(nextFiltered),
|
||||
coreValueDetailsByChipId:
|
||||
omitIdFromStringRecord(prev.coreValueDetailsByChipId, activeModalChipId),
|
||||
...removeCoreValueChipFromDraft(prev, activeModalChipId),
|
||||
}));
|
||||
setCoreValueOptions(nextFiltered);
|
||||
}
|
||||
@@ -355,48 +381,185 @@ export function CoreValuesSelectScreen() {
|
||||
if (!activeModalChipId || !modalSession) return;
|
||||
markCreateFlowInteraction();
|
||||
pendingEphemeralCoreDuplicateRef.current = null;
|
||||
const existingBlocks =
|
||||
draftFieldBlocks && draftFieldBlocks.length > 0
|
||||
? draftFieldBlocks
|
||||
: state.customMethodCardFieldBlocksById?.[activeModalChipId];
|
||||
updateState({
|
||||
coreValueDetailsByChipId: {
|
||||
...(state.coreValueDetailsByChipId ?? {}),
|
||||
[activeModalChipId]: draft,
|
||||
},
|
||||
...(existingBlocks && existingBlocks.length > 0
|
||||
? {
|
||||
customMethodCardFieldBlocksById: {
|
||||
...(state.customMethodCardFieldBlocksById ?? {}),
|
||||
[activeModalChipId]: overlayFacetPrefillValues(existingBlocks, {
|
||||
group: "coreValues",
|
||||
draft,
|
||||
headings: {
|
||||
meaning: cv.detailModal.meaningLabel,
|
||||
signals: cv.detailModal.signalsLabel,
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
finalizeModalDismiss();
|
||||
}, [
|
||||
activeModalChipId,
|
||||
cv.detailModal.meaningLabel,
|
||||
cv.detailModal.signalsLabel,
|
||||
draft,
|
||||
draftFieldBlocks,
|
||||
finalizeModalDismiss,
|
||||
markCreateFlowInteraction,
|
||||
modalSession,
|
||||
state.coreValueDetailsByChipId,
|
||||
state.customMethodCardFieldBlocksById,
|
||||
updateState,
|
||||
]);
|
||||
|
||||
const modalChipLabel =
|
||||
coreValueOptions.find((o) => o.id === activeModalChipId)?.label ?? "";
|
||||
|
||||
const modalUsesWizardFieldBlocksBody = Boolean(
|
||||
activeModalChipId &&
|
||||
usesWizardFieldBlocksModalBody({
|
||||
methodId: activeModalChipId,
|
||||
meta: {},
|
||||
fieldBlocksById: state.customMethodCardFieldBlocksById,
|
||||
modalEditUnlocked: true,
|
||||
draftFieldBlocks,
|
||||
}),
|
||||
);
|
||||
|
||||
const showFooterPrimary = Boolean(modalSession);
|
||||
|
||||
const handleCustomize = useCallback(() => {
|
||||
if (!activeModalChipId) return;
|
||||
markCreateFlowInteraction();
|
||||
setWizardInitialValues(
|
||||
buildMethodCardWizardInitialValues({
|
||||
cardId: activeModalChipId,
|
||||
fallbackTitle: modalChipLabel,
|
||||
fallbackDescription:
|
||||
draft.supportText?.trim() || cv.detailModal.subtitle,
|
||||
meta: {},
|
||||
persistedBlocks: state.customMethodCardFieldBlocksById,
|
||||
draftFieldBlocks,
|
||||
facetPrefill: {
|
||||
group: "coreValues",
|
||||
draft,
|
||||
headings: {
|
||||
meaning: cv.detailModal.meaningLabel,
|
||||
signals: cv.detailModal.signalsLabel,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
setWizardCustomizeChipId(activeModalChipId);
|
||||
setAddCustomWizardOpen(true);
|
||||
}, [
|
||||
activeModalChipId,
|
||||
cv.detailModal.meaningLabel,
|
||||
cv.detailModal.signalsLabel,
|
||||
cv.detailModal.subtitle,
|
||||
draft,
|
||||
draftFieldBlocks,
|
||||
markCreateFlowInteraction,
|
||||
modalChipLabel,
|
||||
state.customMethodCardFieldBlocksById,
|
||||
]);
|
||||
|
||||
const handleCloseAddWizard = useCallback(() => {
|
||||
setAddCustomWizardOpen(false);
|
||||
setWizardCustomizeChipId(null);
|
||||
setWizardInitialValues(null);
|
||||
}, []);
|
||||
|
||||
const handleFinalizeCustomCard = useCallback(
|
||||
({
|
||||
title,
|
||||
description,
|
||||
fieldBlocks,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
fieldBlocks: CustomMethodCardFieldBlock[];
|
||||
}) => {
|
||||
const chipId = wizardCustomizeChipId ?? activeModalChipId;
|
||||
if (!chipId) return;
|
||||
markCreateFlowInteraction();
|
||||
pendingEphemeralCoreDuplicateRef.current = null;
|
||||
const trimmedTitle = title.trim();
|
||||
const trimmedDescription = description.trim();
|
||||
const nextDetails = {
|
||||
...coreValueDetailsFromWizardFieldBlocks(fieldBlocks, draft),
|
||||
...(trimmedDescription.length > 0
|
||||
? { supportText: trimmedDescription }
|
||||
: {}),
|
||||
};
|
||||
replaceState((prev) => {
|
||||
const snap = [...(prev.coreValuesChipsSnapshot ?? [])];
|
||||
const i = snap.findIndex((r) => r.id === chipId);
|
||||
if (i >= 0 && trimmedTitle.length > 0) {
|
||||
snap[i] = { ...snap[i], label: trimmedTitle };
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
coreValuesChipsSnapshot: snap,
|
||||
coreValueDetailsByChipId: {
|
||||
...(prev.coreValueDetailsByChipId ?? {}),
|
||||
[chipId]: nextDetails,
|
||||
},
|
||||
customMethodCardFieldBlocksById: {
|
||||
...(prev.customMethodCardFieldBlocksById ?? {}),
|
||||
[chipId]: structuredClone(fieldBlocks),
|
||||
},
|
||||
};
|
||||
});
|
||||
setDraft(nextDetails);
|
||||
setDraftFieldBlocks(structuredClone(fieldBlocks));
|
||||
setAddCustomWizardOpen(false);
|
||||
setWizardCustomizeChipId(null);
|
||||
setWizardInitialValues(null);
|
||||
},
|
||||
[
|
||||
activeModalChipId,
|
||||
draft,
|
||||
markCreateFlowInteraction,
|
||||
replaceState,
|
||||
wizardCustomizeChipId,
|
||||
],
|
||||
);
|
||||
|
||||
const kebabMenuItems = useMemo(() => {
|
||||
if (!modalSession || !activeModalChipId) return [];
|
||||
const selectedCount = coreValueOptions.filter(
|
||||
(o) => o.state === "selected",
|
||||
).length;
|
||||
return buildCustomRuleModalKebabMenu(modalKebabMenu, {
|
||||
showCustomize: true,
|
||||
onCustomize: handleCustomize,
|
||||
onDuplicate:
|
||||
modalSession !== "editing" || selectedCount >= MAX_CORE_VALUES
|
||||
(state.editingPublishedRuleId?.trim() ?? "") !== "" ||
|
||||
selectedCount >= MAX_CORE_VALUES
|
||||
? undefined
|
||||
: handleDuplicateCoreChip,
|
||||
showRemove: true,
|
||||
showRemove: modalSession === "editing",
|
||||
onRemove: handleRemoveFromKebab,
|
||||
});
|
||||
}, [
|
||||
activeModalChipId,
|
||||
coreValueOptions,
|
||||
handleCustomize,
|
||||
handleDuplicateCoreChip,
|
||||
handleRemoveFromKebab,
|
||||
modalKebabMenu,
|
||||
modalSession,
|
||||
state.editingPublishedRuleId,
|
||||
]);
|
||||
const handleChipClick = (chipId: string) => {
|
||||
const target = coreValueOptions.find((o) => o.id === chipId);
|
||||
@@ -520,14 +683,16 @@ export function CoreValuesSelectScreen() {
|
||||
|
||||
{detailModal && (
|
||||
<Create
|
||||
isOpen={activeModalChipId !== null}
|
||||
isOpen={activeModalChipId !== null && !addCustomWizardOpen}
|
||||
onClose={handleModalDismiss}
|
||||
backdropVariant="blurredYellow"
|
||||
headerContent={
|
||||
<div className="bg-[var(--color-surface-default-primary)] px-[24px] py-[12px] shrink-0">
|
||||
<ContentLockup
|
||||
title={modalChipLabel}
|
||||
description={detailModal.subtitle}
|
||||
description={
|
||||
draft.supportText?.trim() || detailModal.subtitle
|
||||
}
|
||||
variant="modal"
|
||||
alignment="left"
|
||||
/>
|
||||
@@ -548,13 +713,43 @@ export function CoreValuesSelectScreen() {
|
||||
}
|
||||
ariaLabel={modalChipLabel || "Core value details"}
|
||||
>
|
||||
<CoreValueEditFields
|
||||
value={draft}
|
||||
onChange={handleDraftChange}
|
||||
/>
|
||||
{modalUsesWizardFieldBlocksBody && activeModalChipId ? (
|
||||
<CustomMethodCardModalBody
|
||||
cardId={activeModalChipId}
|
||||
blocksById={state.customMethodCardFieldBlocksById}
|
||||
blocksOverride={
|
||||
draftFieldBlocks !== null ? draftFieldBlocks : undefined
|
||||
}
|
||||
showPolicyContentLockupWhenNoBlocks={false}
|
||||
onFieldBlocksChange={
|
||||
draftFieldBlocks === null
|
||||
? undefined
|
||||
: (next) => {
|
||||
setDraftFieldBlocks(next);
|
||||
setDraft(
|
||||
coreValueDetailsFromWizardFieldBlocks(next, draft),
|
||||
);
|
||||
}
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<CoreValueEditFields
|
||||
value={draft}
|
||||
onChange={handleDraftChange}
|
||||
/>
|
||||
)}
|
||||
</Create>
|
||||
)}
|
||||
</CreateFlowTwoColumnSelectShell>
|
||||
<CustomMethodCardWizard
|
||||
isOpen={addCustomWizardOpen}
|
||||
onClose={handleCloseAddWizard}
|
||||
initialValues={wizardInitialValues}
|
||||
onFinalize={handleFinalizeCustomCard}
|
||||
onPersistCustomUploadFile={(file) =>
|
||||
uploadCreateFlowFile(file, "customMethodAttachment")
|
||||
}
|
||||
/>
|
||||
{confirmDialog}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -59,6 +59,7 @@ export type CommunityStructureChipSnapshotRow = {
|
||||
export type CoreValueDetailEntry = {
|
||||
meaning: string;
|
||||
signals: string;
|
||||
supportText?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,7 +21,13 @@ export function readCoreValueDetailsFromLocalStorage(): Record<
|
||||
if (typeof o.meaning !== "string" || typeof o.signals !== "string") {
|
||||
continue;
|
||||
}
|
||||
out[k] = { meaning: o.meaning, signals: o.signals };
|
||||
out[k] = {
|
||||
meaning: o.meaning,
|
||||
signals: o.signals,
|
||||
...(typeof o.supportText === "string"
|
||||
? { supportText: o.supportText }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
return out;
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user