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,
|
useMessages,
|
||||||
useTranslation,
|
useTranslation,
|
||||||
} from "../../../../contexts/MessagesContext";
|
} from "../../../../contexts/MessagesContext";
|
||||||
|
import { useAsyncConfirm } from "../../../../hooks/useAsyncConfirm";
|
||||||
import type { CustomMethodCardFieldBlock } from "../../../../../lib/create/customMethodCardFieldBlocks";
|
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 { AddCustomFieldType } from "../../../../components/controls/AddCustomField/AddCustomField.types";
|
||||||
import type { ModalHeaderMenuItem } from "../../../../components/modals/ModalHeader/ModalHeader.types";
|
import type { ModalHeaderMenuItem } from "../../../../components/modals/ModalHeader/ModalHeader.types";
|
||||||
import { CustomMethodCardWizardView } from "./CustomMethodCardWizard.view";
|
import { CustomMethodCardWizardView } from "./CustomMethodCardWizard.view";
|
||||||
@@ -17,12 +21,13 @@ import type { CustomMethodCardWizardProps } from "./CustomMethodCardWizard.types
|
|||||||
* `20066:14748`, `20094:48551`, `20066:14361`).
|
* `20066:14748`, `20094:48551`, `20066:14361`).
|
||||||
*/
|
*/
|
||||||
const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
||||||
({ isOpen, onClose, onFinalize, onPersistCustomUploadFile }) => {
|
({ isOpen, onClose, onFinalize, onPersistCustomUploadFile, initialValues }) => {
|
||||||
const m = useMessages();
|
const m = useMessages();
|
||||||
const t = useTranslation("common");
|
const t = useTranslation("common");
|
||||||
const tUpload = useTranslation("create.upload");
|
const tUpload = useTranslation("create.upload");
|
||||||
const w = m.create.customRule.customMethodCardWizard;
|
const w = m.create.customRule.customMethodCardWizard;
|
||||||
const menuCopy = m.create.customRule.modalKebabMenu;
|
const menuCopy = m.create.customRule.modalKebabMenu;
|
||||||
|
const { requestConfirm, confirmDialog } = useAsyncConfirm();
|
||||||
|
|
||||||
const copy = useMemo(
|
const copy = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -68,6 +73,7 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
|||||||
const [draftFieldBlocks, setDraftFieldBlocks] = useState<
|
const [draftFieldBlocks, setDraftFieldBlocks] = useState<
|
||||||
CustomMethodCardFieldBlock[]
|
CustomMethodCardFieldBlock[]
|
||||||
>([]);
|
>([]);
|
||||||
|
const [editingBlockId, setEditingBlockId] = useState<string | null>(null);
|
||||||
|
|
||||||
const [textBlockTitle, setTextBlockTitle] = useState("");
|
const [textBlockTitle, setTextBlockTitle] = useState("");
|
||||||
const [textPlaceholderBody, setTextPlaceholderBody] = useState("");
|
const [textPlaceholderBody, setTextPlaceholderBody] = useState("");
|
||||||
@@ -88,6 +94,14 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
|||||||
const [proportionDefault, setProportionDefault] = useState(50);
|
const [proportionDefault, setProportionDefault] = useState(50);
|
||||||
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
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(() => {
|
const resetFieldTypeDrafts = useCallback(() => {
|
||||||
setTextBlockTitle("");
|
setTextBlockTitle("");
|
||||||
@@ -112,21 +126,112 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
|||||||
setPolicyDescription("");
|
setPolicyDescription("");
|
||||||
setAddFieldExpanded(false);
|
setAddFieldExpanded(false);
|
||||||
setFieldTypeModal(null);
|
setFieldTypeModal(null);
|
||||||
|
setEditingBlockId(null);
|
||||||
setDraftFieldBlocks([]);
|
setDraftFieldBlocks([]);
|
||||||
|
openSnapshotRef.current = null;
|
||||||
|
fieldModalSnapshotRef.current = null;
|
||||||
resetFieldTypeDrafts();
|
resetFieldTypeDrafts();
|
||||||
}, [resetFieldTypeDrafts]);
|
}, [resetFieldTypeDrafts]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
reset();
|
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(() => {
|
const dismiss = useCallback(() => {
|
||||||
reset();
|
reset();
|
||||||
onClose();
|
onClose();
|
||||||
}, [onClose, reset]);
|
}, [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 titleTrim = policyTitle.trim();
|
||||||
const descriptionTrim = policyDescription.trim();
|
const descriptionTrim = policyDescription.trim();
|
||||||
|
|
||||||
@@ -136,7 +241,7 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
|||||||
titleTrim.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS;
|
titleTrim.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS;
|
||||||
const descriptionOk =
|
const descriptionOk =
|
||||||
descriptionTrim.length > 0 &&
|
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 === 1) return titleOk;
|
||||||
if (wizardStep === 2) return descriptionOk;
|
if (wizardStep === 2) return descriptionOk;
|
||||||
return titleOk && descriptionOk;
|
return titleOk && descriptionOk;
|
||||||
@@ -213,7 +318,9 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
|||||||
const shellDescription = fieldModalHeader?.description ?? headerDescription;
|
const shellDescription = fieldModalHeader?.description ?? headerDescription;
|
||||||
|
|
||||||
const nextLabel = fieldTypeModal
|
const nextLabel = fieldTypeModal
|
||||||
? copy.fieldModals.addField
|
? editingBlockId
|
||||||
|
? copy.fieldModals.saveField
|
||||||
|
: copy.fieldModals.addField
|
||||||
: wizardStep === 3
|
: wizardStep === 3
|
||||||
? copy.footerFinalize
|
? copy.footerFinalize
|
||||||
: t("buttons.next");
|
: t("buttons.next");
|
||||||
@@ -222,33 +329,124 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
|||||||
? !fieldModalStepValid
|
? !fieldModalStepValid
|
||||||
: !stepValid;
|
: !stepValid;
|
||||||
|
|
||||||
const handleShellClose = useCallback(() => {
|
const handleShellClose = useCallback(async () => {
|
||||||
if (fieldTypeModal) {
|
if (fieldTypeModal) {
|
||||||
|
if (
|
||||||
|
fieldModalSnapshotRef.current != null &&
|
||||||
|
fieldModalDraftSignature() !== fieldModalSnapshotRef.current &&
|
||||||
|
!(await confirmAbandonWizardEdits())
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setFieldTypeModal(null);
|
setFieldTypeModal(null);
|
||||||
|
setEditingBlockId(null);
|
||||||
|
fieldModalSnapshotRef.current = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!(await confirmAbandonWizardEdits())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dismiss();
|
dismiss();
|
||||||
}, [dismiss, fieldTypeModal]);
|
}, [
|
||||||
|
confirmAbandonWizardEdits,
|
||||||
|
dismiss,
|
||||||
|
fieldModalDraftSignature,
|
||||||
|
fieldTypeModal,
|
||||||
|
]);
|
||||||
|
|
||||||
const kebabMenuItems = useMemo<ModalHeaderMenuItem[]>(() => [], []);
|
const kebabMenuItems = useMemo<ModalHeaderMenuItem[]>(() => [], []);
|
||||||
|
|
||||||
const handleBack = useCallback(() => {
|
const handleBack = useCallback(async () => {
|
||||||
if (fieldTypeModal) {
|
if (fieldTypeModal) {
|
||||||
|
if (
|
||||||
|
fieldModalSnapshotRef.current != null &&
|
||||||
|
fieldModalDraftSignature() !== fieldModalSnapshotRef.current &&
|
||||||
|
!(await confirmAbandonWizardEdits())
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setFieldTypeModal(null);
|
setFieldTypeModal(null);
|
||||||
|
setEditingBlockId(null);
|
||||||
|
fieldModalSnapshotRef.current = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (wizardStep === 1) {
|
if (wizardStep === 1) {
|
||||||
|
if (!(await confirmAbandonWizardEdits())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
dismiss();
|
dismiss();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setWizardStep((s) => (s === 2 ? 1 : 2));
|
setWizardStep((s) => (s === 2 ? 1 : 2));
|
||||||
}, [dismiss, fieldTypeModal, wizardStep]);
|
}, [
|
||||||
|
confirmAbandonWizardEdits,
|
||||||
|
dismiss,
|
||||||
|
fieldModalDraftSignature,
|
||||||
|
fieldTypeModal,
|
||||||
|
wizardStep,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleSelectFieldType = useCallback((ft: AddCustomFieldType) => {
|
const handleSelectFieldType = useCallback((ft: AddCustomFieldType) => {
|
||||||
|
setEditingBlockId(null);
|
||||||
resetFieldTypeDrafts();
|
resetFieldTypeDrafts();
|
||||||
setFieldTypeModal(ft);
|
setFieldTypeModal(ft);
|
||||||
|
fieldModalSnapshotRef.current = JSON.stringify({
|
||||||
|
fieldTypeModal: ft,
|
||||||
|
textBlockTitle: "",
|
||||||
|
textPlaceholderBody: "",
|
||||||
|
badgeBlockTitle: "",
|
||||||
|
badgeOptions: [],
|
||||||
|
uploadBlockTitle: "",
|
||||||
|
uploadFileName: undefined,
|
||||||
|
uploadAssetUrl: undefined,
|
||||||
|
proportionBlockTitle: "",
|
||||||
|
proportionDefault: 50,
|
||||||
|
});
|
||||||
}, [resetFieldTypeDrafts]);
|
}, [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(
|
const handleFileChosen = useCallback(
|
||||||
async (e: React.ChangeEvent<HTMLInputElement>) => {
|
async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
@@ -285,9 +483,9 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
|||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const appendFieldBlock = useCallback(() => {
|
const commitFieldBlock = useCallback(() => {
|
||||||
if (!fieldTypeModal || !fieldModalStepValid) return;
|
if (!fieldTypeModal || !fieldModalStepValid) return;
|
||||||
const id = crypto.randomUUID();
|
const id = editingBlockId ?? crypto.randomUUID();
|
||||||
let block: CustomMethodCardFieldBlock;
|
let block: CustomMethodCardFieldBlock;
|
||||||
switch (fieldTypeModal) {
|
switch (fieldTypeModal) {
|
||||||
case "text":
|
case "text":
|
||||||
@@ -325,11 +523,19 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
|||||||
defaultPercent: proportionDefault,
|
defaultPercent: proportionDefault,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
setDraftFieldBlocks((prev) => [...prev, block]);
|
setDraftFieldBlocks((prev) => {
|
||||||
|
if (editingBlockId) {
|
||||||
|
return prev.map((row) => (row.id === editingBlockId ? block : row));
|
||||||
|
}
|
||||||
|
return [...prev, block];
|
||||||
|
});
|
||||||
setFieldTypeModal(null);
|
setFieldTypeModal(null);
|
||||||
|
setEditingBlockId(null);
|
||||||
|
fieldModalSnapshotRef.current = null;
|
||||||
}, [
|
}, [
|
||||||
badgeBlockTitle,
|
badgeBlockTitle,
|
||||||
badgeOptions,
|
badgeOptions,
|
||||||
|
editingBlockId,
|
||||||
fieldModalStepValid,
|
fieldModalStepValid,
|
||||||
fieldTypeModal,
|
fieldTypeModal,
|
||||||
proportionBlockTitle,
|
proportionBlockTitle,
|
||||||
@@ -343,7 +549,7 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
|||||||
|
|
||||||
const handleNext = useCallback(() => {
|
const handleNext = useCallback(() => {
|
||||||
if (fieldTypeModal) {
|
if (fieldTypeModal) {
|
||||||
appendFieldBlock();
|
commitFieldBlock();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!stepValid) return;
|
if (!stepValid) return;
|
||||||
@@ -358,7 +564,7 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
|||||||
}
|
}
|
||||||
setWizardStep((s) => (s === 1 ? 2 : 3));
|
setWizardStep((s) => (s === 1 ? 2 : 3));
|
||||||
}, [
|
}, [
|
||||||
appendFieldBlock,
|
commitFieldBlock,
|
||||||
descriptionTrim,
|
descriptionTrim,
|
||||||
dismiss,
|
dismiss,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
@@ -370,6 +576,7 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<CustomMethodCardWizardView
|
<CustomMethodCardWizardView
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onDismiss={handleShellClose}
|
onDismiss={handleShellClose}
|
||||||
@@ -380,7 +587,8 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
|||||||
policyDescription={policyDescription}
|
policyDescription={policyDescription}
|
||||||
addFieldExpanded={addFieldExpanded}
|
addFieldExpanded={addFieldExpanded}
|
||||||
copy={copy}
|
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}
|
onPolicyTitleChange={setPolicyTitle}
|
||||||
onPolicyDescriptionChange={setPolicyDescription}
|
onPolicyDescriptionChange={setPolicyDescription}
|
||||||
onPressAddCustomField={() => setAddFieldExpanded(true)}
|
onPressAddCustomField={() => setAddFieldExpanded(true)}
|
||||||
@@ -420,10 +628,13 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
|||||||
stepper={!fieldTypeModal}
|
stepper={!fieldTypeModal}
|
||||||
draftFieldBlocks={draftFieldBlocks}
|
draftFieldBlocks={draftFieldBlocks}
|
||||||
onDraftFieldBlocksReorder={setDraftFieldBlocks}
|
onDraftFieldBlocksReorder={setDraftFieldBlocks}
|
||||||
|
onEditFieldBlock={handleEditFieldBlock}
|
||||||
kebabMoreOptionsAriaLabel={menuCopy.triggerAriaLabel}
|
kebabMoreOptionsAriaLabel={menuCopy.triggerAriaLabel}
|
||||||
kebabMenuAriaLabel={menuCopy.menuAriaLabel}
|
kebabMenuAriaLabel={menuCopy.menuAriaLabel}
|
||||||
kebabMenuItems={kebabMenuItems}
|
kebabMenuItems={kebabMenuItems}
|
||||||
/>
|
/>
|
||||||
|
{confirmDialog}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { RefObject } from "react";
|
|||||||
import type { AddCustomFieldType } from "../../../../components/controls/AddCustomField/AddCustomField.types";
|
import type { AddCustomFieldType } from "../../../../components/controls/AddCustomField/AddCustomField.types";
|
||||||
import type { ModalHeaderMenuItem } from "../../../../components/modals/ModalHeader/ModalHeader.types";
|
import type { ModalHeaderMenuItem } from "../../../../components/modals/ModalHeader/ModalHeader.types";
|
||||||
import type { CustomMethodCardFieldBlock } from "../../../../../lib/create/customMethodCardFieldBlocks";
|
import type { CustomMethodCardFieldBlock } from "../../../../../lib/create/customMethodCardFieldBlocks";
|
||||||
|
import type { MethodCardWizardInitialValues } from "../../../../../lib/create/methodCardWizardPrefill";
|
||||||
|
|
||||||
export interface CustomMethodCardWizardFieldBodiesCopy {
|
export interface CustomMethodCardWizardFieldBodiesCopy {
|
||||||
requiredHint: string;
|
requiredHint: string;
|
||||||
@@ -47,6 +48,7 @@ export interface CustomMethodCardWizardCopy {
|
|||||||
footerFinalize: string;
|
footerFinalize: string;
|
||||||
fieldModals: {
|
fieldModals: {
|
||||||
addField: string;
|
addField: string;
|
||||||
|
saveField: string;
|
||||||
requiredHint: string;
|
requiredHint: string;
|
||||||
text: CustomMethodCardWizardFieldBodiesCopy["text"] & {
|
text: CustomMethodCardWizardFieldBodiesCopy["text"] & {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -70,6 +72,8 @@ export interface CustomMethodCardWizardCopy {
|
|||||||
export interface CustomMethodCardWizardProps {
|
export interface CustomMethodCardWizardProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
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. */
|
/** Called when the user completes step 3; parent assigns id and persists state. */
|
||||||
onFinalize: (payload: {
|
onFinalize: (payload: {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -123,7 +127,8 @@ export interface CustomMethodCardWizardViewProps {
|
|||||||
policyDescription: string;
|
policyDescription: string;
|
||||||
addFieldExpanded: boolean;
|
addFieldExpanded: boolean;
|
||||||
copy: CustomMethodCardWizardCopy;
|
copy: CustomMethodCardWizardCopy;
|
||||||
maxChars: number;
|
maxTitleChars: number;
|
||||||
|
maxDescriptionChars: number;
|
||||||
onPolicyTitleChange: (v: string) => void;
|
onPolicyTitleChange: (v: string) => void;
|
||||||
onPolicyDescriptionChange: (v: string) => void;
|
onPolicyDescriptionChange: (v: string) => void;
|
||||||
onPressAddCustomField: () => void;
|
onPressAddCustomField: () => void;
|
||||||
@@ -136,6 +141,7 @@ export interface CustomMethodCardWizardViewProps {
|
|||||||
>;
|
>;
|
||||||
draftFieldBlocks: CustomMethodCardFieldBlock[];
|
draftFieldBlocks: CustomMethodCardFieldBlock[];
|
||||||
onDraftFieldBlocksReorder: (_next: CustomMethodCardFieldBlock[]) => void;
|
onDraftFieldBlocksReorder: (_next: CustomMethodCardFieldBlock[]) => void;
|
||||||
|
onEditFieldBlock: (_block: CustomMethodCardFieldBlock) => void;
|
||||||
nextDisabled: boolean;
|
nextDisabled: boolean;
|
||||||
nextLabel: string;
|
nextLabel: string;
|
||||||
showBackButton: boolean;
|
showBackButton: boolean;
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ function CustomMethodCardWizardViewComponent({
|
|||||||
policyDescription,
|
policyDescription,
|
||||||
addFieldExpanded,
|
addFieldExpanded,
|
||||||
copy,
|
copy,
|
||||||
maxChars,
|
maxTitleChars,
|
||||||
|
maxDescriptionChars,
|
||||||
onPolicyTitleChange,
|
onPolicyTitleChange,
|
||||||
onPolicyDescriptionChange,
|
onPolicyDescriptionChange,
|
||||||
onPressAddCustomField,
|
onPressAddCustomField,
|
||||||
@@ -35,6 +36,7 @@ function CustomMethodCardWizardViewComponent({
|
|||||||
stepper,
|
stepper,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
onDraftFieldBlocksReorder,
|
onDraftFieldBlocksReorder,
|
||||||
|
onEditFieldBlock,
|
||||||
kebabMoreOptionsAriaLabel,
|
kebabMoreOptionsAriaLabel,
|
||||||
kebabMenuAriaLabel,
|
kebabMenuAriaLabel,
|
||||||
kebabMenuItems,
|
kebabMenuItems,
|
||||||
@@ -71,7 +73,7 @@ function CustomMethodCardWizardViewComponent({
|
|||||||
placeholder={copy.step1.fieldPlaceholder}
|
placeholder={copy.step1.fieldPlaceholder}
|
||||||
value={policyTitle}
|
value={policyTitle}
|
||||||
onChange={onPolicyTitleChange}
|
onChange={onPolicyTitleChange}
|
||||||
maxLength={maxChars}
|
maxLength={maxTitleChars}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{!fieldTypeModal && wizardStep === 2 ? (
|
{!fieldTypeModal && wizardStep === 2 ? (
|
||||||
@@ -80,9 +82,9 @@ function CustomMethodCardWizardViewComponent({
|
|||||||
formHeader={false}
|
formHeader={false}
|
||||||
placeholder={copy.step2.fieldPlaceholder}
|
placeholder={copy.step2.fieldPlaceholder}
|
||||||
value={policyDescription}
|
value={policyDescription}
|
||||||
maxLength={maxChars}
|
maxLength={maxDescriptionChars}
|
||||||
onChange={(e) => onPolicyDescriptionChange(e.target.value)}
|
onChange={(e) => onPolicyDescriptionChange(e.target.value)}
|
||||||
textHint={`${policyDescription.length}/${maxChars}`}
|
textHint={`${policyDescription.length}/${maxDescriptionChars}`}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
rows={4}
|
rows={4}
|
||||||
/>
|
/>
|
||||||
@@ -96,6 +98,7 @@ function CustomMethodCardWizardViewComponent({
|
|||||||
dragHandleAriaLabel={copy.step3BlocksList.dragHandleAriaLabel}
|
dragHandleAriaLabel={copy.step3BlocksList.dragHandleAriaLabel}
|
||||||
listLabel={copy.step3BlocksList.listLabel}
|
listLabel={copy.step3BlocksList.listLabel}
|
||||||
onBlocksReorder={onDraftFieldBlocksReorder}
|
onBlocksReorder={onDraftFieldBlocksReorder}
|
||||||
|
onEditBlock={onEditFieldBlock}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<AddCustomField
|
<AddCustomField
|
||||||
|
|||||||
+22
-3
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"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 { reorderCustomMethodCardFieldBlocks } from "../../../../../lib/create/reorderCustomMethodCardFieldBlocks";
|
||||||
import { CustomMethodCardWizardBlocksListView } from "./CustomMethodCardWizardBlocksList.view";
|
import { CustomMethodCardWizardBlocksListView } from "./CustomMethodCardWizardBlocksList.view";
|
||||||
import type { CustomMethodCardWizardBlocksListProps } from "./CustomMethodCardWizardBlocksList.types";
|
import type { CustomMethodCardWizardBlocksListProps } from "./CustomMethodCardWizardBlocksList.types";
|
||||||
@@ -11,19 +11,33 @@ function CustomMethodCardWizardBlocksListContainerComponent({
|
|||||||
dragHandleAriaLabel,
|
dragHandleAriaLabel,
|
||||||
listLabel,
|
listLabel,
|
||||||
onBlocksReorder,
|
onBlocksReorder,
|
||||||
|
onEditBlock,
|
||||||
}: CustomMethodCardWizardBlocksListProps) {
|
}: CustomMethodCardWizardBlocksListProps) {
|
||||||
const [draggingIndex, setDraggingIndex] = useState<number | null>(null);
|
const [draggingIndex, setDraggingIndex] = useState<number | null>(null);
|
||||||
const [overIndex, setOverIndex] = useState<number | null>(null);
|
const [overIndex, setOverIndex] = useState<number | null>(null);
|
||||||
|
const draggingIndexRef = useRef<number | null>(null);
|
||||||
|
const dragFromHandleRef = useRef(false);
|
||||||
|
|
||||||
const clearDragUi = useCallback(() => {
|
const clearDragUi = useCallback(() => {
|
||||||
|
draggingIndexRef.current = null;
|
||||||
|
dragFromHandleRef.current = false;
|
||||||
setDraggingIndex(null);
|
setDraggingIndex(null);
|
||||||
setOverIndex(null);
|
setOverIndex(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleHandlePointerDown = useCallback(() => {
|
||||||
|
dragFromHandleRef.current = true;
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleDragStart = useCallback(
|
const handleDragStart = useCallback(
|
||||||
(index: number) => (e: DragEvent) => {
|
(index: number) => (e: DragEvent) => {
|
||||||
|
if (!dragFromHandleRef.current) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
e.dataTransfer.effectAllowed = "move";
|
e.dataTransfer.effectAllowed = "move";
|
||||||
e.dataTransfer.setData("text/plain", String(index));
|
e.dataTransfer.setData("text/plain", String(index));
|
||||||
|
draggingIndexRef.current = index;
|
||||||
setDraggingIndex(index);
|
setDraggingIndex(index);
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
@@ -40,8 +54,11 @@ function CustomMethodCardWizardBlocksListContainerComponent({
|
|||||||
const handleDrop = useCallback(
|
const handleDrop = useCallback(
|
||||||
(index: number) => (e: DragEvent) => {
|
(index: number) => (e: DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const from = Number.parseInt(e.dataTransfer.getData("text/plain"), 10);
|
const fromData = Number.parseInt(e.dataTransfer.getData("text/plain"), 10);
|
||||||
if (Number.isNaN(from)) {
|
const from = Number.isNaN(fromData)
|
||||||
|
? draggingIndexRef.current
|
||||||
|
: fromData;
|
||||||
|
if (from == null || Number.isNaN(from)) {
|
||||||
clearDragUi();
|
clearDragUi();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -66,6 +83,8 @@ function CustomMethodCardWizardBlocksListContainerComponent({
|
|||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
onDragEnd={clearDragUi}
|
onDragEnd={clearDragUi}
|
||||||
|
onHandlePointerDown={handleHandlePointerDown}
|
||||||
|
onEditBlock={onEditBlock}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -8,6 +8,7 @@ export interface CustomMethodCardWizardBlocksListProps {
|
|||||||
dragHandleAriaLabel: string;
|
dragHandleAriaLabel: string;
|
||||||
listLabel: string;
|
listLabel: string;
|
||||||
onBlocksReorder: (_next: CustomMethodCardFieldBlock[]) => void;
|
onBlocksReorder: (_next: CustomMethodCardFieldBlock[]) => void;
|
||||||
|
onEditBlock: (_block: CustomMethodCardFieldBlock) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CustomMethodCardWizardBlocksListViewProps
|
export interface CustomMethodCardWizardBlocksListViewProps
|
||||||
@@ -18,4 +19,5 @@ export interface CustomMethodCardWizardBlocksListViewProps
|
|||||||
onDragOver: (_index: number) => (_e: DragEvent) => void;
|
onDragOver: (_index: number) => (_e: DragEvent) => void;
|
||||||
onDrop: (_index: number) => (_e: DragEvent) => void;
|
onDrop: (_index: number) => (_e: DragEvent) => void;
|
||||||
onDragEnd: () => void;
|
onDragEnd: () => void;
|
||||||
|
onHandlePointerDown: () => void;
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-7
@@ -38,6 +38,8 @@ function CustomMethodCardWizardBlocksListViewComponent({
|
|||||||
onDragOver,
|
onDragOver,
|
||||||
onDrop,
|
onDrop,
|
||||||
onDragEnd,
|
onDragEnd,
|
||||||
|
onHandlePointerDown,
|
||||||
|
onEditBlock,
|
||||||
}: CustomMethodCardWizardBlocksListViewProps) {
|
}: CustomMethodCardWizardBlocksListViewProps) {
|
||||||
return (
|
return (
|
||||||
<ul
|
<ul
|
||||||
@@ -53,6 +55,7 @@ function CustomMethodCardWizardBlocksListViewComponent({
|
|||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={block.id}
|
key={block.id}
|
||||||
|
draggable
|
||||||
aria-grabbed={isDragging ? true : undefined}
|
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] ${
|
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
|
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)]"
|
? "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)]"
|
: "border-[var(--color-border-default-primary)] hover:border-[var(--color-content-default-secondary)]"
|
||||||
}`}
|
}`}
|
||||||
|
onDragStart={onDragStart(index)}
|
||||||
onDragOver={onDragOver(index)}
|
onDragOver={onDragOver(index)}
|
||||||
onDrop={onDrop(index)}
|
onDrop={onDrop(index)}
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
draggable
|
|
||||||
onDragStart={onDragStart(index)}
|
|
||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
aria-label={dragHandleAriaLabel}
|
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 />
|
<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">
|
<span className="flex h-8 w-8 shrink-0 items-center justify-center self-center">
|
||||||
<Icon
|
<Icon
|
||||||
name={ADD_CUSTOM_FIELD_TYPE_ICONS[kind]}
|
name={ADD_CUSTOM_FIELD_TYPE_ICONS[kind]}
|
||||||
@@ -89,6 +99,7 @@ function CustomMethodCardWizardBlocksListViewComponent({
|
|||||||
{typeLabel}
|
{typeLabel}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</button>
|
||||||
</li>
|
</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 CustomMethodCardModalBody from "../../components/CustomMethodCardModalBody";
|
||||||
import { buildCustomRuleModalKebabMenu } from "../../components/customRuleModalKebabMenu";
|
import { buildCustomRuleModalKebabMenu } from "../../components/customRuleModalKebabMenu";
|
||||||
import { methodCardMetaWithCustomizeHeader } from "../../../../../lib/create/methodCardCustomizeMetaPatch";
|
import { methodCardMetaWithCustomizeHeader } from "../../../../../lib/create/methodCardCustomizeMetaPatch";
|
||||||
|
import { buildMethodCardWizardInitialValues } from "../../../../../lib/create/methodCardWizardPrefill";
|
||||||
|
import type { MethodCardWizardInitialValues } from "../../../../../lib/create/methodCardWizardPrefill";
|
||||||
import {
|
import {
|
||||||
captureMethodCardCustomizeSnapshot,
|
captureMethodCardCustomizeSnapshot,
|
||||||
type MethodCardCustomizeSnapshot,
|
type MethodCardCustomizeSnapshot,
|
||||||
type MethodCardHeaderDraft,
|
type MethodCardHeaderDraft,
|
||||||
} from "../../../../../lib/create/methodCardCustomizeSession";
|
} from "../../../../../lib/create/methodCardCustomizeSession";
|
||||||
import MethodCardCustomizeModalHeader from "../../components/MethodCardCustomizeModalHeader";
|
|
||||||
|
|
||||||
export function CommunicationMethodsScreen() {
|
export function CommunicationMethodsScreen() {
|
||||||
const m = useMessages();
|
const m = useMessages();
|
||||||
const comm = m.create.customRule.communication;
|
const comm = m.create.customRule.communication;
|
||||||
const modalKebabMenu = m.create.customRule.modalKebabMenu;
|
const modalKebabMenu = m.create.customRule.modalKebabMenu;
|
||||||
const mdUp = useCreateFlowMdUp();
|
const mdUp = useCreateFlowMdUp();
|
||||||
const { confirmDiscard, confirmDirtyCustomizeCancel, confirmDialog } =
|
const { confirmDiscard, confirmDialog } = useDiscardCustomizeConfirm();
|
||||||
useDiscardCustomizeConfirm();
|
|
||||||
const { state, updateState, replaceState, markCreateFlowInteraction } =
|
const { state, updateState, replaceState, markCreateFlowInteraction } =
|
||||||
useCreateFlow();
|
useCreateFlow();
|
||||||
const pendingEphemeralDuplicateIdRef = useRef<string | null>(null);
|
const pendingEphemeralDuplicateIdRef = useRef<string | null>(null);
|
||||||
@@ -79,12 +79,14 @@ export function CommunicationMethodsScreen() {
|
|||||||
const [pendingDraft, setPendingDraft] =
|
const [pendingDraft, setPendingDraft] =
|
||||||
useState<CommunicationMethodDetailEntry | null>(null);
|
useState<CommunicationMethodDetailEntry | null>(null);
|
||||||
const [addCustomWizardOpen, setAddCustomWizardOpen] = useState(false);
|
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<
|
const [draftFieldBlocks, setDraftFieldBlocks] = useState<
|
||||||
CustomMethodCardFieldBlock[] | null
|
CustomMethodCardFieldBlock[] | null
|
||||||
>(null);
|
>(null);
|
||||||
const [customizeHeaderDraft, setCustomizeHeaderDraft] =
|
|
||||||
useState<MethodCardHeaderDraft | null>(null);
|
|
||||||
|
|
||||||
const selectedIds = state.selectedCommunicationMethodIds ?? [];
|
const selectedIds = state.selectedCommunicationMethodIds ?? [];
|
||||||
|
|
||||||
@@ -107,6 +109,8 @@ export function CommunicationMethodsScreen() {
|
|||||||
|
|
||||||
const handleOpenAddWizard = useCallback(() => {
|
const handleOpenAddWizard = useCallback(() => {
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
|
setWizardCustomizeCardId(null);
|
||||||
|
setWizardInitialValues(null);
|
||||||
setAddCustomWizardOpen(true);
|
setAddCustomWizardOpen(true);
|
||||||
}, [markCreateFlowInteraction]);
|
}, [markCreateFlowInteraction]);
|
||||||
|
|
||||||
@@ -144,15 +148,40 @@ export function CommunicationMethodsScreen() {
|
|||||||
const handleCardClick = useCallback(
|
const handleCardClick = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
customizeSnapshotRef.current = null;
|
const draft = seedDraft(id);
|
||||||
setModalEditUnlocked(false);
|
const persistedBlocks = state.customMethodCardFieldBlocksById?.[id];
|
||||||
setDraftFieldBlocks(null);
|
const initialBlocks =
|
||||||
setCustomizeHeaderDraft(null);
|
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);
|
setPendingCardId(id);
|
||||||
setPendingDraft(seedDraft(id));
|
setPendingDraft(draft);
|
||||||
|
setDraftFieldBlocks(initialBlocks);
|
||||||
setCreateModalOpen(true);
|
setCreateModalOpen(true);
|
||||||
},
|
},
|
||||||
[markCreateFlowInteraction, seedDraft],
|
[
|
||||||
|
comm.confirmModal.description,
|
||||||
|
comm.confirmModal.title,
|
||||||
|
markCreateFlowInteraction,
|
||||||
|
methodById,
|
||||||
|
seedDraft,
|
||||||
|
state.customMethodCardFieldBlocksById,
|
||||||
|
state.customMethodCardMetaById,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDraftChange = useCallback(
|
const handleDraftChange = useCallback(
|
||||||
@@ -165,9 +194,7 @@ export function CommunicationMethodsScreen() {
|
|||||||
|
|
||||||
const isSelectedCardModal =
|
const isSelectedCardModal =
|
||||||
pendingCardId !== null && selectedIds.includes(pendingCardId);
|
pendingCardId !== null && selectedIds.includes(pendingCardId);
|
||||||
const fieldsLocked = !modalEditUnlocked;
|
const showMethodModalPrimary = true;
|
||||||
|
|
||||||
const showMethodModalPrimary = !isSelectedCardModal || modalEditUnlocked;
|
|
||||||
|
|
||||||
const customFacetDetailsMatchPreset = useMemo(() => {
|
const customFacetDetailsMatchPreset = useMemo(() => {
|
||||||
if (!pendingCardId || !pendingDraft) return false;
|
if (!pendingCardId || !pendingDraft) return false;
|
||||||
@@ -189,7 +216,7 @@ export function CommunicationMethodsScreen() {
|
|||||||
methodId: pendingCardId,
|
methodId: pendingCardId,
|
||||||
meta: state.customMethodCardMetaById,
|
meta: state.customMethodCardMetaById,
|
||||||
fieldBlocksById: state.customMethodCardFieldBlocksById,
|
fieldBlocksById: state.customMethodCardFieldBlocksById,
|
||||||
modalEditUnlocked,
|
modalEditUnlocked: false,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
customFacetDetailsMatchPreset,
|
customFacetDetailsMatchPreset,
|
||||||
}),
|
}),
|
||||||
@@ -197,7 +224,6 @@ export function CommunicationMethodsScreen() {
|
|||||||
[
|
[
|
||||||
customFacetDetailsMatchPreset,
|
customFacetDetailsMatchPreset,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
modalEditUnlocked,
|
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
state.customMethodCardFieldBlocksById,
|
state.customMethodCardFieldBlocksById,
|
||||||
state.customMethodCardMetaById,
|
state.customMethodCardMetaById,
|
||||||
@@ -207,11 +233,11 @@ export function CommunicationMethodsScreen() {
|
|||||||
const handleCreateModalClose = useCallback(async () => {
|
const handleCreateModalClose = useCallback(async () => {
|
||||||
if (
|
if (
|
||||||
!(await confirmDiscard(
|
!(await confirmDiscard(
|
||||||
modalEditUnlocked,
|
true,
|
||||||
customizeSnapshotRef.current,
|
customizeSnapshotRef.current,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
customizeHeaderDraft,
|
customizeSnapshotRef.current?.headerDraft ?? null,
|
||||||
))
|
))
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@@ -239,53 +265,14 @@ export function CommunicationMethodsScreen() {
|
|||||||
setCreateModalOpen(false);
|
setCreateModalOpen(false);
|
||||||
setPendingCardId(null);
|
setPendingCardId(null);
|
||||||
setPendingDraft(null);
|
setPendingDraft(null);
|
||||||
setModalEditUnlocked(false);
|
|
||||||
setDraftFieldBlocks(null);
|
setDraftFieldBlocks(null);
|
||||||
setCustomizeHeaderDraft(null);
|
|
||||||
}, [
|
}, [
|
||||||
confirmDiscard,
|
confirmDiscard,
|
||||||
customizeHeaderDraft,
|
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
modalEditUnlocked,
|
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
replaceState,
|
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 () => {
|
const handleRemoveSelectedFromModal = useCallback(async () => {
|
||||||
if (!pendingCardId || !selectedIds.includes(pendingCardId)) {
|
if (!pendingCardId || !selectedIds.includes(pendingCardId)) {
|
||||||
return;
|
return;
|
||||||
@@ -293,11 +280,11 @@ export function CommunicationMethodsScreen() {
|
|||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
if (
|
if (
|
||||||
!(await confirmDiscard(
|
!(await confirmDiscard(
|
||||||
modalEditUnlocked,
|
true,
|
||||||
customizeSnapshotRef.current,
|
customizeSnapshotRef.current,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
customizeHeaderDraft,
|
customizeSnapshotRef.current?.headerDraft ?? null,
|
||||||
))
|
))
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@@ -313,11 +300,9 @@ export function CommunicationMethodsScreen() {
|
|||||||
await handleCreateModalClose();
|
await handleCreateModalClose();
|
||||||
}, [
|
}, [
|
||||||
confirmDiscard,
|
confirmDiscard,
|
||||||
customizeHeaderDraft,
|
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
handleCreateModalClose,
|
handleCreateModalClose,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
modalEditUnlocked,
|
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
@@ -327,37 +312,36 @@ export function CommunicationMethodsScreen() {
|
|||||||
|
|
||||||
const handleCustomize = useCallback(() => {
|
const handleCustomize = useCallback(() => {
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
if (!pendingDraft || !pendingCardId) {
|
if (!pendingCardId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const persistedBlocks =
|
|
||||||
state.customMethodCardFieldBlocksById?.[pendingCardId] ?? [];
|
|
||||||
const initialFieldBlocks =
|
|
||||||
persistedBlocks.length > 0
|
|
||||||
? structuredClone(persistedBlocks)
|
|
||||||
: isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById)
|
|
||||||
? []
|
|
||||||
: null;
|
|
||||||
const method = methodById.get(pendingCardId);
|
const method = methodById.get(pendingCardId);
|
||||||
const meta = state.customMethodCardMetaById?.[pendingCardId];
|
setWizardInitialValues(
|
||||||
const headerDraft: MethodCardHeaderDraft = {
|
buildMethodCardWizardInitialValues({
|
||||||
title: meta?.label ?? method?.label ?? comm.confirmModal.title,
|
cardId: pendingCardId,
|
||||||
description:
|
fallbackTitle: method?.label ?? comm.confirmModal.title,
|
||||||
meta?.supportText ??
|
fallbackDescription:
|
||||||
method?.supportText ??
|
method?.supportText ?? comm.confirmModal.description,
|
||||||
comm.confirmModal.description,
|
meta: state.customMethodCardMetaById,
|
||||||
};
|
persistedBlocks: state.customMethodCardFieldBlocksById,
|
||||||
setCustomizeHeaderDraft(headerDraft);
|
draftFieldBlocks,
|
||||||
customizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
|
facetPrefill: pendingDraft
|
||||||
pendingDraft,
|
? {
|
||||||
initialFieldBlocks,
|
group: "communication",
|
||||||
headerDraft,
|
draft: pendingDraft,
|
||||||
|
headings: comm.sectionHeadings,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
setDraftFieldBlocks(initialFieldBlocks);
|
setWizardCustomizeCardId(pendingCardId);
|
||||||
setModalEditUnlocked(true);
|
setCreateModalOpen(false);
|
||||||
|
setAddCustomWizardOpen(true);
|
||||||
}, [
|
}, [
|
||||||
comm.confirmModal.description,
|
comm.confirmModal.description,
|
||||||
comm.confirmModal.title,
|
comm.confirmModal.title,
|
||||||
|
comm.sectionHeadings,
|
||||||
|
draftFieldBlocks,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
methodById,
|
methodById,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
@@ -382,9 +366,7 @@ export function CommunicationMethodsScreen() {
|
|||||||
() => communicationPresetFor(newId),
|
() => communicationPresetFor(newId),
|
||||||
);
|
);
|
||||||
const blocksClone = structuredClone(
|
const blocksClone = structuredClone(
|
||||||
modalEditUnlocked &&
|
draftFieldBlocks !== null
|
||||||
draftFieldBlocks !== null &&
|
|
||||||
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById)
|
|
||||||
? draftFieldBlocks
|
? draftFieldBlocks
|
||||||
: cloneMethodCardBlocksForDuplicate(
|
: cloneMethodCardBlocksForDuplicate(
|
||||||
state.customMethodCardFieldBlocksById,
|
state.customMethodCardFieldBlocksById,
|
||||||
@@ -414,16 +396,15 @@ export function CommunicationMethodsScreen() {
|
|||||||
customizeSnapshotRef.current = null;
|
customizeSnapshotRef.current = null;
|
||||||
setPendingCardId(newId);
|
setPendingCardId(newId);
|
||||||
setPendingDraft(structuredClone(detailsClone));
|
setPendingDraft(structuredClone(detailsClone));
|
||||||
setModalEditUnlocked(false);
|
setDraftFieldBlocks(
|
||||||
setDraftFieldBlocks(null);
|
blocksClone.length > 0 ? structuredClone(blocksClone) : null,
|
||||||
setCustomizeHeaderDraft(null);
|
);
|
||||||
}, [
|
}, [
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
modalKebabMenu.duplicateTitleSuffix,
|
modalKebabMenu.duplicateTitleSuffix,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
modalEditUnlocked,
|
|
||||||
state.communicationMethodDetailsById,
|
state.communicationMethodDetailsById,
|
||||||
state.customMethodCardFieldBlocksById,
|
state.customMethodCardFieldBlocksById,
|
||||||
state.customMethodCardMetaById,
|
state.customMethodCardMetaById,
|
||||||
@@ -449,9 +430,7 @@ export function CommunicationMethodsScreen() {
|
|||||||
() => communicationPresetFor(newId),
|
() => communicationPresetFor(newId),
|
||||||
);
|
);
|
||||||
const blocksClone = structuredClone(
|
const blocksClone = structuredClone(
|
||||||
modalEditUnlocked &&
|
draftFieldBlocks !== null
|
||||||
draftFieldBlocks !== null &&
|
|
||||||
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById)
|
|
||||||
? draftFieldBlocks
|
? draftFieldBlocks
|
||||||
: cloneMethodCardBlocksForDuplicate(
|
: cloneMethodCardBlocksForDuplicate(
|
||||||
state.customMethodCardFieldBlocksById,
|
state.customMethodCardFieldBlocksById,
|
||||||
@@ -481,14 +460,13 @@ export function CommunicationMethodsScreen() {
|
|||||||
customizeSnapshotRef.current = null;
|
customizeSnapshotRef.current = null;
|
||||||
setPendingCardId(newId);
|
setPendingCardId(newId);
|
||||||
setPendingDraft(structuredClone(detailsClone));
|
setPendingDraft(structuredClone(detailsClone));
|
||||||
setModalEditUnlocked(false);
|
setDraftFieldBlocks(
|
||||||
setDraftFieldBlocks(null);
|
blocksClone.length > 0 ? structuredClone(blocksClone) : null,
|
||||||
setCustomizeHeaderDraft(null);
|
);
|
||||||
}, [
|
}, [
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
methodById,
|
methodById,
|
||||||
modalEditUnlocked,
|
|
||||||
modalKebabMenu.duplicateTitleSuffix,
|
modalKebabMenu.duplicateTitleSuffix,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
@@ -501,7 +479,7 @@ export function CommunicationMethodsScreen() {
|
|||||||
const kebabMenuItems = useMemo(
|
const kebabMenuItems = useMemo(
|
||||||
() =>
|
() =>
|
||||||
buildCustomRuleModalKebabMenu(modalKebabMenu, {
|
buildCustomRuleModalKebabMenu(modalKebabMenu, {
|
||||||
showCustomize: !modalEditUnlocked,
|
showCustomize: true,
|
||||||
onCustomize: handleCustomize,
|
onCustomize: handleCustomize,
|
||||||
onDuplicate:
|
onDuplicate:
|
||||||
(state.editingPublishedRuleId?.trim() ?? "") !== "" || !pendingCardId
|
(state.editingPublishedRuleId?.trim() ?? "") !== "" || !pendingCardId
|
||||||
@@ -521,7 +499,6 @@ export function CommunicationMethodsScreen() {
|
|||||||
handleDuplicatePrefabCard,
|
handleDuplicatePrefabCard,
|
||||||
handleRemoveSelectedFromModal,
|
handleRemoveSelectedFromModal,
|
||||||
isSelectedCardModal,
|
isSelectedCardModal,
|
||||||
modalEditUnlocked,
|
|
||||||
modalKebabMenu,
|
modalKebabMenu,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
state.customMethodCardMetaById,
|
state.customMethodCardMetaById,
|
||||||
@@ -540,7 +517,7 @@ export function CommunicationMethodsScreen() {
|
|||||||
meta?.supportText ??
|
meta?.supportText ??
|
||||||
method?.supportText ??
|
method?.supportText ??
|
||||||
comm.confirmModal.description,
|
comm.confirmModal.description,
|
||||||
nextButtonText: modalEditUnlocked
|
nextButtonText: isSelectedCardModal
|
||||||
? saveLabel
|
? saveLabel
|
||||||
: comm.addPlatform.nextButtonText,
|
: comm.addPlatform.nextButtonText,
|
||||||
};
|
};
|
||||||
@@ -552,8 +529,14 @@ export function CommunicationMethodsScreen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCloseAddWizard = useCallback(() => {
|
const handleCloseAddWizard = useCallback(() => {
|
||||||
|
const resumeCardId = wizardCustomizeCardId;
|
||||||
setAddCustomWizardOpen(false);
|
setAddCustomWizardOpen(false);
|
||||||
}, []);
|
setWizardCustomizeCardId(null);
|
||||||
|
setWizardInitialValues(null);
|
||||||
|
if (resumeCardId && pendingCardId === resumeCardId) {
|
||||||
|
setCreateModalOpen(true);
|
||||||
|
}
|
||||||
|
}, [pendingCardId, wizardCustomizeCardId]);
|
||||||
|
|
||||||
const handleFinalizeCustomCard = useCallback(
|
const handleFinalizeCustomCard = useCallback(
|
||||||
({
|
({
|
||||||
@@ -566,6 +549,42 @@ export function CommunicationMethodsScreen() {
|
|||||||
fieldBlocks: CustomMethodCardFieldBlock[];
|
fieldBlocks: CustomMethodCardFieldBlock[];
|
||||||
}) => {
|
}) => {
|
||||||
markCreateFlowInteraction();
|
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();
|
const id = crypto.randomUUID();
|
||||||
updateState({
|
updateState({
|
||||||
selectedCommunicationMethodIds: moveFacetSelectionIdToFront(
|
selectedCommunicationMethodIds: moveFacetSelectionIdToFront(
|
||||||
@@ -588,91 +607,29 @@ export function CommunicationMethodsScreen() {
|
|||||||
},
|
},
|
||||||
[
|
[
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
|
pendingDraft,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
state.communicationMethodDetailsById,
|
state.communicationMethodDetailsById,
|
||||||
state.customMethodCardFieldBlocksById,
|
state.customMethodCardFieldBlocksById,
|
||||||
state.customMethodCardMetaById,
|
state.customMethodCardMetaById,
|
||||||
updateState,
|
updateState,
|
||||||
|
wizardCustomizeCardId,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCreateModalPrimary = useCallback(() => {
|
const handleCreateModalPrimary = useCallback(() => {
|
||||||
if (!pendingCardId) {
|
if (!pendingCardId) {
|
||||||
handleCreateModalClose();
|
void handleCreateModalClose();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
|
|
||||||
if (selectedIds.includes(pendingCardId)) {
|
const persistWizardBlocks =
|
||||||
if (modalEditUnlocked) {
|
modalUsesWizardFieldBlocksBody && draftFieldBlocks !== null;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (modalEditUnlocked) {
|
if (selectedIds.includes(pendingCardId)) {
|
||||||
if (!customizeHeaderDraft) {
|
if (persistWizardBlocks) {
|
||||||
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({
|
updateState({
|
||||||
customMethodCardMetaById: nextMeta,
|
|
||||||
customMethodCardFieldBlocksById: {
|
customMethodCardFieldBlocksById: {
|
||||||
...(state.customMethodCardFieldBlocksById ?? {}),
|
...(state.customMethodCardFieldBlocksById ?? {}),
|
||||||
[pendingCardId]: structuredClone(draftFieldBlocks ?? []),
|
[pendingCardId]: structuredClone(draftFieldBlocks ?? []),
|
||||||
@@ -680,22 +637,27 @@ export function CommunicationMethodsScreen() {
|
|||||||
});
|
});
|
||||||
} else if (pendingDraft) {
|
} else if (pendingDraft) {
|
||||||
updateState({
|
updateState({
|
||||||
customMethodCardMetaById: nextMeta,
|
|
||||||
communicationMethodDetailsById: {
|
communicationMethodDetailsById: {
|
||||||
...(state.communicationMethodDetailsById ?? {}),
|
...(state.communicationMethodDetailsById ?? {}),
|
||||||
[pendingCardId]: pendingDraft,
|
[pendingCardId]: pendingDraft,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
customizeSnapshotRef.current = null;
|
if (pendingDraft) {
|
||||||
setModalEditUnlocked(false);
|
customizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
|
||||||
setDraftFieldBlocks(null);
|
pendingDraft,
|
||||||
setCustomizeHeaderDraft(null);
|
persistWizardBlocks ? draftFieldBlocks : null,
|
||||||
|
customizeSnapshotRef.current?.headerDraft ?? {
|
||||||
|
title: "",
|
||||||
|
description: "",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!pendingDraft) {
|
if (!pendingDraft) {
|
||||||
handleCreateModalClose();
|
void handleCreateModalClose();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updateState({
|
updateState({
|
||||||
@@ -707,15 +669,23 @@ export function CommunicationMethodsScreen() {
|
|||||||
...(state.communicationMethodDetailsById ?? {}),
|
...(state.communicationMethodDetailsById ?? {}),
|
||||||
[pendingCardId]: pendingDraft,
|
[pendingCardId]: pendingDraft,
|
||||||
},
|
},
|
||||||
|
...(persistWizardBlocks
|
||||||
|
? {
|
||||||
|
customMethodCardFieldBlocksById: {
|
||||||
|
...(state.customMethodCardFieldBlocksById ?? {}),
|
||||||
|
[pendingCardId]: structuredClone(draftFieldBlocks ?? []),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
});
|
});
|
||||||
pendingEphemeralDuplicateIdRef.current = null;
|
pendingEphemeralDuplicateIdRef.current = null;
|
||||||
handleCreateModalClose();
|
customizeSnapshotRef.current = null;
|
||||||
|
void handleCreateModalClose();
|
||||||
}, [
|
}, [
|
||||||
customizeHeaderDraft,
|
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
handleCreateModalClose,
|
handleCreateModalClose,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
modalEditUnlocked,
|
modalUsesWizardFieldBlocksBody,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
@@ -766,33 +736,10 @@ export function CommunicationMethodsScreen() {
|
|||||||
<Create
|
<Create
|
||||||
isOpen={createModalOpen}
|
isOpen={createModalOpen}
|
||||||
onClose={handleCreateModalClose}
|
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}
|
onNext={handleCreateModalPrimary}
|
||||||
title={modalConfig.title}
|
title={modalConfig.title}
|
||||||
description={modalConfig.description}
|
description={modalConfig.description}
|
||||||
nextButtonText={modalConfig.nextButtonText}
|
nextButtonText={modalConfig.nextButtonText}
|
||||||
showBackButton={modalEditUnlocked}
|
|
||||||
onBack={handleCancelCustomize}
|
|
||||||
backButtonText={modalKebabMenu.cancelCustomize}
|
|
||||||
showNextButton={showMethodModalPrimary}
|
showNextButton={showMethodModalPrimary}
|
||||||
backdropVariant="blurredYellow"
|
backdropVariant="blurredYellow"
|
||||||
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
|
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
|
||||||
@@ -805,14 +752,14 @@ export function CommunicationMethodsScreen() {
|
|||||||
cardId={pendingCardId}
|
cardId={pendingCardId}
|
||||||
blocksById={state.customMethodCardFieldBlocksById}
|
blocksById={state.customMethodCardFieldBlocksById}
|
||||||
blocksOverride={
|
blocksOverride={
|
||||||
modalEditUnlocked && draftFieldBlocks !== null
|
draftFieldBlocks !== null ? draftFieldBlocks : undefined
|
||||||
? draftFieldBlocks
|
|
||||||
: undefined
|
|
||||||
}
|
}
|
||||||
policyMeta={state.customMethodCardMetaById?.[pendingCardId]}
|
policyMeta={state.customMethodCardMetaById?.[pendingCardId]}
|
||||||
showPolicyContentLockupWhenNoBlocks={!modalEditUnlocked}
|
showPolicyContentLockupWhenNoBlocks={
|
||||||
|
draftFieldBlocks === null || draftFieldBlocks.length === 0
|
||||||
|
}
|
||||||
onFieldBlocksChange={
|
onFieldBlocksChange={
|
||||||
fieldsLocked
|
draftFieldBlocks === null
|
||||||
? undefined
|
? undefined
|
||||||
: (next) => setDraftFieldBlocks(next)
|
: (next) => setDraftFieldBlocks(next)
|
||||||
}
|
}
|
||||||
@@ -821,7 +768,6 @@ export function CommunicationMethodsScreen() {
|
|||||||
<CommunicationMethodEditFields
|
<CommunicationMethodEditFields
|
||||||
value={pendingDraft}
|
value={pendingDraft}
|
||||||
onChange={handleDraftChange}
|
onChange={handleDraftChange}
|
||||||
readOnly={fieldsLocked}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
) : null}
|
) : null}
|
||||||
@@ -830,6 +776,7 @@ export function CommunicationMethodsScreen() {
|
|||||||
<CustomMethodCardWizard
|
<CustomMethodCardWizard
|
||||||
isOpen={addCustomWizardOpen}
|
isOpen={addCustomWizardOpen}
|
||||||
onClose={handleCloseAddWizard}
|
onClose={handleCloseAddWizard}
|
||||||
|
initialValues={wizardInitialValues}
|
||||||
onFinalize={handleFinalizeCustomCard}
|
onFinalize={handleFinalizeCustomCard}
|
||||||
onPersistCustomUploadFile={(file) =>
|
onPersistCustomUploadFile={(file) =>
|
||||||
uploadCreateFlowFile(file, "customMethodAttachment")
|
uploadCreateFlowFile(file, "customMethodAttachment")
|
||||||
|
|||||||
@@ -50,20 +50,20 @@ import type { ConflictManagementDetailEntry } from "../../types";
|
|||||||
import CustomMethodCardModalBody from "../../components/CustomMethodCardModalBody";
|
import CustomMethodCardModalBody from "../../components/CustomMethodCardModalBody";
|
||||||
import { buildCustomRuleModalKebabMenu } from "../../components/customRuleModalKebabMenu";
|
import { buildCustomRuleModalKebabMenu } from "../../components/customRuleModalKebabMenu";
|
||||||
import { methodCardMetaWithCustomizeHeader } from "../../../../../lib/create/methodCardCustomizeMetaPatch";
|
import { methodCardMetaWithCustomizeHeader } from "../../../../../lib/create/methodCardCustomizeMetaPatch";
|
||||||
|
import { buildMethodCardWizardInitialValues } from "../../../../../lib/create/methodCardWizardPrefill";
|
||||||
|
import type { MethodCardWizardInitialValues } from "../../../../../lib/create/methodCardWizardPrefill";
|
||||||
import {
|
import {
|
||||||
captureMethodCardCustomizeSnapshot,
|
captureMethodCardCustomizeSnapshot,
|
||||||
type MethodCardCustomizeSnapshot,
|
type MethodCardCustomizeSnapshot,
|
||||||
type MethodCardHeaderDraft,
|
type MethodCardHeaderDraft,
|
||||||
} from "../../../../../lib/create/methodCardCustomizeSession";
|
} from "../../../../../lib/create/methodCardCustomizeSession";
|
||||||
import MethodCardCustomizeModalHeader from "../../components/MethodCardCustomizeModalHeader";
|
|
||||||
|
|
||||||
export function ConflictManagementScreen() {
|
export function ConflictManagementScreen() {
|
||||||
const m = useMessages();
|
const m = useMessages();
|
||||||
const cm = m.create.customRule.conflictManagement;
|
const cm = m.create.customRule.conflictManagement;
|
||||||
const modalKebabMenu = m.create.customRule.modalKebabMenu;
|
const modalKebabMenu = m.create.customRule.modalKebabMenu;
|
||||||
const mdUp = useCreateFlowMdUp();
|
const mdUp = useCreateFlowMdUp();
|
||||||
const { confirmDiscard, confirmDirtyCustomizeCancel, confirmDialog } =
|
const { confirmDiscard, confirmDialog } = useDiscardCustomizeConfirm();
|
||||||
useDiscardCustomizeConfirm();
|
|
||||||
const { state, updateState, replaceState, markCreateFlowInteraction } =
|
const { state, updateState, replaceState, markCreateFlowInteraction } =
|
||||||
useCreateFlow();
|
useCreateFlow();
|
||||||
const pendingEphemeralDuplicateIdRef = useRef<string | null>(null);
|
const pendingEphemeralDuplicateIdRef = useRef<string | null>(null);
|
||||||
@@ -76,12 +76,14 @@ export function ConflictManagementScreen() {
|
|||||||
const [pendingDraft, setPendingDraft] =
|
const [pendingDraft, setPendingDraft] =
|
||||||
useState<ConflictManagementDetailEntry | null>(null);
|
useState<ConflictManagementDetailEntry | null>(null);
|
||||||
const [addCustomWizardOpen, setAddCustomWizardOpen] = useState(false);
|
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<
|
const [draftFieldBlocks, setDraftFieldBlocks] = useState<
|
||||||
CustomMethodCardFieldBlock[] | null
|
CustomMethodCardFieldBlock[] | null
|
||||||
>(null);
|
>(null);
|
||||||
const [customizeHeaderDraft, setCustomizeHeaderDraft] =
|
|
||||||
useState<MethodCardHeaderDraft | null>(null);
|
|
||||||
|
|
||||||
const selectedIds = state.selectedConflictManagementIds ?? [];
|
const selectedIds = state.selectedConflictManagementIds ?? [];
|
||||||
|
|
||||||
@@ -104,6 +106,8 @@ export function ConflictManagementScreen() {
|
|||||||
|
|
||||||
const handleOpenAddWizard = useCallback(() => {
|
const handleOpenAddWizard = useCallback(() => {
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
|
setWizardCustomizeCardId(null);
|
||||||
|
setWizardInitialValues(null);
|
||||||
setAddCustomWizardOpen(true);
|
setAddCustomWizardOpen(true);
|
||||||
}, [markCreateFlowInteraction]);
|
}, [markCreateFlowInteraction]);
|
||||||
|
|
||||||
@@ -145,15 +149,40 @@ export function ConflictManagementScreen() {
|
|||||||
const handleCardClick = useCallback(
|
const handleCardClick = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
customizeSnapshotRef.current = null;
|
const draft = seedDraft(id);
|
||||||
setModalEditUnlocked(false);
|
const persistedBlocks = state.customMethodCardFieldBlocksById?.[id];
|
||||||
setDraftFieldBlocks(null);
|
const initialBlocks =
|
||||||
setCustomizeHeaderDraft(null);
|
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);
|
setPendingCardId(id);
|
||||||
setPendingDraft(seedDraft(id));
|
setPendingDraft(draft);
|
||||||
|
setDraftFieldBlocks(initialBlocks);
|
||||||
setCreateModalOpen(true);
|
setCreateModalOpen(true);
|
||||||
},
|
},
|
||||||
[markCreateFlowInteraction, seedDraft],
|
[
|
||||||
|
cm.confirmModal.description,
|
||||||
|
cm.confirmModal.title,
|
||||||
|
markCreateFlowInteraction,
|
||||||
|
methodById,
|
||||||
|
seedDraft,
|
||||||
|
state.customMethodCardFieldBlocksById,
|
||||||
|
state.customMethodCardMetaById,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDraftChange = useCallback(
|
const handleDraftChange = useCallback(
|
||||||
@@ -166,9 +195,7 @@ export function ConflictManagementScreen() {
|
|||||||
|
|
||||||
const isSelectedCardModal =
|
const isSelectedCardModal =
|
||||||
pendingCardId !== null && selectedIds.includes(pendingCardId);
|
pendingCardId !== null && selectedIds.includes(pendingCardId);
|
||||||
const fieldsLocked = !modalEditUnlocked;
|
const showMethodModalPrimary = true;
|
||||||
|
|
||||||
const showMethodModalPrimary = !isSelectedCardModal || modalEditUnlocked;
|
|
||||||
|
|
||||||
const customFacetDetailsMatchPreset = useMemo(() => {
|
const customFacetDetailsMatchPreset = useMemo(() => {
|
||||||
if (!pendingCardId || !pendingDraft) return false;
|
if (!pendingCardId || !pendingDraft) return false;
|
||||||
@@ -190,7 +217,7 @@ export function ConflictManagementScreen() {
|
|||||||
methodId: pendingCardId,
|
methodId: pendingCardId,
|
||||||
meta: state.customMethodCardMetaById,
|
meta: state.customMethodCardMetaById,
|
||||||
fieldBlocksById: state.customMethodCardFieldBlocksById,
|
fieldBlocksById: state.customMethodCardFieldBlocksById,
|
||||||
modalEditUnlocked,
|
modalEditUnlocked: false,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
customFacetDetailsMatchPreset,
|
customFacetDetailsMatchPreset,
|
||||||
}),
|
}),
|
||||||
@@ -198,7 +225,6 @@ export function ConflictManagementScreen() {
|
|||||||
[
|
[
|
||||||
customFacetDetailsMatchPreset,
|
customFacetDetailsMatchPreset,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
modalEditUnlocked,
|
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
state.customMethodCardFieldBlocksById,
|
state.customMethodCardFieldBlocksById,
|
||||||
state.customMethodCardMetaById,
|
state.customMethodCardMetaById,
|
||||||
@@ -208,11 +234,11 @@ export function ConflictManagementScreen() {
|
|||||||
const handleCreateModalClose = useCallback(async () => {
|
const handleCreateModalClose = useCallback(async () => {
|
||||||
if (
|
if (
|
||||||
!(await confirmDiscard(
|
!(await confirmDiscard(
|
||||||
modalEditUnlocked,
|
true,
|
||||||
customizeSnapshotRef.current,
|
customizeSnapshotRef.current,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
customizeHeaderDraft,
|
customizeSnapshotRef.current?.headerDraft ?? null,
|
||||||
))
|
))
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@@ -240,53 +266,14 @@ export function ConflictManagementScreen() {
|
|||||||
setCreateModalOpen(false);
|
setCreateModalOpen(false);
|
||||||
setPendingCardId(null);
|
setPendingCardId(null);
|
||||||
setPendingDraft(null);
|
setPendingDraft(null);
|
||||||
setModalEditUnlocked(false);
|
|
||||||
setDraftFieldBlocks(null);
|
setDraftFieldBlocks(null);
|
||||||
setCustomizeHeaderDraft(null);
|
|
||||||
}, [
|
}, [
|
||||||
confirmDiscard,
|
confirmDiscard,
|
||||||
customizeHeaderDraft,
|
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
modalEditUnlocked,
|
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
replaceState,
|
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 () => {
|
const handleRemoveSelectedFromModal = useCallback(async () => {
|
||||||
if (!pendingCardId || !selectedIds.includes(pendingCardId)) {
|
if (!pendingCardId || !selectedIds.includes(pendingCardId)) {
|
||||||
return;
|
return;
|
||||||
@@ -294,11 +281,11 @@ export function ConflictManagementScreen() {
|
|||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
if (
|
if (
|
||||||
!(await confirmDiscard(
|
!(await confirmDiscard(
|
||||||
modalEditUnlocked,
|
true,
|
||||||
customizeSnapshotRef.current,
|
customizeSnapshotRef.current,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
customizeHeaderDraft,
|
customizeSnapshotRef.current?.headerDraft ?? null,
|
||||||
))
|
))
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@@ -314,11 +301,9 @@ export function ConflictManagementScreen() {
|
|||||||
await handleCreateModalClose();
|
await handleCreateModalClose();
|
||||||
}, [
|
}, [
|
||||||
confirmDiscard,
|
confirmDiscard,
|
||||||
customizeHeaderDraft,
|
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
handleCreateModalClose,
|
handleCreateModalClose,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
modalEditUnlocked,
|
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
@@ -328,35 +313,36 @@ export function ConflictManagementScreen() {
|
|||||||
|
|
||||||
const handleCustomize = useCallback(() => {
|
const handleCustomize = useCallback(() => {
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
if (!pendingDraft || !pendingCardId) {
|
if (!pendingCardId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const initialFieldBlocks =
|
|
||||||
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById)
|
|
||||||
? structuredClone(
|
|
||||||
state.customMethodCardFieldBlocksById?.[pendingCardId] ?? [],
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
const method = methodById.get(pendingCardId);
|
const method = methodById.get(pendingCardId);
|
||||||
const meta = state.customMethodCardMetaById?.[pendingCardId];
|
setWizardInitialValues(
|
||||||
const headerDraft: MethodCardHeaderDraft = {
|
buildMethodCardWizardInitialValues({
|
||||||
title: meta?.label ?? method?.label ?? cm.confirmModal.title,
|
cardId: pendingCardId,
|
||||||
description:
|
fallbackTitle: method?.label ?? cm.confirmModal.title,
|
||||||
meta?.supportText ??
|
fallbackDescription:
|
||||||
method?.supportText ??
|
method?.supportText ?? cm.confirmModal.description,
|
||||||
cm.confirmModal.description,
|
meta: state.customMethodCardMetaById,
|
||||||
};
|
persistedBlocks: state.customMethodCardFieldBlocksById,
|
||||||
setCustomizeHeaderDraft(headerDraft);
|
draftFieldBlocks,
|
||||||
customizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
|
facetPrefill: pendingDraft
|
||||||
pendingDraft,
|
? {
|
||||||
initialFieldBlocks,
|
group: "conflictManagement",
|
||||||
headerDraft,
|
draft: pendingDraft,
|
||||||
|
headings: cm.sectionHeadings,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
setDraftFieldBlocks(initialFieldBlocks);
|
setWizardCustomizeCardId(pendingCardId);
|
||||||
setModalEditUnlocked(true);
|
setCreateModalOpen(false);
|
||||||
|
setAddCustomWizardOpen(true);
|
||||||
}, [
|
}, [
|
||||||
cm.confirmModal.description,
|
cm.confirmModal.description,
|
||||||
cm.confirmModal.title,
|
cm.confirmModal.title,
|
||||||
|
cm.sectionHeadings,
|
||||||
|
draftFieldBlocks,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
methodById,
|
methodById,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
@@ -381,9 +367,7 @@ export function ConflictManagementScreen() {
|
|||||||
() => conflictManagementPresetFor(newId),
|
() => conflictManagementPresetFor(newId),
|
||||||
);
|
);
|
||||||
const blocksClone = structuredClone(
|
const blocksClone = structuredClone(
|
||||||
modalEditUnlocked &&
|
draftFieldBlocks !== null
|
||||||
draftFieldBlocks !== null &&
|
|
||||||
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById)
|
|
||||||
? draftFieldBlocks
|
? draftFieldBlocks
|
||||||
: cloneMethodCardBlocksForDuplicate(
|
: cloneMethodCardBlocksForDuplicate(
|
||||||
state.customMethodCardFieldBlocksById,
|
state.customMethodCardFieldBlocksById,
|
||||||
@@ -413,13 +397,12 @@ export function ConflictManagementScreen() {
|
|||||||
customizeSnapshotRef.current = null;
|
customizeSnapshotRef.current = null;
|
||||||
setPendingCardId(newId);
|
setPendingCardId(newId);
|
||||||
setPendingDraft(structuredClone(detailsClone));
|
setPendingDraft(structuredClone(detailsClone));
|
||||||
setModalEditUnlocked(false);
|
setDraftFieldBlocks(
|
||||||
setDraftFieldBlocks(null);
|
blocksClone.length > 0 ? structuredClone(blocksClone) : null,
|
||||||
setCustomizeHeaderDraft(null);
|
);
|
||||||
}, [
|
}, [
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
modalEditUnlocked,
|
|
||||||
modalKebabMenu.duplicateTitleSuffix,
|
modalKebabMenu.duplicateTitleSuffix,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
@@ -448,9 +431,7 @@ export function ConflictManagementScreen() {
|
|||||||
() => conflictManagementPresetFor(newId),
|
() => conflictManagementPresetFor(newId),
|
||||||
);
|
);
|
||||||
const blocksClone = structuredClone(
|
const blocksClone = structuredClone(
|
||||||
modalEditUnlocked &&
|
draftFieldBlocks !== null
|
||||||
draftFieldBlocks !== null &&
|
|
||||||
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById)
|
|
||||||
? draftFieldBlocks
|
? draftFieldBlocks
|
||||||
: cloneMethodCardBlocksForDuplicate(
|
: cloneMethodCardBlocksForDuplicate(
|
||||||
state.customMethodCardFieldBlocksById,
|
state.customMethodCardFieldBlocksById,
|
||||||
@@ -480,14 +461,13 @@ export function ConflictManagementScreen() {
|
|||||||
customizeSnapshotRef.current = null;
|
customizeSnapshotRef.current = null;
|
||||||
setPendingCardId(newId);
|
setPendingCardId(newId);
|
||||||
setPendingDraft(structuredClone(detailsClone));
|
setPendingDraft(structuredClone(detailsClone));
|
||||||
setModalEditUnlocked(false);
|
setDraftFieldBlocks(
|
||||||
setDraftFieldBlocks(null);
|
blocksClone.length > 0 ? structuredClone(blocksClone) : null,
|
||||||
setCustomizeHeaderDraft(null);
|
);
|
||||||
}, [
|
}, [
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
methodById,
|
methodById,
|
||||||
modalEditUnlocked,
|
|
||||||
modalKebabMenu.duplicateTitleSuffix,
|
modalKebabMenu.duplicateTitleSuffix,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
@@ -500,7 +480,7 @@ export function ConflictManagementScreen() {
|
|||||||
const kebabMenuItems = useMemo(
|
const kebabMenuItems = useMemo(
|
||||||
() =>
|
() =>
|
||||||
buildCustomRuleModalKebabMenu(modalKebabMenu, {
|
buildCustomRuleModalKebabMenu(modalKebabMenu, {
|
||||||
showCustomize: !modalEditUnlocked,
|
showCustomize: true,
|
||||||
onCustomize: handleCustomize,
|
onCustomize: handleCustomize,
|
||||||
onDuplicate:
|
onDuplicate:
|
||||||
(state.editingPublishedRuleId?.trim() ?? "") !== "" || !pendingCardId
|
(state.editingPublishedRuleId?.trim() ?? "") !== "" || !pendingCardId
|
||||||
@@ -520,7 +500,6 @@ export function ConflictManagementScreen() {
|
|||||||
handleDuplicatePrefabCard,
|
handleDuplicatePrefabCard,
|
||||||
handleRemoveSelectedFromModal,
|
handleRemoveSelectedFromModal,
|
||||||
isSelectedCardModal,
|
isSelectedCardModal,
|
||||||
modalEditUnlocked,
|
|
||||||
modalKebabMenu,
|
modalKebabMenu,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
state.customMethodCardMetaById,
|
state.customMethodCardMetaById,
|
||||||
@@ -539,7 +518,7 @@ export function ConflictManagementScreen() {
|
|||||||
meta?.supportText ??
|
meta?.supportText ??
|
||||||
method?.supportText ??
|
method?.supportText ??
|
||||||
cm.confirmModal.description,
|
cm.confirmModal.description,
|
||||||
nextButtonText: modalEditUnlocked
|
nextButtonText: isSelectedCardModal
|
||||||
? saveLabel
|
? saveLabel
|
||||||
: cm.addApproach.nextButtonText,
|
: cm.addApproach.nextButtonText,
|
||||||
};
|
};
|
||||||
@@ -551,8 +530,14 @@ export function ConflictManagementScreen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCloseAddWizard = useCallback(() => {
|
const handleCloseAddWizard = useCallback(() => {
|
||||||
|
const resumeCardId = wizardCustomizeCardId;
|
||||||
setAddCustomWizardOpen(false);
|
setAddCustomWizardOpen(false);
|
||||||
}, []);
|
setWizardCustomizeCardId(null);
|
||||||
|
setWizardInitialValues(null);
|
||||||
|
if (resumeCardId && pendingCardId === resumeCardId) {
|
||||||
|
setCreateModalOpen(true);
|
||||||
|
}
|
||||||
|
}, [pendingCardId, wizardCustomizeCardId]);
|
||||||
|
|
||||||
const handleFinalizeCustomCard = useCallback(
|
const handleFinalizeCustomCard = useCallback(
|
||||||
({
|
({
|
||||||
@@ -565,6 +550,42 @@ export function ConflictManagementScreen() {
|
|||||||
fieldBlocks: CustomMethodCardFieldBlock[];
|
fieldBlocks: CustomMethodCardFieldBlock[];
|
||||||
}) => {
|
}) => {
|
||||||
markCreateFlowInteraction();
|
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();
|
const id = crypto.randomUUID();
|
||||||
updateState({
|
updateState({
|
||||||
selectedConflictManagementIds: moveFacetSelectionIdToFront(
|
selectedConflictManagementIds: moveFacetSelectionIdToFront(
|
||||||
@@ -587,91 +608,29 @@ export function ConflictManagementScreen() {
|
|||||||
},
|
},
|
||||||
[
|
[
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
|
pendingDraft,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
state.conflictManagementDetailsById,
|
state.conflictManagementDetailsById,
|
||||||
state.customMethodCardFieldBlocksById,
|
state.customMethodCardFieldBlocksById,
|
||||||
state.customMethodCardMetaById,
|
state.customMethodCardMetaById,
|
||||||
updateState,
|
updateState,
|
||||||
|
wizardCustomizeCardId,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCreateModalPrimary = useCallback(() => {
|
const handleCreateModalPrimary = useCallback(() => {
|
||||||
if (!pendingCardId) {
|
if (!pendingCardId) {
|
||||||
handleCreateModalClose();
|
void handleCreateModalClose();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
|
|
||||||
if (selectedIds.includes(pendingCardId)) {
|
const persistWizardBlocks =
|
||||||
if (modalEditUnlocked) {
|
modalUsesWizardFieldBlocksBody && draftFieldBlocks !== null;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (modalEditUnlocked) {
|
if (selectedIds.includes(pendingCardId)) {
|
||||||
if (!customizeHeaderDraft) {
|
if (persistWizardBlocks) {
|
||||||
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({
|
updateState({
|
||||||
customMethodCardMetaById: nextMeta,
|
|
||||||
customMethodCardFieldBlocksById: {
|
customMethodCardFieldBlocksById: {
|
||||||
...(state.customMethodCardFieldBlocksById ?? {}),
|
...(state.customMethodCardFieldBlocksById ?? {}),
|
||||||
[pendingCardId]: structuredClone(draftFieldBlocks ?? []),
|
[pendingCardId]: structuredClone(draftFieldBlocks ?? []),
|
||||||
@@ -679,22 +638,27 @@ export function ConflictManagementScreen() {
|
|||||||
});
|
});
|
||||||
} else if (pendingDraft) {
|
} else if (pendingDraft) {
|
||||||
updateState({
|
updateState({
|
||||||
customMethodCardMetaById: nextMeta,
|
|
||||||
conflictManagementDetailsById: {
|
conflictManagementDetailsById: {
|
||||||
...(state.conflictManagementDetailsById ?? {}),
|
...(state.conflictManagementDetailsById ?? {}),
|
||||||
[pendingCardId]: pendingDraft,
|
[pendingCardId]: pendingDraft,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
customizeSnapshotRef.current = null;
|
if (pendingDraft) {
|
||||||
setModalEditUnlocked(false);
|
customizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
|
||||||
setDraftFieldBlocks(null);
|
pendingDraft,
|
||||||
setCustomizeHeaderDraft(null);
|
persistWizardBlocks ? draftFieldBlocks : null,
|
||||||
|
customizeSnapshotRef.current?.headerDraft ?? {
|
||||||
|
title: "",
|
||||||
|
description: "",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!pendingDraft) {
|
if (!pendingDraft) {
|
||||||
handleCreateModalClose();
|
void handleCreateModalClose();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updateState({
|
updateState({
|
||||||
@@ -706,15 +670,23 @@ export function ConflictManagementScreen() {
|
|||||||
...(state.conflictManagementDetailsById ?? {}),
|
...(state.conflictManagementDetailsById ?? {}),
|
||||||
[pendingCardId]: pendingDraft,
|
[pendingCardId]: pendingDraft,
|
||||||
},
|
},
|
||||||
|
...(persistWizardBlocks
|
||||||
|
? {
|
||||||
|
customMethodCardFieldBlocksById: {
|
||||||
|
...(state.customMethodCardFieldBlocksById ?? {}),
|
||||||
|
[pendingCardId]: structuredClone(draftFieldBlocks ?? []),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
});
|
});
|
||||||
pendingEphemeralDuplicateIdRef.current = null;
|
pendingEphemeralDuplicateIdRef.current = null;
|
||||||
handleCreateModalClose();
|
customizeSnapshotRef.current = null;
|
||||||
|
void handleCreateModalClose();
|
||||||
}, [
|
}, [
|
||||||
customizeHeaderDraft,
|
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
handleCreateModalClose,
|
handleCreateModalClose,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
modalEditUnlocked,
|
modalUsesWizardFieldBlocksBody,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
@@ -765,33 +737,10 @@ export function ConflictManagementScreen() {
|
|||||||
<Create
|
<Create
|
||||||
isOpen={createModalOpen}
|
isOpen={createModalOpen}
|
||||||
onClose={handleCreateModalClose}
|
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}
|
onNext={handleCreateModalPrimary}
|
||||||
title={modalConfig.title}
|
title={modalConfig.title}
|
||||||
description={modalConfig.description}
|
description={modalConfig.description}
|
||||||
nextButtonText={modalConfig.nextButtonText}
|
nextButtonText={modalConfig.nextButtonText}
|
||||||
showBackButton={modalEditUnlocked}
|
|
||||||
onBack={handleCancelCustomize}
|
|
||||||
backButtonText={modalKebabMenu.cancelCustomize}
|
|
||||||
showNextButton={showMethodModalPrimary}
|
showNextButton={showMethodModalPrimary}
|
||||||
backdropVariant="blurredYellow"
|
backdropVariant="blurredYellow"
|
||||||
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
|
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
|
||||||
@@ -804,14 +753,14 @@ export function ConflictManagementScreen() {
|
|||||||
cardId={pendingCardId}
|
cardId={pendingCardId}
|
||||||
blocksById={state.customMethodCardFieldBlocksById}
|
blocksById={state.customMethodCardFieldBlocksById}
|
||||||
blocksOverride={
|
blocksOverride={
|
||||||
modalEditUnlocked && draftFieldBlocks !== null
|
draftFieldBlocks !== null ? draftFieldBlocks : undefined
|
||||||
? draftFieldBlocks
|
|
||||||
: undefined
|
|
||||||
}
|
}
|
||||||
policyMeta={state.customMethodCardMetaById?.[pendingCardId]}
|
policyMeta={state.customMethodCardMetaById?.[pendingCardId]}
|
||||||
showPolicyContentLockupWhenNoBlocks={!modalEditUnlocked}
|
showPolicyContentLockupWhenNoBlocks={
|
||||||
|
draftFieldBlocks === null || draftFieldBlocks.length === 0
|
||||||
|
}
|
||||||
onFieldBlocksChange={
|
onFieldBlocksChange={
|
||||||
fieldsLocked
|
draftFieldBlocks === null
|
||||||
? undefined
|
? undefined
|
||||||
: (next) => setDraftFieldBlocks(next)
|
: (next) => setDraftFieldBlocks(next)
|
||||||
}
|
}
|
||||||
@@ -820,7 +769,6 @@ export function ConflictManagementScreen() {
|
|||||||
<ConflictManagementEditFields
|
<ConflictManagementEditFields
|
||||||
value={pendingDraft}
|
value={pendingDraft}
|
||||||
onChange={handleDraftChange}
|
onChange={handleDraftChange}
|
||||||
readOnly={fieldsLocked}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
) : null}
|
) : null}
|
||||||
@@ -829,6 +777,7 @@ export function ConflictManagementScreen() {
|
|||||||
<CustomMethodCardWizard
|
<CustomMethodCardWizard
|
||||||
isOpen={addCustomWizardOpen}
|
isOpen={addCustomWizardOpen}
|
||||||
onClose={handleCloseAddWizard}
|
onClose={handleCloseAddWizard}
|
||||||
|
initialValues={wizardInitialValues}
|
||||||
onFinalize={handleFinalizeCustomCard}
|
onFinalize={handleFinalizeCustomCard}
|
||||||
onPersistCustomUploadFile={(file) =>
|
onPersistCustomUploadFile={(file) =>
|
||||||
uploadCreateFlowFile(file, "customMethodAttachment")
|
uploadCreateFlowFile(file, "customMethodAttachment")
|
||||||
|
|||||||
@@ -51,20 +51,20 @@ import type { MembershipMethodDetailEntry } from "../../types";
|
|||||||
import CustomMethodCardModalBody from "../../components/CustomMethodCardModalBody";
|
import CustomMethodCardModalBody from "../../components/CustomMethodCardModalBody";
|
||||||
import { buildCustomRuleModalKebabMenu } from "../../components/customRuleModalKebabMenu";
|
import { buildCustomRuleModalKebabMenu } from "../../components/customRuleModalKebabMenu";
|
||||||
import { methodCardMetaWithCustomizeHeader } from "../../../../../lib/create/methodCardCustomizeMetaPatch";
|
import { methodCardMetaWithCustomizeHeader } from "../../../../../lib/create/methodCardCustomizeMetaPatch";
|
||||||
|
import { buildMethodCardWizardInitialValues } from "../../../../../lib/create/methodCardWizardPrefill";
|
||||||
|
import type { MethodCardWizardInitialValues } from "../../../../../lib/create/methodCardWizardPrefill";
|
||||||
import {
|
import {
|
||||||
captureMethodCardCustomizeSnapshot,
|
captureMethodCardCustomizeSnapshot,
|
||||||
type MethodCardCustomizeSnapshot,
|
type MethodCardCustomizeSnapshot,
|
||||||
type MethodCardHeaderDraft,
|
type MethodCardHeaderDraft,
|
||||||
} from "../../../../../lib/create/methodCardCustomizeSession";
|
} from "../../../../../lib/create/methodCardCustomizeSession";
|
||||||
import MethodCardCustomizeModalHeader from "../../components/MethodCardCustomizeModalHeader";
|
|
||||||
|
|
||||||
export function MembershipMethodsScreen() {
|
export function MembershipMethodsScreen() {
|
||||||
const m = useMessages();
|
const m = useMessages();
|
||||||
const mem = m.create.customRule.membership;
|
const mem = m.create.customRule.membership;
|
||||||
const modalKebabMenu = m.create.customRule.modalKebabMenu;
|
const modalKebabMenu = m.create.customRule.modalKebabMenu;
|
||||||
const mdUp = useCreateFlowMdUp();
|
const mdUp = useCreateFlowMdUp();
|
||||||
const { confirmDiscard, confirmDirtyCustomizeCancel, confirmDialog } =
|
const { confirmDiscard, confirmDialog } = useDiscardCustomizeConfirm();
|
||||||
useDiscardCustomizeConfirm();
|
|
||||||
const { state, updateState, replaceState, markCreateFlowInteraction } =
|
const { state, updateState, replaceState, markCreateFlowInteraction } =
|
||||||
useCreateFlow();
|
useCreateFlow();
|
||||||
const pendingEphemeralDuplicateIdRef = useRef<string | null>(null);
|
const pendingEphemeralDuplicateIdRef = useRef<string | null>(null);
|
||||||
@@ -77,12 +77,14 @@ export function MembershipMethodsScreen() {
|
|||||||
const [pendingDraft, setPendingDraft] =
|
const [pendingDraft, setPendingDraft] =
|
||||||
useState<MembershipMethodDetailEntry | null>(null);
|
useState<MembershipMethodDetailEntry | null>(null);
|
||||||
const [addCustomWizardOpen, setAddCustomWizardOpen] = useState(false);
|
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<
|
const [draftFieldBlocks, setDraftFieldBlocks] = useState<
|
||||||
CustomMethodCardFieldBlock[] | null
|
CustomMethodCardFieldBlock[] | null
|
||||||
>(null);
|
>(null);
|
||||||
const [customizeHeaderDraft, setCustomizeHeaderDraft] =
|
|
||||||
useState<MethodCardHeaderDraft | null>(null);
|
|
||||||
|
|
||||||
const selectedIds = state.selectedMembershipMethodIds ?? [];
|
const selectedIds = state.selectedMembershipMethodIds ?? [];
|
||||||
|
|
||||||
@@ -105,6 +107,8 @@ export function MembershipMethodsScreen() {
|
|||||||
|
|
||||||
const handleOpenAddWizard = useCallback(() => {
|
const handleOpenAddWizard = useCallback(() => {
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
|
setWizardCustomizeCardId(null);
|
||||||
|
setWizardInitialValues(null);
|
||||||
setAddCustomWizardOpen(true);
|
setAddCustomWizardOpen(true);
|
||||||
}, [markCreateFlowInteraction]);
|
}, [markCreateFlowInteraction]);
|
||||||
|
|
||||||
@@ -142,15 +146,40 @@ export function MembershipMethodsScreen() {
|
|||||||
const handleCardClick = useCallback(
|
const handleCardClick = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
customizeSnapshotRef.current = null;
|
const draft = seedDraft(id);
|
||||||
setModalEditUnlocked(false);
|
const persistedBlocks = state.customMethodCardFieldBlocksById?.[id];
|
||||||
setDraftFieldBlocks(null);
|
const initialBlocks =
|
||||||
setCustomizeHeaderDraft(null);
|
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);
|
setPendingCardId(id);
|
||||||
setPendingDraft(seedDraft(id));
|
setPendingDraft(draft);
|
||||||
|
setDraftFieldBlocks(initialBlocks);
|
||||||
setCreateModalOpen(true);
|
setCreateModalOpen(true);
|
||||||
},
|
},
|
||||||
[markCreateFlowInteraction, seedDraft],
|
[
|
||||||
|
mem.confirmModal.description,
|
||||||
|
mem.confirmModal.title,
|
||||||
|
markCreateFlowInteraction,
|
||||||
|
methodById,
|
||||||
|
seedDraft,
|
||||||
|
state.customMethodCardFieldBlocksById,
|
||||||
|
state.customMethodCardMetaById,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDraftChange = useCallback(
|
const handleDraftChange = useCallback(
|
||||||
@@ -163,9 +192,7 @@ export function MembershipMethodsScreen() {
|
|||||||
|
|
||||||
const isSelectedCardModal =
|
const isSelectedCardModal =
|
||||||
pendingCardId !== null && selectedIds.includes(pendingCardId);
|
pendingCardId !== null && selectedIds.includes(pendingCardId);
|
||||||
const fieldsLocked = !modalEditUnlocked;
|
const showMethodModalPrimary = true;
|
||||||
|
|
||||||
const showMethodModalPrimary = !isSelectedCardModal || modalEditUnlocked;
|
|
||||||
|
|
||||||
const customFacetDetailsMatchPreset = useMemo(() => {
|
const customFacetDetailsMatchPreset = useMemo(() => {
|
||||||
if (!pendingCardId || !pendingDraft) return false;
|
if (!pendingCardId || !pendingDraft) return false;
|
||||||
@@ -187,7 +214,7 @@ export function MembershipMethodsScreen() {
|
|||||||
methodId: pendingCardId,
|
methodId: pendingCardId,
|
||||||
meta: state.customMethodCardMetaById,
|
meta: state.customMethodCardMetaById,
|
||||||
fieldBlocksById: state.customMethodCardFieldBlocksById,
|
fieldBlocksById: state.customMethodCardFieldBlocksById,
|
||||||
modalEditUnlocked,
|
modalEditUnlocked: false,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
customFacetDetailsMatchPreset,
|
customFacetDetailsMatchPreset,
|
||||||
}),
|
}),
|
||||||
@@ -195,7 +222,6 @@ export function MembershipMethodsScreen() {
|
|||||||
[
|
[
|
||||||
customFacetDetailsMatchPreset,
|
customFacetDetailsMatchPreset,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
modalEditUnlocked,
|
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
state.customMethodCardFieldBlocksById,
|
state.customMethodCardFieldBlocksById,
|
||||||
state.customMethodCardMetaById,
|
state.customMethodCardMetaById,
|
||||||
@@ -205,11 +231,11 @@ export function MembershipMethodsScreen() {
|
|||||||
const handleCreateModalClose = useCallback(async () => {
|
const handleCreateModalClose = useCallback(async () => {
|
||||||
if (
|
if (
|
||||||
!(await confirmDiscard(
|
!(await confirmDiscard(
|
||||||
modalEditUnlocked,
|
true,
|
||||||
customizeSnapshotRef.current,
|
customizeSnapshotRef.current,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
customizeHeaderDraft,
|
customizeSnapshotRef.current?.headerDraft ?? null,
|
||||||
))
|
))
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@@ -237,53 +263,14 @@ export function MembershipMethodsScreen() {
|
|||||||
setCreateModalOpen(false);
|
setCreateModalOpen(false);
|
||||||
setPendingCardId(null);
|
setPendingCardId(null);
|
||||||
setPendingDraft(null);
|
setPendingDraft(null);
|
||||||
setModalEditUnlocked(false);
|
|
||||||
setDraftFieldBlocks(null);
|
setDraftFieldBlocks(null);
|
||||||
setCustomizeHeaderDraft(null);
|
|
||||||
}, [
|
}, [
|
||||||
confirmDiscard,
|
confirmDiscard,
|
||||||
customizeHeaderDraft,
|
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
modalEditUnlocked,
|
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
replaceState,
|
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 () => {
|
const handleRemoveSelectedFromModal = useCallback(async () => {
|
||||||
if (!pendingCardId || !selectedIds.includes(pendingCardId)) {
|
if (!pendingCardId || !selectedIds.includes(pendingCardId)) {
|
||||||
return;
|
return;
|
||||||
@@ -291,11 +278,11 @@ export function MembershipMethodsScreen() {
|
|||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
if (
|
if (
|
||||||
!(await confirmDiscard(
|
!(await confirmDiscard(
|
||||||
modalEditUnlocked,
|
true,
|
||||||
customizeSnapshotRef.current,
|
customizeSnapshotRef.current,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
customizeHeaderDraft,
|
customizeSnapshotRef.current?.headerDraft ?? null,
|
||||||
))
|
))
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@@ -307,11 +294,9 @@ export function MembershipMethodsScreen() {
|
|||||||
await handleCreateModalClose();
|
await handleCreateModalClose();
|
||||||
}, [
|
}, [
|
||||||
confirmDiscard,
|
confirmDiscard,
|
||||||
customizeHeaderDraft,
|
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
handleCreateModalClose,
|
handleCreateModalClose,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
modalEditUnlocked,
|
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
@@ -321,35 +306,36 @@ export function MembershipMethodsScreen() {
|
|||||||
|
|
||||||
const handleCustomize = useCallback(() => {
|
const handleCustomize = useCallback(() => {
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
if (!pendingDraft || !pendingCardId) {
|
if (!pendingCardId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const initialFieldBlocks =
|
|
||||||
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById)
|
|
||||||
? structuredClone(
|
|
||||||
state.customMethodCardFieldBlocksById?.[pendingCardId] ?? [],
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
const method = methodById.get(pendingCardId);
|
const method = methodById.get(pendingCardId);
|
||||||
const meta = state.customMethodCardMetaById?.[pendingCardId];
|
setWizardInitialValues(
|
||||||
const headerDraft: MethodCardHeaderDraft = {
|
buildMethodCardWizardInitialValues({
|
||||||
title: meta?.label ?? method?.label ?? mem.confirmModal.title,
|
cardId: pendingCardId,
|
||||||
description:
|
fallbackTitle: method?.label ?? mem.confirmModal.title,
|
||||||
meta?.supportText ??
|
fallbackDescription:
|
||||||
method?.supportText ??
|
method?.supportText ?? mem.confirmModal.description,
|
||||||
mem.confirmModal.description,
|
meta: state.customMethodCardMetaById,
|
||||||
};
|
persistedBlocks: state.customMethodCardFieldBlocksById,
|
||||||
setCustomizeHeaderDraft(headerDraft);
|
draftFieldBlocks,
|
||||||
customizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
|
facetPrefill: pendingDraft
|
||||||
pendingDraft,
|
? {
|
||||||
initialFieldBlocks,
|
group: "membership",
|
||||||
headerDraft,
|
draft: pendingDraft,
|
||||||
|
headings: mem.sectionHeadings,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
setDraftFieldBlocks(initialFieldBlocks);
|
setWizardCustomizeCardId(pendingCardId);
|
||||||
setModalEditUnlocked(true);
|
setCreateModalOpen(false);
|
||||||
|
setAddCustomWizardOpen(true);
|
||||||
}, [
|
}, [
|
||||||
mem.confirmModal.description,
|
mem.confirmModal.description,
|
||||||
mem.confirmModal.title,
|
mem.confirmModal.title,
|
||||||
|
mem.sectionHeadings,
|
||||||
|
draftFieldBlocks,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
methodById,
|
methodById,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
@@ -374,9 +360,7 @@ export function MembershipMethodsScreen() {
|
|||||||
() => membershipPresetFor(newId),
|
() => membershipPresetFor(newId),
|
||||||
);
|
);
|
||||||
const blocksClone = structuredClone(
|
const blocksClone = structuredClone(
|
||||||
modalEditUnlocked &&
|
draftFieldBlocks !== null
|
||||||
draftFieldBlocks !== null &&
|
|
||||||
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById)
|
|
||||||
? draftFieldBlocks
|
? draftFieldBlocks
|
||||||
: cloneMethodCardBlocksForDuplicate(
|
: cloneMethodCardBlocksForDuplicate(
|
||||||
state.customMethodCardFieldBlocksById,
|
state.customMethodCardFieldBlocksById,
|
||||||
@@ -406,13 +390,12 @@ export function MembershipMethodsScreen() {
|
|||||||
customizeSnapshotRef.current = null;
|
customizeSnapshotRef.current = null;
|
||||||
setPendingCardId(newId);
|
setPendingCardId(newId);
|
||||||
setPendingDraft(structuredClone(detailsClone));
|
setPendingDraft(structuredClone(detailsClone));
|
||||||
setModalEditUnlocked(false);
|
setDraftFieldBlocks(
|
||||||
setDraftFieldBlocks(null);
|
blocksClone.length > 0 ? structuredClone(blocksClone) : null,
|
||||||
setCustomizeHeaderDraft(null);
|
);
|
||||||
}, [
|
}, [
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
modalEditUnlocked,
|
|
||||||
modalKebabMenu.duplicateTitleSuffix,
|
modalKebabMenu.duplicateTitleSuffix,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
@@ -441,9 +424,7 @@ export function MembershipMethodsScreen() {
|
|||||||
() => membershipPresetFor(newId),
|
() => membershipPresetFor(newId),
|
||||||
);
|
);
|
||||||
const blocksClone = structuredClone(
|
const blocksClone = structuredClone(
|
||||||
modalEditUnlocked &&
|
draftFieldBlocks !== null
|
||||||
draftFieldBlocks !== null &&
|
|
||||||
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById)
|
|
||||||
? draftFieldBlocks
|
? draftFieldBlocks
|
||||||
: cloneMethodCardBlocksForDuplicate(
|
: cloneMethodCardBlocksForDuplicate(
|
||||||
state.customMethodCardFieldBlocksById,
|
state.customMethodCardFieldBlocksById,
|
||||||
@@ -473,14 +454,13 @@ export function MembershipMethodsScreen() {
|
|||||||
customizeSnapshotRef.current = null;
|
customizeSnapshotRef.current = null;
|
||||||
setPendingCardId(newId);
|
setPendingCardId(newId);
|
||||||
setPendingDraft(structuredClone(detailsClone));
|
setPendingDraft(structuredClone(detailsClone));
|
||||||
setModalEditUnlocked(false);
|
setDraftFieldBlocks(
|
||||||
setDraftFieldBlocks(null);
|
blocksClone.length > 0 ? structuredClone(blocksClone) : null,
|
||||||
setCustomizeHeaderDraft(null);
|
);
|
||||||
}, [
|
}, [
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
methodById,
|
methodById,
|
||||||
modalEditUnlocked,
|
|
||||||
modalKebabMenu.duplicateTitleSuffix,
|
modalKebabMenu.duplicateTitleSuffix,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
@@ -493,7 +473,7 @@ export function MembershipMethodsScreen() {
|
|||||||
const kebabMenuItems = useMemo(
|
const kebabMenuItems = useMemo(
|
||||||
() =>
|
() =>
|
||||||
buildCustomRuleModalKebabMenu(modalKebabMenu, {
|
buildCustomRuleModalKebabMenu(modalKebabMenu, {
|
||||||
showCustomize: !modalEditUnlocked,
|
showCustomize: true,
|
||||||
onCustomize: handleCustomize,
|
onCustomize: handleCustomize,
|
||||||
onDuplicate:
|
onDuplicate:
|
||||||
(state.editingPublishedRuleId?.trim() ?? "") !== "" || !pendingCardId
|
(state.editingPublishedRuleId?.trim() ?? "") !== "" || !pendingCardId
|
||||||
@@ -513,7 +493,6 @@ export function MembershipMethodsScreen() {
|
|||||||
handleDuplicatePrefabCard,
|
handleDuplicatePrefabCard,
|
||||||
handleRemoveSelectedFromModal,
|
handleRemoveSelectedFromModal,
|
||||||
isSelectedCardModal,
|
isSelectedCardModal,
|
||||||
modalEditUnlocked,
|
|
||||||
modalKebabMenu,
|
modalKebabMenu,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
state.customMethodCardMetaById,
|
state.customMethodCardMetaById,
|
||||||
@@ -532,7 +511,7 @@ export function MembershipMethodsScreen() {
|
|||||||
meta?.supportText ??
|
meta?.supportText ??
|
||||||
method?.supportText ??
|
method?.supportText ??
|
||||||
mem.confirmModal.description,
|
mem.confirmModal.description,
|
||||||
nextButtonText: modalEditUnlocked
|
nextButtonText: isSelectedCardModal
|
||||||
? saveLabel
|
? saveLabel
|
||||||
: mem.addPlatform.nextButtonText,
|
: mem.addPlatform.nextButtonText,
|
||||||
};
|
};
|
||||||
@@ -544,8 +523,14 @@ export function MembershipMethodsScreen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCloseAddWizard = useCallback(() => {
|
const handleCloseAddWizard = useCallback(() => {
|
||||||
|
const resumeCardId = wizardCustomizeCardId;
|
||||||
setAddCustomWizardOpen(false);
|
setAddCustomWizardOpen(false);
|
||||||
}, []);
|
setWizardCustomizeCardId(null);
|
||||||
|
setWizardInitialValues(null);
|
||||||
|
if (resumeCardId && pendingCardId === resumeCardId) {
|
||||||
|
setCreateModalOpen(true);
|
||||||
|
}
|
||||||
|
}, [pendingCardId, wizardCustomizeCardId]);
|
||||||
|
|
||||||
const handleFinalizeCustomCard = useCallback(
|
const handleFinalizeCustomCard = useCallback(
|
||||||
({
|
({
|
||||||
@@ -558,6 +543,42 @@ export function MembershipMethodsScreen() {
|
|||||||
fieldBlocks: CustomMethodCardFieldBlock[];
|
fieldBlocks: CustomMethodCardFieldBlock[];
|
||||||
}) => {
|
}) => {
|
||||||
markCreateFlowInteraction();
|
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();
|
const id = crypto.randomUUID();
|
||||||
updateState({
|
updateState({
|
||||||
selectedMembershipMethodIds: moveFacetSelectionIdToFront(
|
selectedMembershipMethodIds: moveFacetSelectionIdToFront(
|
||||||
@@ -580,91 +601,29 @@ export function MembershipMethodsScreen() {
|
|||||||
},
|
},
|
||||||
[
|
[
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
|
pendingDraft,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
state.customMethodCardFieldBlocksById,
|
state.customMethodCardFieldBlocksById,
|
||||||
state.customMethodCardMetaById,
|
state.customMethodCardMetaById,
|
||||||
state.membershipMethodDetailsById,
|
state.membershipMethodDetailsById,
|
||||||
updateState,
|
updateState,
|
||||||
|
wizardCustomizeCardId,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCreateModalPrimary = useCallback(() => {
|
const handleCreateModalPrimary = useCallback(() => {
|
||||||
if (!pendingCardId) {
|
if (!pendingCardId) {
|
||||||
handleCreateModalClose();
|
void handleCreateModalClose();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
|
|
||||||
if (selectedIds.includes(pendingCardId)) {
|
const persistWizardBlocks =
|
||||||
if (modalEditUnlocked) {
|
modalUsesWizardFieldBlocksBody && draftFieldBlocks !== null;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (modalEditUnlocked) {
|
if (selectedIds.includes(pendingCardId)) {
|
||||||
if (!customizeHeaderDraft) {
|
if (persistWizardBlocks) {
|
||||||
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({
|
updateState({
|
||||||
customMethodCardMetaById: nextMeta,
|
|
||||||
customMethodCardFieldBlocksById: {
|
customMethodCardFieldBlocksById: {
|
||||||
...(state.customMethodCardFieldBlocksById ?? {}),
|
...(state.customMethodCardFieldBlocksById ?? {}),
|
||||||
[pendingCardId]: structuredClone(draftFieldBlocks ?? []),
|
[pendingCardId]: structuredClone(draftFieldBlocks ?? []),
|
||||||
@@ -672,22 +631,27 @@ export function MembershipMethodsScreen() {
|
|||||||
});
|
});
|
||||||
} else if (pendingDraft) {
|
} else if (pendingDraft) {
|
||||||
updateState({
|
updateState({
|
||||||
customMethodCardMetaById: nextMeta,
|
|
||||||
membershipMethodDetailsById: {
|
membershipMethodDetailsById: {
|
||||||
...(state.membershipMethodDetailsById ?? {}),
|
...(state.membershipMethodDetailsById ?? {}),
|
||||||
[pendingCardId]: pendingDraft,
|
[pendingCardId]: pendingDraft,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
customizeSnapshotRef.current = null;
|
if (pendingDraft) {
|
||||||
setModalEditUnlocked(false);
|
customizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
|
||||||
setDraftFieldBlocks(null);
|
pendingDraft,
|
||||||
setCustomizeHeaderDraft(null);
|
persistWizardBlocks ? draftFieldBlocks : null,
|
||||||
|
customizeSnapshotRef.current?.headerDraft ?? {
|
||||||
|
title: "",
|
||||||
|
description: "",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!pendingDraft) {
|
if (!pendingDraft) {
|
||||||
handleCreateModalClose();
|
void handleCreateModalClose();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updateState({
|
updateState({
|
||||||
@@ -699,15 +663,23 @@ export function MembershipMethodsScreen() {
|
|||||||
...(state.membershipMethodDetailsById ?? {}),
|
...(state.membershipMethodDetailsById ?? {}),
|
||||||
[pendingCardId]: pendingDraft,
|
[pendingCardId]: pendingDraft,
|
||||||
},
|
},
|
||||||
|
...(persistWizardBlocks
|
||||||
|
? {
|
||||||
|
customMethodCardFieldBlocksById: {
|
||||||
|
...(state.customMethodCardFieldBlocksById ?? {}),
|
||||||
|
[pendingCardId]: structuredClone(draftFieldBlocks ?? []),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
});
|
});
|
||||||
pendingEphemeralDuplicateIdRef.current = null;
|
pendingEphemeralDuplicateIdRef.current = null;
|
||||||
handleCreateModalClose();
|
customizeSnapshotRef.current = null;
|
||||||
|
void handleCreateModalClose();
|
||||||
}, [
|
}, [
|
||||||
customizeHeaderDraft,
|
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
handleCreateModalClose,
|
handleCreateModalClose,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
modalEditUnlocked,
|
modalUsesWizardFieldBlocksBody,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
@@ -758,33 +730,10 @@ export function MembershipMethodsScreen() {
|
|||||||
<Create
|
<Create
|
||||||
isOpen={createModalOpen}
|
isOpen={createModalOpen}
|
||||||
onClose={handleCreateModalClose}
|
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}
|
onNext={handleCreateModalPrimary}
|
||||||
title={modalConfig.title}
|
title={modalConfig.title}
|
||||||
description={modalConfig.description}
|
description={modalConfig.description}
|
||||||
nextButtonText={modalConfig.nextButtonText}
|
nextButtonText={modalConfig.nextButtonText}
|
||||||
showBackButton={modalEditUnlocked}
|
|
||||||
onBack={handleCancelCustomize}
|
|
||||||
backButtonText={modalKebabMenu.cancelCustomize}
|
|
||||||
showNextButton={showMethodModalPrimary}
|
showNextButton={showMethodModalPrimary}
|
||||||
backdropVariant="blurredYellow"
|
backdropVariant="blurredYellow"
|
||||||
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
|
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
|
||||||
@@ -797,14 +746,14 @@ export function MembershipMethodsScreen() {
|
|||||||
cardId={pendingCardId}
|
cardId={pendingCardId}
|
||||||
blocksById={state.customMethodCardFieldBlocksById}
|
blocksById={state.customMethodCardFieldBlocksById}
|
||||||
blocksOverride={
|
blocksOverride={
|
||||||
modalEditUnlocked && draftFieldBlocks !== null
|
draftFieldBlocks !== null ? draftFieldBlocks : undefined
|
||||||
? draftFieldBlocks
|
|
||||||
: undefined
|
|
||||||
}
|
}
|
||||||
policyMeta={state.customMethodCardMetaById?.[pendingCardId]}
|
policyMeta={state.customMethodCardMetaById?.[pendingCardId]}
|
||||||
showPolicyContentLockupWhenNoBlocks={!modalEditUnlocked}
|
showPolicyContentLockupWhenNoBlocks={
|
||||||
|
draftFieldBlocks === null || draftFieldBlocks.length === 0
|
||||||
|
}
|
||||||
onFieldBlocksChange={
|
onFieldBlocksChange={
|
||||||
fieldsLocked
|
draftFieldBlocks === null
|
||||||
? undefined
|
? undefined
|
||||||
: (next) => setDraftFieldBlocks(next)
|
: (next) => setDraftFieldBlocks(next)
|
||||||
}
|
}
|
||||||
@@ -813,7 +762,6 @@ export function MembershipMethodsScreen() {
|
|||||||
<MembershipMethodEditFields
|
<MembershipMethodEditFields
|
||||||
value={pendingDraft}
|
value={pendingDraft}
|
||||||
onChange={handleDraftChange}
|
onChange={handleDraftChange}
|
||||||
readOnly={fieldsLocked}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
) : null}
|
) : null}
|
||||||
@@ -822,6 +770,7 @@ export function MembershipMethodsScreen() {
|
|||||||
<CustomMethodCardWizard
|
<CustomMethodCardWizard
|
||||||
isOpen={addCustomWizardOpen}
|
isOpen={addCustomWizardOpen}
|
||||||
onClose={handleCloseAddWizard}
|
onClose={handleCloseAddWizard}
|
||||||
|
initialValues={wizardInitialValues}
|
||||||
onFinalize={handleFinalizeCustomCard}
|
onFinalize={handleFinalizeCustomCard}
|
||||||
onPersistCustomUploadFile={(file) =>
|
onPersistCustomUploadFile={(file) =>
|
||||||
uploadCreateFlowFile(file, "customMethodAttachment")
|
uploadCreateFlowFile(file, "customMethodAttachment")
|
||||||
|
|||||||
@@ -98,10 +98,10 @@ export function FinalReviewScreen({
|
|||||||
* Two modals coexist on this screen:
|
* Two modals coexist on this screen:
|
||||||
*
|
*
|
||||||
* - {@link FinalReviewChipEditModal} — core values + method chips: kebab
|
* - {@link FinalReviewChipEditModal} — core values + method chips: kebab
|
||||||
* Customize / Remove; values also offer Duplicate under the five-chip cap.
|
* Remove; values also offer Duplicate under the five-chip cap; method chips
|
||||||
* Save respects the same unlock/dirty rules as the facet create modals;
|
* offer Customize (prefilled custom-policy wizard). Fields are editable on
|
||||||
* writes `{group}DetailsById`, snapshot label (values), `customMethodCardMetaById`,
|
* open. Save writes `{group}DetailsById` and field blocks; wizard Finalize
|
||||||
* and field blocks on Save.
|
* also writes `customMethodCardMetaById`.
|
||||||
* - {@link TemplateChipDetailModal} — read-only fallback for chips we
|
* - {@link TemplateChipDetailModal} — read-only fallback for chips we
|
||||||
* can't map to an override key (e.g. template body entries on the
|
* can't map to an override key (e.g. template body entries on the
|
||||||
* "Use without changes" path where no preset matches the title).
|
* "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 CustomMethodCardModalBody from "../../components/CustomMethodCardModalBody";
|
||||||
import { buildCustomRuleModalKebabMenu } from "../../components/customRuleModalKebabMenu";
|
import { buildCustomRuleModalKebabMenu } from "../../components/customRuleModalKebabMenu";
|
||||||
import { methodCardMetaWithCustomizeHeader } from "../../../../../lib/create/methodCardCustomizeMetaPatch";
|
import { methodCardMetaWithCustomizeHeader } from "../../../../../lib/create/methodCardCustomizeMetaPatch";
|
||||||
|
import { buildMethodCardWizardInitialValues } from "../../../../../lib/create/methodCardWizardPrefill";
|
||||||
|
import type { MethodCardWizardInitialValues } from "../../../../../lib/create/methodCardWizardPrefill";
|
||||||
import {
|
import {
|
||||||
captureMethodCardCustomizeSnapshot,
|
captureMethodCardCustomizeSnapshot,
|
||||||
type MethodCardCustomizeSnapshot,
|
type MethodCardCustomizeSnapshot,
|
||||||
type MethodCardHeaderDraft,
|
type MethodCardHeaderDraft,
|
||||||
} from "../../../../../lib/create/methodCardCustomizeSession";
|
} from "../../../../../lib/create/methodCardCustomizeSession";
|
||||||
import MethodCardCustomizeModalHeader from "../../components/MethodCardCustomizeModalHeader";
|
|
||||||
|
|
||||||
export function DecisionApproachesScreen() {
|
export function DecisionApproachesScreen() {
|
||||||
const m = useMessages();
|
const m = useMessages();
|
||||||
const da = m.create.customRule.decisionApproaches;
|
const da = m.create.customRule.decisionApproaches;
|
||||||
const modalKebabMenu = m.create.customRule.modalKebabMenu;
|
const modalKebabMenu = m.create.customRule.modalKebabMenu;
|
||||||
const mdUp = useCreateFlowMdUp();
|
const mdUp = useCreateFlowMdUp();
|
||||||
const { confirmDiscard, confirmDirtyCustomizeCancel, confirmDialog } =
|
const { confirmDiscard, confirmDialog } = useDiscardCustomizeConfirm();
|
||||||
useDiscardCustomizeConfirm();
|
|
||||||
const { state, updateState, replaceState, markCreateFlowInteraction } =
|
const { state, updateState, replaceState, markCreateFlowInteraction } =
|
||||||
useCreateFlow();
|
useCreateFlow();
|
||||||
const pendingEphemeralDuplicateIdRef = useRef<string | null>(null);
|
const pendingEphemeralDuplicateIdRef = useRef<string | null>(null);
|
||||||
@@ -80,12 +80,14 @@ export function DecisionApproachesScreen() {
|
|||||||
const [pendingDraft, setPendingDraft] =
|
const [pendingDraft, setPendingDraft] =
|
||||||
useState<DecisionApproachDetailEntry | null>(null);
|
useState<DecisionApproachDetailEntry | null>(null);
|
||||||
const [addCustomWizardOpen, setAddCustomWizardOpen] = useState(false);
|
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<
|
const [draftFieldBlocks, setDraftFieldBlocks] = useState<
|
||||||
CustomMethodCardFieldBlock[] | null
|
CustomMethodCardFieldBlock[] | null
|
||||||
>(null);
|
>(null);
|
||||||
const [customizeHeaderDraft, setCustomizeHeaderDraft] =
|
|
||||||
useState<MethodCardHeaderDraft | null>(null);
|
|
||||||
|
|
||||||
const selectedIds = state.selectedDecisionApproachIds ?? [];
|
const selectedIds = state.selectedDecisionApproachIds ?? [];
|
||||||
|
|
||||||
@@ -117,6 +119,8 @@ export function DecisionApproachesScreen() {
|
|||||||
|
|
||||||
const handleOpenAddWizard = useCallback(() => {
|
const handleOpenAddWizard = useCallback(() => {
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
|
setWizardCustomizeCardId(null);
|
||||||
|
setWizardInitialValues(null);
|
||||||
setAddCustomWizardOpen(true);
|
setAddCustomWizardOpen(true);
|
||||||
}, [markCreateFlowInteraction]);
|
}, [markCreateFlowInteraction]);
|
||||||
|
|
||||||
@@ -158,15 +162,40 @@ export function DecisionApproachesScreen() {
|
|||||||
const handleCardSelect = useCallback(
|
const handleCardSelect = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
customizeSnapshotRef.current = null;
|
const draft = seedDraft(id);
|
||||||
setModalEditUnlocked(false);
|
const persistedBlocks = state.customMethodCardFieldBlocksById?.[id];
|
||||||
setDraftFieldBlocks(null);
|
const initialBlocks =
|
||||||
setCustomizeHeaderDraft(null);
|
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);
|
setPendingCardId(id);
|
||||||
setPendingDraft(seedDraft(id));
|
setPendingDraft(draft);
|
||||||
|
setDraftFieldBlocks(initialBlocks);
|
||||||
setCreateModalOpen(true);
|
setCreateModalOpen(true);
|
||||||
},
|
},
|
||||||
[markCreateFlowInteraction, seedDraft],
|
[
|
||||||
|
da.confirmModal.description,
|
||||||
|
da.confirmModal.title,
|
||||||
|
markCreateFlowInteraction,
|
||||||
|
methodById,
|
||||||
|
seedDraft,
|
||||||
|
state.customMethodCardFieldBlocksById,
|
||||||
|
state.customMethodCardMetaById,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDraftChange = useCallback(
|
const handleDraftChange = useCallback(
|
||||||
@@ -179,9 +208,7 @@ export function DecisionApproachesScreen() {
|
|||||||
|
|
||||||
const isSelectedCardModal =
|
const isSelectedCardModal =
|
||||||
pendingCardId !== null && selectedIds.includes(pendingCardId);
|
pendingCardId !== null && selectedIds.includes(pendingCardId);
|
||||||
const fieldsLocked = !modalEditUnlocked;
|
const showMethodModalPrimary = true;
|
||||||
|
|
||||||
const showMethodModalPrimary = !isSelectedCardModal || modalEditUnlocked;
|
|
||||||
|
|
||||||
const customFacetDetailsMatchPreset = useMemo(() => {
|
const customFacetDetailsMatchPreset = useMemo(() => {
|
||||||
if (!pendingCardId || !pendingDraft) return false;
|
if (!pendingCardId || !pendingDraft) return false;
|
||||||
@@ -203,7 +230,7 @@ export function DecisionApproachesScreen() {
|
|||||||
methodId: pendingCardId,
|
methodId: pendingCardId,
|
||||||
meta: state.customMethodCardMetaById,
|
meta: state.customMethodCardMetaById,
|
||||||
fieldBlocksById: state.customMethodCardFieldBlocksById,
|
fieldBlocksById: state.customMethodCardFieldBlocksById,
|
||||||
modalEditUnlocked,
|
modalEditUnlocked: false,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
customFacetDetailsMatchPreset,
|
customFacetDetailsMatchPreset,
|
||||||
}),
|
}),
|
||||||
@@ -211,7 +238,6 @@ export function DecisionApproachesScreen() {
|
|||||||
[
|
[
|
||||||
customFacetDetailsMatchPreset,
|
customFacetDetailsMatchPreset,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
modalEditUnlocked,
|
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
state.customMethodCardFieldBlocksById,
|
state.customMethodCardFieldBlocksById,
|
||||||
state.customMethodCardMetaById,
|
state.customMethodCardMetaById,
|
||||||
@@ -221,11 +247,11 @@ export function DecisionApproachesScreen() {
|
|||||||
const handleCreateModalClose = useCallback(async () => {
|
const handleCreateModalClose = useCallback(async () => {
|
||||||
if (
|
if (
|
||||||
!(await confirmDiscard(
|
!(await confirmDiscard(
|
||||||
modalEditUnlocked,
|
true,
|
||||||
customizeSnapshotRef.current,
|
customizeSnapshotRef.current,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
customizeHeaderDraft,
|
customizeSnapshotRef.current?.headerDraft ?? null,
|
||||||
))
|
))
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@@ -253,53 +279,14 @@ export function DecisionApproachesScreen() {
|
|||||||
setCreateModalOpen(false);
|
setCreateModalOpen(false);
|
||||||
setPendingCardId(null);
|
setPendingCardId(null);
|
||||||
setPendingDraft(null);
|
setPendingDraft(null);
|
||||||
setModalEditUnlocked(false);
|
|
||||||
setDraftFieldBlocks(null);
|
setDraftFieldBlocks(null);
|
||||||
setCustomizeHeaderDraft(null);
|
|
||||||
}, [
|
}, [
|
||||||
confirmDiscard,
|
confirmDiscard,
|
||||||
customizeHeaderDraft,
|
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
modalEditUnlocked,
|
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
replaceState,
|
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 () => {
|
const handleRemoveSelectedFromModal = useCallback(async () => {
|
||||||
if (!pendingCardId || !selectedIds.includes(pendingCardId)) {
|
if (!pendingCardId || !selectedIds.includes(pendingCardId)) {
|
||||||
return;
|
return;
|
||||||
@@ -307,11 +294,11 @@ export function DecisionApproachesScreen() {
|
|||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
if (
|
if (
|
||||||
!(await confirmDiscard(
|
!(await confirmDiscard(
|
||||||
modalEditUnlocked,
|
true,
|
||||||
customizeSnapshotRef.current,
|
customizeSnapshotRef.current,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
customizeHeaderDraft,
|
customizeSnapshotRef.current?.headerDraft ?? null,
|
||||||
))
|
))
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@@ -327,11 +314,9 @@ export function DecisionApproachesScreen() {
|
|||||||
await handleCreateModalClose();
|
await handleCreateModalClose();
|
||||||
}, [
|
}, [
|
||||||
confirmDiscard,
|
confirmDiscard,
|
||||||
customizeHeaderDraft,
|
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
handleCreateModalClose,
|
handleCreateModalClose,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
modalEditUnlocked,
|
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
@@ -341,35 +326,36 @@ export function DecisionApproachesScreen() {
|
|||||||
|
|
||||||
const handleCustomize = useCallback(() => {
|
const handleCustomize = useCallback(() => {
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
if (!pendingDraft || !pendingCardId) {
|
if (!pendingCardId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const initialFieldBlocks =
|
|
||||||
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById)
|
|
||||||
? structuredClone(
|
|
||||||
state.customMethodCardFieldBlocksById?.[pendingCardId] ?? [],
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
const method = methodById.get(pendingCardId);
|
const method = methodById.get(pendingCardId);
|
||||||
const meta = state.customMethodCardMetaById?.[pendingCardId];
|
setWizardInitialValues(
|
||||||
const headerDraft: MethodCardHeaderDraft = {
|
buildMethodCardWizardInitialValues({
|
||||||
title: meta?.label ?? method?.label ?? da.confirmModal.title,
|
cardId: pendingCardId,
|
||||||
description:
|
fallbackTitle: method?.label ?? da.confirmModal.title,
|
||||||
meta?.supportText ??
|
fallbackDescription:
|
||||||
method?.supportText ??
|
method?.supportText ?? da.confirmModal.description,
|
||||||
da.confirmModal.description,
|
meta: state.customMethodCardMetaById,
|
||||||
};
|
persistedBlocks: state.customMethodCardFieldBlocksById,
|
||||||
setCustomizeHeaderDraft(headerDraft);
|
draftFieldBlocks,
|
||||||
customizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
|
facetPrefill: pendingDraft
|
||||||
pendingDraft,
|
? {
|
||||||
initialFieldBlocks,
|
group: "decisionApproaches",
|
||||||
headerDraft,
|
draft: pendingDraft,
|
||||||
|
headings: da.sectionHeadings,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
setDraftFieldBlocks(initialFieldBlocks);
|
setWizardCustomizeCardId(pendingCardId);
|
||||||
setModalEditUnlocked(true);
|
setCreateModalOpen(false);
|
||||||
|
setAddCustomWizardOpen(true);
|
||||||
}, [
|
}, [
|
||||||
da.confirmModal.description,
|
da.confirmModal.description,
|
||||||
da.confirmModal.title,
|
da.confirmModal.title,
|
||||||
|
da.sectionHeadings,
|
||||||
|
draftFieldBlocks,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
methodById,
|
methodById,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
@@ -394,9 +380,7 @@ export function DecisionApproachesScreen() {
|
|||||||
() => decisionApproachPresetFor(newId),
|
() => decisionApproachPresetFor(newId),
|
||||||
);
|
);
|
||||||
const blocksClone = structuredClone(
|
const blocksClone = structuredClone(
|
||||||
modalEditUnlocked &&
|
draftFieldBlocks !== null
|
||||||
draftFieldBlocks !== null &&
|
|
||||||
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById)
|
|
||||||
? draftFieldBlocks
|
? draftFieldBlocks
|
||||||
: cloneMethodCardBlocksForDuplicate(
|
: cloneMethodCardBlocksForDuplicate(
|
||||||
state.customMethodCardFieldBlocksById,
|
state.customMethodCardFieldBlocksById,
|
||||||
@@ -426,13 +410,12 @@ export function DecisionApproachesScreen() {
|
|||||||
customizeSnapshotRef.current = null;
|
customizeSnapshotRef.current = null;
|
||||||
setPendingCardId(newId);
|
setPendingCardId(newId);
|
||||||
setPendingDraft(structuredClone(detailsClone));
|
setPendingDraft(structuredClone(detailsClone));
|
||||||
setModalEditUnlocked(false);
|
setDraftFieldBlocks(
|
||||||
setDraftFieldBlocks(null);
|
blocksClone.length > 0 ? structuredClone(blocksClone) : null,
|
||||||
setCustomizeHeaderDraft(null);
|
);
|
||||||
}, [
|
}, [
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
modalEditUnlocked,
|
|
||||||
modalKebabMenu.duplicateTitleSuffix,
|
modalKebabMenu.duplicateTitleSuffix,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
@@ -461,9 +444,7 @@ export function DecisionApproachesScreen() {
|
|||||||
() => decisionApproachPresetFor(newId),
|
() => decisionApproachPresetFor(newId),
|
||||||
);
|
);
|
||||||
const blocksClone = structuredClone(
|
const blocksClone = structuredClone(
|
||||||
modalEditUnlocked &&
|
draftFieldBlocks !== null
|
||||||
draftFieldBlocks !== null &&
|
|
||||||
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById)
|
|
||||||
? draftFieldBlocks
|
? draftFieldBlocks
|
||||||
: cloneMethodCardBlocksForDuplicate(
|
: cloneMethodCardBlocksForDuplicate(
|
||||||
state.customMethodCardFieldBlocksById,
|
state.customMethodCardFieldBlocksById,
|
||||||
@@ -493,14 +474,13 @@ export function DecisionApproachesScreen() {
|
|||||||
customizeSnapshotRef.current = null;
|
customizeSnapshotRef.current = null;
|
||||||
setPendingCardId(newId);
|
setPendingCardId(newId);
|
||||||
setPendingDraft(structuredClone(detailsClone));
|
setPendingDraft(structuredClone(detailsClone));
|
||||||
setModalEditUnlocked(false);
|
setDraftFieldBlocks(
|
||||||
setDraftFieldBlocks(null);
|
blocksClone.length > 0 ? structuredClone(blocksClone) : null,
|
||||||
setCustomizeHeaderDraft(null);
|
);
|
||||||
}, [
|
}, [
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
methodById,
|
methodById,
|
||||||
modalEditUnlocked,
|
|
||||||
modalKebabMenu.duplicateTitleSuffix,
|
modalKebabMenu.duplicateTitleSuffix,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
@@ -513,7 +493,7 @@ export function DecisionApproachesScreen() {
|
|||||||
const kebabMenuItems = useMemo(
|
const kebabMenuItems = useMemo(
|
||||||
() =>
|
() =>
|
||||||
buildCustomRuleModalKebabMenu(modalKebabMenu, {
|
buildCustomRuleModalKebabMenu(modalKebabMenu, {
|
||||||
showCustomize: !modalEditUnlocked,
|
showCustomize: true,
|
||||||
onCustomize: handleCustomize,
|
onCustomize: handleCustomize,
|
||||||
onDuplicate:
|
onDuplicate:
|
||||||
(state.editingPublishedRuleId?.trim() ?? "") !== "" || !pendingCardId
|
(state.editingPublishedRuleId?.trim() ?? "") !== "" || !pendingCardId
|
||||||
@@ -533,7 +513,6 @@ export function DecisionApproachesScreen() {
|
|||||||
handleDuplicatePrefabCard,
|
handleDuplicatePrefabCard,
|
||||||
handleRemoveSelectedFromModal,
|
handleRemoveSelectedFromModal,
|
||||||
isSelectedCardModal,
|
isSelectedCardModal,
|
||||||
modalEditUnlocked,
|
|
||||||
modalKebabMenu,
|
modalKebabMenu,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
state.customMethodCardMetaById,
|
state.customMethodCardMetaById,
|
||||||
@@ -547,8 +526,14 @@ export function DecisionApproachesScreen() {
|
|||||||
}, [markCreateFlowInteraction]);
|
}, [markCreateFlowInteraction]);
|
||||||
|
|
||||||
const handleCloseAddWizard = useCallback(() => {
|
const handleCloseAddWizard = useCallback(() => {
|
||||||
|
const resumeCardId = wizardCustomizeCardId;
|
||||||
setAddCustomWizardOpen(false);
|
setAddCustomWizardOpen(false);
|
||||||
}, []);
|
setWizardCustomizeCardId(null);
|
||||||
|
setWizardInitialValues(null);
|
||||||
|
if (resumeCardId && pendingCardId === resumeCardId) {
|
||||||
|
setCreateModalOpen(true);
|
||||||
|
}
|
||||||
|
}, [pendingCardId, wizardCustomizeCardId]);
|
||||||
|
|
||||||
const handleFinalizeCustomCard = useCallback(
|
const handleFinalizeCustomCard = useCallback(
|
||||||
({
|
({
|
||||||
@@ -561,6 +546,42 @@ export function DecisionApproachesScreen() {
|
|||||||
fieldBlocks: CustomMethodCardFieldBlock[];
|
fieldBlocks: CustomMethodCardFieldBlock[];
|
||||||
}) => {
|
}) => {
|
||||||
markCreateFlowInteraction();
|
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();
|
const id = crypto.randomUUID();
|
||||||
updateState({
|
updateState({
|
||||||
selectedDecisionApproachIds: moveFacetSelectionIdToFront(
|
selectedDecisionApproachIds: moveFacetSelectionIdToFront(
|
||||||
@@ -583,91 +604,29 @@ export function DecisionApproachesScreen() {
|
|||||||
},
|
},
|
||||||
[
|
[
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
|
pendingDraft,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
state.customMethodCardFieldBlocksById,
|
state.customMethodCardFieldBlocksById,
|
||||||
state.customMethodCardMetaById,
|
state.customMethodCardMetaById,
|
||||||
state.decisionApproachDetailsById,
|
state.decisionApproachDetailsById,
|
||||||
updateState,
|
updateState,
|
||||||
|
wizardCustomizeCardId,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCreateModalPrimary = useCallback(() => {
|
const handleCreateModalPrimary = useCallback(() => {
|
||||||
if (!pendingCardId) {
|
if (!pendingCardId) {
|
||||||
handleCreateModalClose();
|
void handleCreateModalClose();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
|
|
||||||
if (selectedIds.includes(pendingCardId)) {
|
const persistWizardBlocks =
|
||||||
if (modalEditUnlocked) {
|
modalUsesWizardFieldBlocksBody && draftFieldBlocks !== null;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (modalEditUnlocked) {
|
if (selectedIds.includes(pendingCardId)) {
|
||||||
if (!customizeHeaderDraft) {
|
if (persistWizardBlocks) {
|
||||||
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({
|
updateState({
|
||||||
customMethodCardMetaById: nextMeta,
|
|
||||||
customMethodCardFieldBlocksById: {
|
customMethodCardFieldBlocksById: {
|
||||||
...(state.customMethodCardFieldBlocksById ?? {}),
|
...(state.customMethodCardFieldBlocksById ?? {}),
|
||||||
[pendingCardId]: structuredClone(draftFieldBlocks ?? []),
|
[pendingCardId]: structuredClone(draftFieldBlocks ?? []),
|
||||||
@@ -675,22 +634,27 @@ export function DecisionApproachesScreen() {
|
|||||||
});
|
});
|
||||||
} else if (pendingDraft) {
|
} else if (pendingDraft) {
|
||||||
updateState({
|
updateState({
|
||||||
customMethodCardMetaById: nextMeta,
|
|
||||||
decisionApproachDetailsById: {
|
decisionApproachDetailsById: {
|
||||||
...(state.decisionApproachDetailsById ?? {}),
|
...(state.decisionApproachDetailsById ?? {}),
|
||||||
[pendingCardId]: pendingDraft,
|
[pendingCardId]: pendingDraft,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
customizeSnapshotRef.current = null;
|
if (pendingDraft) {
|
||||||
setModalEditUnlocked(false);
|
customizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
|
||||||
setDraftFieldBlocks(null);
|
pendingDraft,
|
||||||
setCustomizeHeaderDraft(null);
|
persistWizardBlocks ? draftFieldBlocks : null,
|
||||||
|
customizeSnapshotRef.current?.headerDraft ?? {
|
||||||
|
title: "",
|
||||||
|
description: "",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!pendingDraft) {
|
if (!pendingDraft) {
|
||||||
handleCreateModalClose();
|
void handleCreateModalClose();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updateState({
|
updateState({
|
||||||
@@ -702,15 +666,23 @@ export function DecisionApproachesScreen() {
|
|||||||
...(state.decisionApproachDetailsById ?? {}),
|
...(state.decisionApproachDetailsById ?? {}),
|
||||||
[pendingCardId]: pendingDraft,
|
[pendingCardId]: pendingDraft,
|
||||||
},
|
},
|
||||||
|
...(persistWizardBlocks
|
||||||
|
? {
|
||||||
|
customMethodCardFieldBlocksById: {
|
||||||
|
...(state.customMethodCardFieldBlocksById ?? {}),
|
||||||
|
[pendingCardId]: structuredClone(draftFieldBlocks ?? []),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
});
|
});
|
||||||
pendingEphemeralDuplicateIdRef.current = null;
|
pendingEphemeralDuplicateIdRef.current = null;
|
||||||
handleCreateModalClose();
|
customizeSnapshotRef.current = null;
|
||||||
|
void handleCreateModalClose();
|
||||||
}, [
|
}, [
|
||||||
customizeHeaderDraft,
|
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
handleCreateModalClose,
|
handleCreateModalClose,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
modalEditUnlocked,
|
modalUsesWizardFieldBlocksBody,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
@@ -729,7 +701,7 @@ export function DecisionApproachesScreen() {
|
|||||||
meta?.supportText ??
|
meta?.supportText ??
|
||||||
method?.supportText ??
|
method?.supportText ??
|
||||||
da.confirmModal.description,
|
da.confirmModal.description,
|
||||||
nextButtonText: modalEditUnlocked
|
nextButtonText: isSelectedCardModal
|
||||||
? saveLabel
|
? saveLabel
|
||||||
: da.addApproach.nextButtonText,
|
: da.addApproach.nextButtonText,
|
||||||
};
|
};
|
||||||
@@ -802,33 +774,10 @@ export function DecisionApproachesScreen() {
|
|||||||
<Create
|
<Create
|
||||||
isOpen={createModalOpen}
|
isOpen={createModalOpen}
|
||||||
onClose={handleCreateModalClose}
|
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}
|
onNext={handleCreateModalPrimary}
|
||||||
title={modalConfig.title}
|
title={modalConfig.title}
|
||||||
description={modalConfig.description}
|
description={modalConfig.description}
|
||||||
nextButtonText={modalConfig.nextButtonText}
|
nextButtonText={modalConfig.nextButtonText}
|
||||||
showBackButton={modalEditUnlocked}
|
|
||||||
onBack={handleCancelCustomize}
|
|
||||||
backButtonText={modalKebabMenu.cancelCustomize}
|
|
||||||
showNextButton={showMethodModalPrimary}
|
showNextButton={showMethodModalPrimary}
|
||||||
backdropVariant="blurredYellow"
|
backdropVariant="blurredYellow"
|
||||||
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
|
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
|
||||||
@@ -841,14 +790,14 @@ export function DecisionApproachesScreen() {
|
|||||||
cardId={pendingCardId}
|
cardId={pendingCardId}
|
||||||
blocksById={state.customMethodCardFieldBlocksById}
|
blocksById={state.customMethodCardFieldBlocksById}
|
||||||
blocksOverride={
|
blocksOverride={
|
||||||
modalEditUnlocked && draftFieldBlocks !== null
|
draftFieldBlocks !== null ? draftFieldBlocks : undefined
|
||||||
? draftFieldBlocks
|
|
||||||
: undefined
|
|
||||||
}
|
}
|
||||||
policyMeta={state.customMethodCardMetaById?.[pendingCardId]}
|
policyMeta={state.customMethodCardMetaById?.[pendingCardId]}
|
||||||
showPolicyContentLockupWhenNoBlocks={!modalEditUnlocked}
|
showPolicyContentLockupWhenNoBlocks={
|
||||||
|
draftFieldBlocks === null || draftFieldBlocks.length === 0
|
||||||
|
}
|
||||||
onFieldBlocksChange={
|
onFieldBlocksChange={
|
||||||
fieldsLocked
|
draftFieldBlocks === null
|
||||||
? undefined
|
? undefined
|
||||||
: (next) => setDraftFieldBlocks(next)
|
: (next) => setDraftFieldBlocks(next)
|
||||||
}
|
}
|
||||||
@@ -857,7 +806,6 @@ export function DecisionApproachesScreen() {
|
|||||||
<DecisionApproachEditFields
|
<DecisionApproachEditFields
|
||||||
value={pendingDraft}
|
value={pendingDraft}
|
||||||
onChange={handleDraftChange}
|
onChange={handleDraftChange}
|
||||||
readOnly={fieldsLocked}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
) : null}
|
) : null}
|
||||||
@@ -866,6 +814,7 @@ export function DecisionApproachesScreen() {
|
|||||||
<CustomMethodCardWizard
|
<CustomMethodCardWizard
|
||||||
isOpen={addCustomWizardOpen}
|
isOpen={addCustomWizardOpen}
|
||||||
onClose={handleCloseAddWizard}
|
onClose={handleCloseAddWizard}
|
||||||
|
initialValues={wizardInitialValues}
|
||||||
onFinalize={handleFinalizeCustomCard}
|
onFinalize={handleFinalizeCustomCard}
|
||||||
onPersistCustomUploadFile={(file) =>
|
onPersistCustomUploadFile={(file) =>
|
||||||
uploadCreateFlowFile(file, "customMethodAttachment")
|
uploadCreateFlowFile(file, "customMethodAttachment")
|
||||||
|
|||||||
@@ -16,13 +16,23 @@ import type {
|
|||||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||||
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
|
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
|
||||||
import { CoreValueEditFields } from "../../components/methodEditFields";
|
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 { buildCustomRuleModalKebabMenu } from "../../components/customRuleModalKebabMenu";
|
||||||
import {
|
import {
|
||||||
duplicateCoreValueChipInDraft,
|
duplicateCoreValueChipInDraft,
|
||||||
MAX_SELECTED_CORE_VALUES,
|
MAX_SELECTED_CORE_VALUES,
|
||||||
removeCoreValueChipFromDraft,
|
removeCoreValueChipFromDraft,
|
||||||
} from "../../../../../lib/create/coreValueChipFacet";
|
} 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;
|
const MAX_CORE_VALUES = MAX_SELECTED_CORE_VALUES;
|
||||||
|
|
||||||
@@ -113,6 +123,15 @@ export function CoreValuesSelectScreen() {
|
|||||||
);
|
);
|
||||||
const [modalSession, setModalSession] = useState<ModalSession | null>(null);
|
const [modalSession, setModalSession] = useState<ModalSession | null>(null);
|
||||||
const [draft, setDraft] = useState<CoreValueDetailEntry>(EMPTY_DETAIL);
|
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(() => {
|
useEffect(() => {
|
||||||
setCoreValueOptions(
|
setCoreValueOptions(
|
||||||
@@ -165,6 +184,7 @@ export function CoreValuesSelectScreen() {
|
|||||||
return {
|
return {
|
||||||
meaning: saved?.meaning ?? preset.meaning,
|
meaning: saved?.meaning ?? preset.meaning,
|
||||||
signals: saved?.signals ?? preset.signals,
|
signals: saved?.signals ?? preset.signals,
|
||||||
|
...(saved?.supportText ? { supportText: saved.supportText } : {}),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[state.coreValueDetailsByChipId, getPresetTexts],
|
[state.coreValueDetailsByChipId, getPresetTexts],
|
||||||
@@ -179,12 +199,22 @@ export function CoreValuesSelectScreen() {
|
|||||||
) => {
|
) => {
|
||||||
const initial = seedDetail ?? getInitialTexts(chipId, valueLabel);
|
const initial = seedDetail ?? getInitialTexts(chipId, valueLabel);
|
||||||
initialDraftRef.current = { ...initial };
|
initialDraftRef.current = { ...initial };
|
||||||
|
const persisted = state.customMethodCardFieldBlocksById?.[chipId];
|
||||||
|
setDraftFieldBlocks(
|
||||||
|
Array.isArray(persisted) && persisted.length > 0
|
||||||
|
? structuredClone(persisted)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
setDraft(initial);
|
setDraft(initial);
|
||||||
setActiveModalChipId(chipId);
|
setActiveModalChipId(chipId);
|
||||||
setModalSession(session);
|
setModalSession(session);
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
},
|
},
|
||||||
[getInitialTexts, markCreateFlowInteraction],
|
[
|
||||||
|
getInitialTexts,
|
||||||
|
markCreateFlowInteraction,
|
||||||
|
state.customMethodCardFieldBlocksById,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDraftChange = useCallback(
|
const handleDraftChange = useCallback(
|
||||||
@@ -200,19 +230,19 @@ export function CoreValuesSelectScreen() {
|
|||||||
initialDraftRef.current = null;
|
initialDraftRef.current = null;
|
||||||
setActiveModalChipId(null);
|
setActiveModalChipId(null);
|
||||||
setModalSession(null);
|
setModalSession(null);
|
||||||
|
setDraftFieldBlocks(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const confirmLeaveWithoutSaving = useCallback(async () => {
|
const confirmLeaveWithoutSaving = useCallback(async () => {
|
||||||
const isPendingAdd =
|
|
||||||
modalSession === "pending" || modalSession === "customPending";
|
|
||||||
const initial = initialDraftRef.current;
|
const initial = initialDraftRef.current;
|
||||||
const editingDirty =
|
const fieldsDirty =
|
||||||
modalSession === "editing" &&
|
|
||||||
initial != null &&
|
initial != null &&
|
||||||
(draft.meaning !== initial.meaning || draft.signals !== initial.signals);
|
(draft.meaning !== initial.meaning || draft.signals !== initial.signals);
|
||||||
if (!isPendingAdd && !editingDirty) {
|
if (!fieldsDirty) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
const isPendingAdd =
|
||||||
|
modalSession === "pending" || modalSession === "customPending";
|
||||||
return requestConfirm({
|
return requestConfirm({
|
||||||
title: cv.detailModal.discardTitle,
|
title: cv.detailModal.discardTitle,
|
||||||
description: isPendingAdd
|
description: isPendingAdd
|
||||||
@@ -294,11 +324,7 @@ export function CoreValuesSelectScreen() {
|
|||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
replaceState((prev) => ({
|
replaceState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
selectedCoreValueIds: selectedIdsFromOptions(nextFiltered),
|
...removeCoreValueChipFromDraft(prev, activeModalChipId),
|
||||||
coreValuesChipsSnapshot:
|
|
||||||
chipOptionsToSnapshotRows(nextFiltered),
|
|
||||||
coreValueDetailsByChipId:
|
|
||||||
omitIdFromStringRecord(prev.coreValueDetailsByChipId, activeModalChipId),
|
|
||||||
}));
|
}));
|
||||||
setCoreValueOptions(nextFiltered);
|
setCoreValueOptions(nextFiltered);
|
||||||
}
|
}
|
||||||
@@ -355,48 +381,185 @@ export function CoreValuesSelectScreen() {
|
|||||||
if (!activeModalChipId || !modalSession) return;
|
if (!activeModalChipId || !modalSession) return;
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
pendingEphemeralCoreDuplicateRef.current = null;
|
pendingEphemeralCoreDuplicateRef.current = null;
|
||||||
|
const existingBlocks =
|
||||||
|
draftFieldBlocks && draftFieldBlocks.length > 0
|
||||||
|
? draftFieldBlocks
|
||||||
|
: state.customMethodCardFieldBlocksById?.[activeModalChipId];
|
||||||
updateState({
|
updateState({
|
||||||
coreValueDetailsByChipId: {
|
coreValueDetailsByChipId: {
|
||||||
...(state.coreValueDetailsByChipId ?? {}),
|
...(state.coreValueDetailsByChipId ?? {}),
|
||||||
[activeModalChipId]: draft,
|
[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();
|
finalizeModalDismiss();
|
||||||
}, [
|
}, [
|
||||||
activeModalChipId,
|
activeModalChipId,
|
||||||
|
cv.detailModal.meaningLabel,
|
||||||
|
cv.detailModal.signalsLabel,
|
||||||
draft,
|
draft,
|
||||||
|
draftFieldBlocks,
|
||||||
finalizeModalDismiss,
|
finalizeModalDismiss,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
modalSession,
|
modalSession,
|
||||||
state.coreValueDetailsByChipId,
|
state.coreValueDetailsByChipId,
|
||||||
|
state.customMethodCardFieldBlocksById,
|
||||||
updateState,
|
updateState,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const modalChipLabel =
|
const modalChipLabel =
|
||||||
coreValueOptions.find((o) => o.id === activeModalChipId)?.label ?? "";
|
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 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(() => {
|
const kebabMenuItems = useMemo(() => {
|
||||||
if (!modalSession || !activeModalChipId) return [];
|
if (!modalSession || !activeModalChipId) return [];
|
||||||
const selectedCount = coreValueOptions.filter(
|
const selectedCount = coreValueOptions.filter(
|
||||||
(o) => o.state === "selected",
|
(o) => o.state === "selected",
|
||||||
).length;
|
).length;
|
||||||
return buildCustomRuleModalKebabMenu(modalKebabMenu, {
|
return buildCustomRuleModalKebabMenu(modalKebabMenu, {
|
||||||
|
showCustomize: true,
|
||||||
|
onCustomize: handleCustomize,
|
||||||
onDuplicate:
|
onDuplicate:
|
||||||
modalSession !== "editing" || selectedCount >= MAX_CORE_VALUES
|
(state.editingPublishedRuleId?.trim() ?? "") !== "" ||
|
||||||
|
selectedCount >= MAX_CORE_VALUES
|
||||||
? undefined
|
? undefined
|
||||||
: handleDuplicateCoreChip,
|
: handleDuplicateCoreChip,
|
||||||
showRemove: true,
|
showRemove: modalSession === "editing",
|
||||||
onRemove: handleRemoveFromKebab,
|
onRemove: handleRemoveFromKebab,
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
activeModalChipId,
|
activeModalChipId,
|
||||||
coreValueOptions,
|
coreValueOptions,
|
||||||
|
handleCustomize,
|
||||||
handleDuplicateCoreChip,
|
handleDuplicateCoreChip,
|
||||||
handleRemoveFromKebab,
|
handleRemoveFromKebab,
|
||||||
modalKebabMenu,
|
modalKebabMenu,
|
||||||
modalSession,
|
modalSession,
|
||||||
|
state.editingPublishedRuleId,
|
||||||
]);
|
]);
|
||||||
const handleChipClick = (chipId: string) => {
|
const handleChipClick = (chipId: string) => {
|
||||||
const target = coreValueOptions.find((o) => o.id === chipId);
|
const target = coreValueOptions.find((o) => o.id === chipId);
|
||||||
@@ -520,14 +683,16 @@ export function CoreValuesSelectScreen() {
|
|||||||
|
|
||||||
{detailModal && (
|
{detailModal && (
|
||||||
<Create
|
<Create
|
||||||
isOpen={activeModalChipId !== null}
|
isOpen={activeModalChipId !== null && !addCustomWizardOpen}
|
||||||
onClose={handleModalDismiss}
|
onClose={handleModalDismiss}
|
||||||
backdropVariant="blurredYellow"
|
backdropVariant="blurredYellow"
|
||||||
headerContent={
|
headerContent={
|
||||||
<div className="bg-[var(--color-surface-default-primary)] px-[24px] py-[12px] shrink-0">
|
<div className="bg-[var(--color-surface-default-primary)] px-[24px] py-[12px] shrink-0">
|
||||||
<ContentLockup
|
<ContentLockup
|
||||||
title={modalChipLabel}
|
title={modalChipLabel}
|
||||||
description={detailModal.subtitle}
|
description={
|
||||||
|
draft.supportText?.trim() || detailModal.subtitle
|
||||||
|
}
|
||||||
variant="modal"
|
variant="modal"
|
||||||
alignment="left"
|
alignment="left"
|
||||||
/>
|
/>
|
||||||
@@ -548,13 +713,43 @@ export function CoreValuesSelectScreen() {
|
|||||||
}
|
}
|
||||||
ariaLabel={modalChipLabel || "Core value details"}
|
ariaLabel={modalChipLabel || "Core value details"}
|
||||||
>
|
>
|
||||||
|
{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
|
<CoreValueEditFields
|
||||||
value={draft}
|
value={draft}
|
||||||
onChange={handleDraftChange}
|
onChange={handleDraftChange}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</Create>
|
</Create>
|
||||||
)}
|
)}
|
||||||
</CreateFlowTwoColumnSelectShell>
|
</CreateFlowTwoColumnSelectShell>
|
||||||
|
<CustomMethodCardWizard
|
||||||
|
isOpen={addCustomWizardOpen}
|
||||||
|
onClose={handleCloseAddWizard}
|
||||||
|
initialValues={wizardInitialValues}
|
||||||
|
onFinalize={handleFinalizeCustomCard}
|
||||||
|
onPersistCustomUploadFile={(file) =>
|
||||||
|
uploadCreateFlowFile(file, "customMethodAttachment")
|
||||||
|
}
|
||||||
|
/>
|
||||||
{confirmDialog}
|
{confirmDialog}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export type CommunityStructureChipSnapshotRow = {
|
|||||||
export type CoreValueDetailEntry = {
|
export type CoreValueDetailEntry = {
|
||||||
meaning: string;
|
meaning: string;
|
||||||
signals: string;
|
signals: string;
|
||||||
|
supportText?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -21,7 +21,13 @@ export function readCoreValueDetailsFromLocalStorage(): Record<
|
|||||||
if (typeof o.meaning !== "string" || typeof o.signals !== "string") {
|
if (typeof o.meaning !== "string" || typeof o.signals !== "string") {
|
||||||
continue;
|
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;
|
return out;
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -69,7 +69,6 @@ export function applyFinalReviewChipEditPatch(
|
|||||||
}
|
}
|
||||||
: {};
|
: {};
|
||||||
if (
|
if (
|
||||||
patch.groupKey !== "coreValues" &&
|
|
||||||
"customMethodCardFieldBlocks" in patch &&
|
"customMethodCardFieldBlocks" in patch &&
|
||||||
patch.customMethodCardFieldBlocks !== undefined
|
patch.customMethodCardFieldBlocks !== undefined
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -24,6 +24,15 @@ export function removeCoreValueChipFromDraft(
|
|||||||
const nextDetails = hadDetail
|
const nextDetails = hadDetail
|
||||||
? omitIdFromStringRecord(state.coreValueDetailsByChipId, chipId)
|
? omitIdFromStringRecord(state.coreValueDetailsByChipId, chipId)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const hadBlocks =
|
||||||
|
Boolean(state.customMethodCardFieldBlocksById) &&
|
||||||
|
Object.prototype.hasOwnProperty.call(
|
||||||
|
state.customMethodCardFieldBlocksById,
|
||||||
|
chipId,
|
||||||
|
);
|
||||||
|
const nextBlocks = hadBlocks
|
||||||
|
? omitIdFromStringRecord(state.customMethodCardFieldBlocksById, chipId)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const out: Partial<CreateFlowState> = {
|
const out: Partial<CreateFlowState> = {
|
||||||
coreValuesChipsSnapshot: nextSnap,
|
coreValuesChipsSnapshot: nextSnap,
|
||||||
@@ -33,6 +42,9 @@ export function removeCoreValueChipFromDraft(
|
|||||||
if (hadDetail) {
|
if (hadDetail) {
|
||||||
out.coreValueDetailsByChipId = nextDetails;
|
out.coreValueDetailsByChipId = nextDetails;
|
||||||
}
|
}
|
||||||
|
if (hadBlocks) {
|
||||||
|
out.customMethodCardFieldBlocksById = nextBlocks;
|
||||||
|
}
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
@@ -77,6 +89,7 @@ export function duplicateCoreValueChipInDraft(
|
|||||||
[newId]: structuredClone(inherited),
|
[newId]: structuredClone(inherited),
|
||||||
}
|
}
|
||||||
: { ...(state.coreValueDetailsByChipId ?? {}) };
|
: { ...(state.coreValueDetailsByChipId ?? {}) };
|
||||||
|
const inheritedBlocks = state.customMethodCardFieldBlocksById?.[chipId];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
newId,
|
newId,
|
||||||
@@ -87,6 +100,14 @@ export function duplicateCoreValueChipInDraft(
|
|||||||
...(Object.keys(nextDetails).length > 0
|
...(Object.keys(nextDetails).length > 0
|
||||||
? { coreValueDetailsByChipId: nextDetails }
|
? { coreValueDetailsByChipId: nextDetails }
|
||||||
: {}),
|
: {}),
|
||||||
|
...(inheritedBlocks !== undefined
|
||||||
|
? {
|
||||||
|
customMethodCardFieldBlocksById: {
|
||||||
|
...(state.customMethodCardFieldBlocksById ?? {}),
|
||||||
|
[newId]: structuredClone(inheritedBlocks),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,8 @@
|
|||||||
/** Max length for title and description fields in the add-custom-method-card wizard (Figma 0/48). */
|
/** Max length for the policy title in the add-custom-method-card wizard (Figma 0/48). */
|
||||||
export const CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS = 48;
|
export const CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS = 48;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Max length for the policy description. Wider than the title so Customize can
|
||||||
|
* seed existing card support text without blocking Next.
|
||||||
|
*/
|
||||||
|
export const CUSTOM_METHOD_CARD_WIZARD_MAX_DESCRIPTION_CHARS = 200;
|
||||||
|
|||||||
@@ -0,0 +1,391 @@
|
|||||||
|
import type {
|
||||||
|
CommunicationMethodDetailEntry,
|
||||||
|
ConflictManagementDetailEntry,
|
||||||
|
CoreValueDetailEntry,
|
||||||
|
CreateFlowState,
|
||||||
|
DecisionApproachDetailEntry,
|
||||||
|
MembershipMethodDetailEntry,
|
||||||
|
} from "../../app/(app)/create/types";
|
||||||
|
import type { CustomMethodCardFieldBlock } from "./customMethodCardFieldBlocks";
|
||||||
|
import { formatConflictApplicableScopeForTextarea } from "./ruleSectionsFromMethodSelections";
|
||||||
|
|
||||||
|
/** Seed for {@link CustomMethodCardWizard} when Customize opens on an existing card. */
|
||||||
|
export type MethodCardWizardInitialValues = {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
fieldBlocks: CustomMethodCardFieldBlock[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MethodCardWizardFacetPrefill =
|
||||||
|
| {
|
||||||
|
group: "communication";
|
||||||
|
draft: CommunicationMethodDetailEntry;
|
||||||
|
headings: {
|
||||||
|
corePrinciple: string;
|
||||||
|
logisticsAdmin: string;
|
||||||
|
codeOfConduct: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
group: "membership";
|
||||||
|
draft: MembershipMethodDetailEntry;
|
||||||
|
headings: {
|
||||||
|
eligibility: string;
|
||||||
|
joiningProcess: string;
|
||||||
|
expectations: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
group: "decisionApproaches";
|
||||||
|
draft: DecisionApproachDetailEntry;
|
||||||
|
headings: {
|
||||||
|
corePrinciple: string;
|
||||||
|
applicableScope: string;
|
||||||
|
stepByStepInstructions: string;
|
||||||
|
consensusLevel: string;
|
||||||
|
objectionsDeadlocks: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
group: "conflictManagement";
|
||||||
|
draft: ConflictManagementDetailEntry;
|
||||||
|
headings: {
|
||||||
|
corePrinciple: string;
|
||||||
|
applicableScope: string;
|
||||||
|
processProtocol: string;
|
||||||
|
restorationFallbacks: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
group: "coreValues";
|
||||||
|
draft: CoreValueDetailEntry;
|
||||||
|
headings: {
|
||||||
|
meaning: string;
|
||||||
|
signals: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const TEXT_BODY_MAX = 8000;
|
||||||
|
|
||||||
|
function textBlock(
|
||||||
|
id: string,
|
||||||
|
blockTitle: string,
|
||||||
|
placeholderText: string,
|
||||||
|
): CustomMethodCardFieldBlock {
|
||||||
|
return {
|
||||||
|
kind: "text",
|
||||||
|
id,
|
||||||
|
blockTitle,
|
||||||
|
placeholderText: placeholderText.slice(0, TEXT_BODY_MAX),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasTrimmedText(value: string): boolean {
|
||||||
|
return value.trim().length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampPercent(n: number): number {
|
||||||
|
if (!Number.isFinite(n)) return 1;
|
||||||
|
return Math.min(100, Math.max(1, Math.round(n)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function facetPrefillHasContent(prefill: MethodCardWizardFacetPrefill): boolean {
|
||||||
|
switch (prefill.group) {
|
||||||
|
case "communication":
|
||||||
|
return (
|
||||||
|
hasTrimmedText(prefill.draft.corePrinciple) ||
|
||||||
|
hasTrimmedText(prefill.draft.logisticsAdmin) ||
|
||||||
|
hasTrimmedText(prefill.draft.codeOfConduct)
|
||||||
|
);
|
||||||
|
case "membership":
|
||||||
|
return (
|
||||||
|
hasTrimmedText(prefill.draft.eligibility) ||
|
||||||
|
hasTrimmedText(prefill.draft.joiningProcess) ||
|
||||||
|
hasTrimmedText(prefill.draft.expectations)
|
||||||
|
);
|
||||||
|
case "decisionApproaches":
|
||||||
|
return (
|
||||||
|
hasTrimmedText(prefill.draft.corePrinciple) ||
|
||||||
|
hasTrimmedText(prefill.draft.stepByStepInstructions) ||
|
||||||
|
hasTrimmedText(prefill.draft.objectionsDeadlocks) ||
|
||||||
|
prefill.draft.applicableScope.length > 0 ||
|
||||||
|
prefill.draft.selectedApplicableScope.length > 0
|
||||||
|
);
|
||||||
|
case "conflictManagement":
|
||||||
|
return (
|
||||||
|
hasTrimmedText(prefill.draft.corePrinciple) ||
|
||||||
|
hasTrimmedText(prefill.draft.processProtocol) ||
|
||||||
|
hasTrimmedText(prefill.draft.restorationFallbacks) ||
|
||||||
|
formatConflictApplicableScopeForTextarea(
|
||||||
|
prefill.draft.selectedApplicableScope,
|
||||||
|
prefill.draft.applicableScope,
|
||||||
|
).trim().length > 0
|
||||||
|
);
|
||||||
|
case "coreValues":
|
||||||
|
return (
|
||||||
|
hasTrimmedText(prefill.draft.meaning) ||
|
||||||
|
hasTrimmedText(prefill.draft.signals)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapFacetPrefillToWizardFieldBlocks(
|
||||||
|
prefill: MethodCardWizardFacetPrefill,
|
||||||
|
): CustomMethodCardFieldBlock[] {
|
||||||
|
switch (prefill.group) {
|
||||||
|
case "communication":
|
||||||
|
return [
|
||||||
|
textBlock(
|
||||||
|
"facet-corePrinciple",
|
||||||
|
prefill.headings.corePrinciple,
|
||||||
|
prefill.draft.corePrinciple,
|
||||||
|
),
|
||||||
|
textBlock(
|
||||||
|
"facet-logisticsAdmin",
|
||||||
|
prefill.headings.logisticsAdmin,
|
||||||
|
prefill.draft.logisticsAdmin,
|
||||||
|
),
|
||||||
|
textBlock(
|
||||||
|
"facet-codeOfConduct",
|
||||||
|
prefill.headings.codeOfConduct,
|
||||||
|
prefill.draft.codeOfConduct,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
case "membership":
|
||||||
|
return [
|
||||||
|
textBlock(
|
||||||
|
"facet-eligibility",
|
||||||
|
prefill.headings.eligibility,
|
||||||
|
prefill.draft.eligibility,
|
||||||
|
),
|
||||||
|
textBlock(
|
||||||
|
"facet-joiningProcess",
|
||||||
|
prefill.headings.joiningProcess,
|
||||||
|
prefill.draft.joiningProcess,
|
||||||
|
),
|
||||||
|
textBlock(
|
||||||
|
"facet-expectations",
|
||||||
|
prefill.headings.expectations,
|
||||||
|
prefill.draft.expectations,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
case "decisionApproaches": {
|
||||||
|
const scopeOptions =
|
||||||
|
prefill.draft.applicableScope.length > 0
|
||||||
|
? [...prefill.draft.applicableScope]
|
||||||
|
: [...prefill.draft.selectedApplicableScope];
|
||||||
|
const blocks: CustomMethodCardFieldBlock[] = [
|
||||||
|
textBlock(
|
||||||
|
"facet-corePrinciple",
|
||||||
|
prefill.headings.corePrinciple,
|
||||||
|
prefill.draft.corePrinciple,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
if (scopeOptions.length > 0) {
|
||||||
|
blocks.push({
|
||||||
|
kind: "badges",
|
||||||
|
id: "facet-applicableScope",
|
||||||
|
blockTitle: prefill.headings.applicableScope,
|
||||||
|
options: scopeOptions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
blocks.push(
|
||||||
|
textBlock(
|
||||||
|
"facet-stepByStepInstructions",
|
||||||
|
prefill.headings.stepByStepInstructions,
|
||||||
|
prefill.draft.stepByStepInstructions,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
kind: "proportion",
|
||||||
|
id: "facet-consensusLevel",
|
||||||
|
blockTitle: prefill.headings.consensusLevel,
|
||||||
|
defaultPercent: clampPercent(prefill.draft.consensusLevel),
|
||||||
|
},
|
||||||
|
textBlock(
|
||||||
|
"facet-objectionsDeadlocks",
|
||||||
|
prefill.headings.objectionsDeadlocks,
|
||||||
|
prefill.draft.objectionsDeadlocks,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
case "conflictManagement":
|
||||||
|
return [
|
||||||
|
textBlock(
|
||||||
|
"facet-corePrinciple",
|
||||||
|
prefill.headings.corePrinciple,
|
||||||
|
prefill.draft.corePrinciple,
|
||||||
|
),
|
||||||
|
textBlock(
|
||||||
|
"facet-applicableScope",
|
||||||
|
prefill.headings.applicableScope,
|
||||||
|
formatConflictApplicableScopeForTextarea(
|
||||||
|
prefill.draft.selectedApplicableScope,
|
||||||
|
prefill.draft.applicableScope,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
textBlock(
|
||||||
|
"facet-processProtocol",
|
||||||
|
prefill.headings.processProtocol,
|
||||||
|
prefill.draft.processProtocol,
|
||||||
|
),
|
||||||
|
textBlock(
|
||||||
|
"facet-restorationFallbacks",
|
||||||
|
prefill.headings.restorationFallbacks,
|
||||||
|
prefill.draft.restorationFallbacks,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
case "coreValues":
|
||||||
|
return [
|
||||||
|
textBlock(
|
||||||
|
"facet-meaning",
|
||||||
|
prefill.headings.meaning,
|
||||||
|
prefill.draft.meaning,
|
||||||
|
),
|
||||||
|
textBlock(
|
||||||
|
"facet-signals",
|
||||||
|
prefill.headings.signals,
|
||||||
|
prefill.draft.signals,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map a method card's facet editors onto wizard field blocks so Customize
|
||||||
|
* can show and edit the same sections on step 3.
|
||||||
|
*/
|
||||||
|
export function facetDetailsToWizardFieldBlocks(
|
||||||
|
prefill: MethodCardWizardFacetPrefill,
|
||||||
|
): CustomMethodCardFieldBlock[] {
|
||||||
|
if (!facetPrefillHasContent(prefill)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return mapFacetPrefillToWizardFieldBlocks(prefill);
|
||||||
|
}
|
||||||
|
|
||||||
|
function facetBlockHasContent(block: CustomMethodCardFieldBlock): boolean {
|
||||||
|
switch (block.kind) {
|
||||||
|
case "text":
|
||||||
|
return hasTrimmedText(block.placeholderText);
|
||||||
|
case "badges":
|
||||||
|
return block.options.length > 0;
|
||||||
|
case "proportion":
|
||||||
|
return true;
|
||||||
|
case "upload":
|
||||||
|
return Boolean(block.fileName?.trim() || block.assetUrl?.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function overlayBlockValue(
|
||||||
|
existing: CustomMethodCardFieldBlock,
|
||||||
|
incoming: CustomMethodCardFieldBlock,
|
||||||
|
): CustomMethodCardFieldBlock {
|
||||||
|
if (existing.kind === "text" && incoming.kind === "text") {
|
||||||
|
return { ...existing, placeholderText: incoming.placeholderText };
|
||||||
|
}
|
||||||
|
if (existing.kind === "badges" && incoming.kind === "badges") {
|
||||||
|
return { ...existing, options: [...incoming.options] };
|
||||||
|
}
|
||||||
|
if (existing.kind === "proportion" && incoming.kind === "proportion") {
|
||||||
|
return { ...existing, defaultPercent: incoming.defaultPercent };
|
||||||
|
}
|
||||||
|
if (existing.kind === "upload" && incoming.kind === "upload") {
|
||||||
|
return {
|
||||||
|
...existing,
|
||||||
|
fileName: incoming.fileName,
|
||||||
|
...(incoming.assetUrl !== undefined
|
||||||
|
? { assetUrl: incoming.assetUrl }
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return incoming;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep extra wizard-only blocks, but refresh facet-* field values from the
|
||||||
|
* current modal editors so Customize does not show a stale copy.
|
||||||
|
*/
|
||||||
|
export function overlayFacetPrefillValues(
|
||||||
|
blocks: CustomMethodCardFieldBlock[],
|
||||||
|
prefill: MethodCardWizardFacetPrefill,
|
||||||
|
): CustomMethodCardFieldBlock[] {
|
||||||
|
const incoming = mapFacetPrefillToWizardFieldBlocks(prefill);
|
||||||
|
const incomingById = new Map(incoming.map((block) => [block.id, block]));
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const next = blocks.map((block) => {
|
||||||
|
const overlay = incomingById.get(block.id);
|
||||||
|
if (!overlay) {
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
seen.add(block.id);
|
||||||
|
return overlayBlockValue(block, overlay);
|
||||||
|
});
|
||||||
|
for (const block of incoming) {
|
||||||
|
if (seen.has(block.id) || !facetBlockHasContent(block)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
next.push(block);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
function textPlaceholderForBlockId(
|
||||||
|
blocks: CustomMethodCardFieldBlock[],
|
||||||
|
id: string,
|
||||||
|
): string | undefined {
|
||||||
|
const block = blocks.find((b) => b.id === id);
|
||||||
|
if (!block || block.kind !== "text") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return block.placeholderText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Map Customize wizard field blocks back onto meaning/signals for a value chip. */
|
||||||
|
export function coreValueDetailsFromWizardFieldBlocks(
|
||||||
|
blocks: CustomMethodCardFieldBlock[],
|
||||||
|
fallback: CoreValueDetailEntry,
|
||||||
|
): CoreValueDetailEntry {
|
||||||
|
return {
|
||||||
|
meaning: textPlaceholderForBlockId(blocks, "facet-meaning") ?? fallback.meaning,
|
||||||
|
signals:
|
||||||
|
textPlaceholderForBlockId(blocks, "facet-signals") ?? fallback.signals,
|
||||||
|
...(fallback.supportText !== undefined
|
||||||
|
? { supportText: fallback.supportText }
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Title, support text, and field blocks for the custom-policy wizard, preferring
|
||||||
|
* in-modal drafts over persisted meta/blocks. When the card has no wizard
|
||||||
|
* blocks yet, facet body fields are mapped onto step-3 blocks.
|
||||||
|
*/
|
||||||
|
export function buildMethodCardWizardInitialValues(args: {
|
||||||
|
cardId: string;
|
||||||
|
fallbackTitle: string;
|
||||||
|
fallbackDescription: string;
|
||||||
|
meta: CreateFlowState["customMethodCardMetaById"];
|
||||||
|
persistedBlocks: CreateFlowState["customMethodCardFieldBlocksById"];
|
||||||
|
draftFieldBlocks: CustomMethodCardFieldBlock[] | null;
|
||||||
|
facetPrefill?: MethodCardWizardFacetPrefill;
|
||||||
|
}): MethodCardWizardInitialValues {
|
||||||
|
const metaRow = args.meta?.[args.cardId];
|
||||||
|
const persisted = args.persistedBlocks?.[args.cardId];
|
||||||
|
let fieldBlocks =
|
||||||
|
args.draftFieldBlocks !== null
|
||||||
|
? structuredClone(args.draftFieldBlocks)
|
||||||
|
: Array.isArray(persisted) && persisted.length > 0
|
||||||
|
? structuredClone(persisted)
|
||||||
|
: [];
|
||||||
|
if (fieldBlocks.length === 0 && args.facetPrefill) {
|
||||||
|
fieldBlocks = facetDetailsToWizardFieldBlocks(args.facetPrefill);
|
||||||
|
} else if (fieldBlocks.length > 0 && args.facetPrefill) {
|
||||||
|
fieldBlocks = overlayFacetPrefillValues(fieldBlocks, args.facetPrefill);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
title: metaRow?.label ?? args.fallbackTitle,
|
||||||
|
description: metaRow?.supportText ?? args.fallbackDescription,
|
||||||
|
fieldBlocks,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -14,6 +14,9 @@ import { isCustomMethodCardId } from "./isCustomMethodCardId";
|
|||||||
* stubs keep the facet's structured edit fields until the user adds blocks (then this
|
* stubs keep the facet's structured edit fields until the user adds blocks (then this
|
||||||
* returns true once persisted blocks are non-empty).
|
* returns true once persisted blocks are non-empty).
|
||||||
*
|
*
|
||||||
|
* Non-empty **draft** blocks also win, including catalog ids after Customize
|
||||||
|
* Finalize (facet editors ignore array order).
|
||||||
|
*
|
||||||
* **View mode** (`modalEditUnlocked` false): when the custom card still has facet copy
|
* **View mode** (`modalEditUnlocked` false): when the custom card still has facet copy
|
||||||
* that matches preset seeds only (see `./methodCardFacetMatchesPresetForId`), route to
|
* that matches preset seeds only (see `./methodCardFacetMatchesPresetForId`), route to
|
||||||
* {@link CustomMethodCardModalBody} so meta-only wizard cards show policy copy instead
|
* {@link CustomMethodCardModalBody} so meta-only wizard cards show policy copy instead
|
||||||
@@ -33,16 +36,15 @@ export function usesWizardFieldBlocksModalBody(args: {
|
|||||||
if (Array.isArray(persisted) && persisted.length > 0) {
|
if (Array.isArray(persisted) && persisted.length > 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (!isCustomMethodCardId(args.methodId, args.meta)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (
|
if (
|
||||||
args.modalEditUnlocked &&
|
|
||||||
args.draftFieldBlocks !== null &&
|
args.draftFieldBlocks !== null &&
|
||||||
args.draftFieldBlocks.length > 0
|
args.draftFieldBlocks.length > 0
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (!isCustomMethodCardId(args.methodId, args.meta)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
!args.modalEditUnlocked && args.customFacetDetailsMatchPreset === true
|
!args.modalEditUnlocked && args.customFacetDetailsMatchPreset === true
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const communityStructureChipSnapshotsSchema = z
|
|||||||
const coreValueDetailEntrySchema = z.object({
|
const coreValueDetailEntrySchema = z.object({
|
||||||
meaning: z.string().max(8000),
|
meaning: z.string().max(8000),
|
||||||
signals: z.string().max(8000),
|
signals: z.string().max(8000),
|
||||||
|
supportText: z.string().max(2000).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
},
|
},
|
||||||
"fieldModals": {
|
"fieldModals": {
|
||||||
"addField": "Add field",
|
"addField": "Add field",
|
||||||
|
"saveField": "Save field",
|
||||||
"requiredHint": "Required",
|
"requiredHint": "Required",
|
||||||
"text": {
|
"text": {
|
||||||
"title": "Add text block",
|
"title": "Add text block",
|
||||||
|
|||||||
@@ -75,20 +75,16 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const dialog = await screen.findByRole("dialog");
|
const dialog = await screen.findByRole("dialog");
|
||||||
fireEvent.click(within(dialog).getByRole("button", { name: "More options" }));
|
const textboxes = within(dialog).getAllByRole("textbox");
|
||||||
fireEvent.click(screen.getByRole("menuitem", { name: "Customize" }));
|
expect(textboxes.length).toBe(3);
|
||||||
|
const corePrincipleField = textboxes[0] as HTMLTextAreaElement;
|
||||||
const textboxes = within(screen.getByRole("dialog")).getAllByRole("textbox");
|
|
||||||
expect(textboxes.length).toBe(5);
|
|
||||||
const corePrincipleField = textboxes[2] as HTMLTextAreaElement;
|
|
||||||
// Preset corePrinciple must seed into the first body textarea so the user
|
// Preset corePrinciple must seed into the first body textarea so the user
|
||||||
// edits a real starting point rather than an empty field.
|
// edits a real starting point rather than an empty field.
|
||||||
expect(corePrincipleField.value.length).toBeGreaterThan(0);
|
expect(corePrincipleField.value.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
fireEvent.change(corePrincipleField, { target: { value: "Custom principle" } });
|
fireEvent.change(corePrincipleField, { target: { value: "Custom principle" } });
|
||||||
fireEvent.click(within(dialog).getByRole("button", { name: "Save" }));
|
|
||||||
fireEvent.click(
|
fireEvent.click(
|
||||||
within(screen.getByRole("dialog")).getByRole("button", {
|
within(dialog).getByRole("button", {
|
||||||
name: "Add Platform",
|
name: "Add Platform",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -162,7 +158,30 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
|
|||||||
expect(textareas[2].value).toBe("Saved coc");
|
expect(textareas[2].value).toBe("Saved coc");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Cancel customize reverts edited preset without persisting (no confirm when unchanged)", async () => {
|
it("opens meaning fields editable without Customize", async () => {
|
||||||
|
render(
|
||||||
|
<ScreenWithStateProbe
|
||||||
|
onState={() => {
|
||||||
|
/* noop */
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(
|
||||||
|
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0],
|
||||||
|
);
|
||||||
|
const dialog = await screen.findByRole("dialog");
|
||||||
|
const textareas = within(dialog).getAllByRole(
|
||||||
|
"textbox",
|
||||||
|
) as HTMLTextAreaElement[];
|
||||||
|
expect(textareas[0]).not.toBeDisabled();
|
||||||
|
fireEvent.click(within(dialog).getByRole("button", { name: "More options" }));
|
||||||
|
expect(
|
||||||
|
screen.getByRole("menuitem", { name: "Customize" }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("closing the wizard without Finalize leaves the card modal and does not persist", async () => {
|
||||||
let latest: CreateFlowState = {};
|
let latest: CreateFlowState = {};
|
||||||
render(
|
render(
|
||||||
<ScreenWithStateProbe
|
<ScreenWithStateProbe
|
||||||
@@ -179,20 +198,84 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
|
|||||||
fireEvent.click(within(dialog).getByRole("button", { name: "More options" }));
|
fireEvent.click(within(dialog).getByRole("button", { name: "More options" }));
|
||||||
fireEvent.click(screen.getByRole("menuitem", { name: "Customize" }));
|
fireEvent.click(screen.getByRole("menuitem", { name: "Customize" }));
|
||||||
|
|
||||||
fireEvent.click(within(dialog).getByRole("button", { name: "Cancel" }));
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
|
||||||
expect(
|
expect(
|
||||||
(within(screen.getByRole("dialog")).getAllByRole(
|
await screen.findByPlaceholderText("Policy name"),
|
||||||
"textbox",
|
).toBeInTheDocument();
|
||||||
)[0] as HTMLTextAreaElement).disabled,
|
fireEvent.keyDown(document, { key: "Escape" });
|
||||||
).toBe(true);
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByPlaceholderText("Policy name")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
expect(latest.communicationMethodDetailsById).toBeUndefined();
|
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
||||||
expect(screen.queryByRole("button", { name: "Discard" })).not.toBeInTheDocument();
|
expect(latest.customMethodCardMetaById).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Cancel customize with edits restores snapshot after confirm", async () => {
|
it("Back from the wizard with edits asks to discard and does not persist", async () => {
|
||||||
|
let latest: CreateFlowState = {};
|
||||||
|
render(
|
||||||
|
<ScreenWithStateProbe
|
||||||
|
onState={(s) => {
|
||||||
|
latest = s;
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(
|
||||||
|
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0],
|
||||||
|
);
|
||||||
|
const dialog = await screen.findByRole("dialog");
|
||||||
|
fireEvent.click(within(dialog).getByRole("button", { name: "More options" }));
|
||||||
|
fireEvent.click(screen.getByRole("menuitem", { name: "Customize" }));
|
||||||
|
|
||||||
|
const nameInput = await screen.findByPlaceholderText("Policy name");
|
||||||
|
fireEvent.change(nameInput, { target: { value: "Renamed in wizard" } });
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Back" }));
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await screen.findByRole("button", { name: "Keep editing" }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Discard" }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByPlaceholderText("Policy name")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
||||||
|
expect(latest.customMethodCardMetaById).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Escape after a field edit stays open when user declines discard confirm", async () => {
|
||||||
|
render(
|
||||||
|
<ScreenWithStateProbe
|
||||||
|
onState={() => {
|
||||||
|
/* noop */
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(
|
||||||
|
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0],
|
||||||
|
);
|
||||||
|
const dialog = await screen.findByRole("dialog");
|
||||||
|
const textboxes = within(dialog).getAllByRole(
|
||||||
|
"textbox",
|
||||||
|
) as HTMLTextAreaElement[];
|
||||||
|
fireEvent.change(textboxes[0], { target: { value: "Edited principle" } });
|
||||||
|
|
||||||
|
fireEvent.keyDown(document, { key: "Escape" });
|
||||||
|
const keepEditing = await screen.findByRole("button", {
|
||||||
|
name: "Keep editing",
|
||||||
|
});
|
||||||
|
expect(keepEditing.parentElement).toHaveClass(
|
||||||
|
"absolute",
|
||||||
|
"left-[16px]",
|
||||||
|
"top-[12px]",
|
||||||
|
);
|
||||||
|
await declineDiscardCustomizeEdits();
|
||||||
|
|
||||||
|
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Escape after a field edit discards without persisting when confirmed", async () => {
|
||||||
let latest: CreateFlowState = {};
|
let latest: CreateFlowState = {};
|
||||||
render(
|
render(
|
||||||
<ScreenWithStateProbe
|
<ScreenWithStateProbe
|
||||||
@@ -216,67 +299,22 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
|
|||||||
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0],
|
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0],
|
||||||
);
|
);
|
||||||
const dialog = await screen.findByRole("dialog");
|
const dialog = await screen.findByRole("dialog");
|
||||||
fireEvent.click(within(dialog).getByRole("button", { name: "More options" }));
|
|
||||||
fireEvent.click(screen.getByRole("menuitem", { name: "Customize" }));
|
|
||||||
|
|
||||||
const textboxes = within(dialog).getAllByRole(
|
const textboxes = within(dialog).getAllByRole(
|
||||||
"textbox",
|
"textbox",
|
||||||
) as HTMLTextAreaElement[];
|
) as HTMLTextAreaElement[];
|
||||||
fireEvent.change(textboxes[2], { target: { value: "Edited principle" } });
|
fireEvent.change(textboxes[0], { target: { value: "Edited principle" } });
|
||||||
|
fireEvent.keyDown(document, { key: "Escape" });
|
||||||
fireEvent.click(within(dialog).getByRole("button", { name: "Cancel" }));
|
|
||||||
await confirmDiscardCustomizeEdits();
|
await confirmDiscardCustomizeEdits();
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||||
(
|
|
||||||
within(screen.getByRole("dialog")).getAllByRole(
|
|
||||||
"textbox",
|
|
||||||
)[0] as HTMLTextAreaElement
|
|
||||||
).value,
|
|
||||||
).toBe("Saved principle");
|
|
||||||
});
|
});
|
||||||
expect(
|
expect(
|
||||||
latest.communicationMethodDetailsById?.signal?.corePrinciple,
|
latest.communicationMethodDetailsById?.signal?.corePrinciple,
|
||||||
).toBe("Saved principle");
|
).toBe("Saved principle");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("dirty Escape close stays open when user declines discard confirm", async () => {
|
it("Customize wizard prefills the card title and persists a rename on Finalize", async () => {
|
||||||
render(
|
|
||||||
<ScreenWithStateProbe
|
|
||||||
onState={() => {
|
|
||||||
/* noop */
|
|
||||||
}}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
fireEvent.click(
|
|
||||||
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0],
|
|
||||||
);
|
|
||||||
const dialog = await screen.findByRole("dialog");
|
|
||||||
fireEvent.click(within(dialog).getByRole("button", { name: "More options" }));
|
|
||||||
fireEvent.click(screen.getByRole("menuitem", { name: "Customize" }));
|
|
||||||
|
|
||||||
const textboxes = within(dialog).getAllByRole(
|
|
||||||
"textbox",
|
|
||||||
) as HTMLTextAreaElement[];
|
|
||||||
fireEvent.change(textboxes[2], { target: { value: "Edited principle" } });
|
|
||||||
|
|
||||||
fireEvent.keyDown(document, { key: "Escape" });
|
|
||||||
const keepEditing = await screen.findByRole("button", {
|
|
||||||
name: "Keep editing",
|
|
||||||
});
|
|
||||||
expect(keepEditing.parentElement).toHaveClass(
|
|
||||||
"absolute",
|
|
||||||
"left-[16px]",
|
|
||||||
"top-[12px]",
|
|
||||||
);
|
|
||||||
await declineDiscardCustomizeEdits();
|
|
||||||
|
|
||||||
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("persists customized policy title for a custom UUID card on Save", async () => {
|
|
||||||
const customId = "00000000-0000-4000-8000-0000000000aa";
|
const customId = "00000000-0000-4000-8000-0000000000aa";
|
||||||
let latest: CreateFlowState = {};
|
let latest: CreateFlowState = {};
|
||||||
render(
|
render(
|
||||||
@@ -307,13 +345,21 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
|
|||||||
fireEvent.click(within(dialog).getByRole("button", { name: "More options" }));
|
fireEvent.click(within(dialog).getByRole("button", { name: "More options" }));
|
||||||
fireEvent.click(screen.getByRole("menuitem", { name: "Customize" }));
|
fireEvent.click(screen.getByRole("menuitem", { name: "Customize" }));
|
||||||
|
|
||||||
const titleInput = within(screen.getByRole("dialog")).getAllByRole(
|
expect(
|
||||||
"textbox",
|
await screen.findByPlaceholderText("Policy name"),
|
||||||
)[0] as HTMLInputElement;
|
).toBeInTheDocument();
|
||||||
fireEvent.change(titleInput, { target: { value: "Renamed policy" } });
|
const nameInput = screen.getByPlaceholderText("Policy name");
|
||||||
fireEvent.click(
|
expect(nameInput).toHaveValue("Original title");
|
||||||
within(screen.getByRole("dialog")).getByRole("button", { name: "Save" }),
|
fireEvent.change(nameInput, { target: { value: "Renamed policy" } });
|
||||||
);
|
fireEvent.click(screen.getByRole("button", { name: "Next" }));
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Next" }));
|
||||||
|
expect(
|
||||||
|
await screen.findByText("Custom policy details"),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByRole("button", { name: "Core Principle & Scope" }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Finalize" }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(latest.customMethodCardMetaById?.[customId]?.label).toBe(
|
expect(latest.customMethodCardMetaById?.[customId]?.label).toBe(
|
||||||
@@ -322,7 +368,7 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("stores preset id title override in customMethodCardMetaById on Save", async () => {
|
it("stores preset id title override in customMethodCardMetaById on Finalize", async () => {
|
||||||
let latest: CreateFlowState = {};
|
let latest: CreateFlowState = {};
|
||||||
render(
|
render(
|
||||||
<ScreenWithStateProbe
|
<ScreenWithStateProbe
|
||||||
@@ -339,15 +385,32 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
|
|||||||
fireEvent.click(within(dialog).getByRole("button", { name: "More options" }));
|
fireEvent.click(within(dialog).getByRole("button", { name: "More options" }));
|
||||||
fireEvent.click(screen.getByRole("menuitem", { name: "Customize" }));
|
fireEvent.click(screen.getByRole("menuitem", { name: "Customize" }));
|
||||||
|
|
||||||
const titleInput = within(screen.getByRole("dialog")).getAllByRole(
|
expect(
|
||||||
"textbox",
|
await screen.findByPlaceholderText("Policy name"),
|
||||||
)[0] as HTMLInputElement;
|
).toHaveValue("Signal");
|
||||||
fireEvent.change(titleInput, {
|
fireEvent.click(screen.getByRole("button", { name: "Next" }));
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Next" }));
|
||||||
|
expect(
|
||||||
|
await screen.findByText("Custom policy details"),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
fireEvent.click(
|
||||||
|
screen.getByRole("button", { name: "Core Principle & Scope" }),
|
||||||
|
);
|
||||||
|
expect(await screen.findByText("Add text block")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByDisplayValue(/We prioritize privacy and security/i),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Back" }));
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Back" }));
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Back" }));
|
||||||
|
const nameInput = await screen.findByPlaceholderText("Policy name");
|
||||||
|
expect(nameInput).toHaveValue("Signal");
|
||||||
|
fireEvent.change(nameInput, {
|
||||||
target: { value: "Custom Signal header" },
|
target: { value: "Custom Signal header" },
|
||||||
});
|
});
|
||||||
fireEvent.click(
|
fireEvent.click(screen.getByRole("button", { name: "Next" }));
|
||||||
within(screen.getByRole("dialog")).getByRole("button", { name: "Save" }),
|
fireEvent.click(screen.getByRole("button", { name: "Next" }));
|
||||||
);
|
fireEvent.click(screen.getByRole("button", { name: "Finalize" }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(latest.customMethodCardMetaById?.signal?.label).toBe(
|
expect(latest.customMethodCardMetaById?.signal?.label).toBe(
|
||||||
@@ -355,4 +418,79 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Customize Finalize shows reordered field blocks in the card modal", async () => {
|
||||||
|
let latest: CreateFlowState = {};
|
||||||
|
render(
|
||||||
|
<ScreenWithStateProbe
|
||||||
|
onState={(s) => {
|
||||||
|
latest = s;
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(
|
||||||
|
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0],
|
||||||
|
);
|
||||||
|
const cardDialog = await screen.findByRole("dialog");
|
||||||
|
fireEvent.click(
|
||||||
|
within(cardDialog).getByRole("button", { name: "More options" }),
|
||||||
|
);
|
||||||
|
fireEvent.click(screen.getByRole("menuitem", { name: "Customize" }));
|
||||||
|
|
||||||
|
fireEvent.click(await screen.findByRole("button", { name: "Next" }));
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Next" }));
|
||||||
|
expect(
|
||||||
|
await screen.findByText("Custom policy details"),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
const handles = screen.getAllByRole("button", {
|
||||||
|
name: "Drag to reorder this field",
|
||||||
|
});
|
||||||
|
const rows = screen.getAllByRole("listitem");
|
||||||
|
const store: Record<string, string> = {};
|
||||||
|
const dataTransfer = {
|
||||||
|
effectAllowed: "all",
|
||||||
|
dropEffect: "move",
|
||||||
|
setData(type: string, value: string) {
|
||||||
|
store[type] = value;
|
||||||
|
},
|
||||||
|
getData(type: string) {
|
||||||
|
return store[type] ?? "";
|
||||||
|
},
|
||||||
|
};
|
||||||
|
fireEvent.pointerDown(handles[0]);
|
||||||
|
fireEvent.dragStart(rows[0], { dataTransfer });
|
||||||
|
fireEvent.drop(rows[2], { dataTransfer });
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Finalize" }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.queryByPlaceholderText("Policy name"),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
latest.customMethodCardFieldBlocksById?.signal?.map((b) => b.id),
|
||||||
|
).toEqual([
|
||||||
|
"facet-logisticsAdmin",
|
||||||
|
"facet-codeOfConduct",
|
||||||
|
"facet-corePrinciple",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = screen.getByRole("dialog");
|
||||||
|
const labels = within(result)
|
||||||
|
.getAllByRole("textbox")
|
||||||
|
.map((el) => {
|
||||||
|
const labelledby = el.getAttribute("aria-labelledby");
|
||||||
|
return labelledby
|
||||||
|
? (document.getElementById(labelledby)?.textContent ?? "").trim()
|
||||||
|
: "";
|
||||||
|
});
|
||||||
|
expect(labels[0]).toMatch(/Logistics, Admin/);
|
||||||
|
expect(labels[1]).toMatch(/Code of Conduct/);
|
||||||
|
expect(labels[2]).toMatch(/Core Principle/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ describe("CoreValuesSelectScreen", () => {
|
|||||||
fireEvent.click(await screen.findByRole("button", { name: "Discard" }));
|
fireEvent.click(await screen.findByRole("button", { name: "Discard" }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function editMeaningInOpenDialog(next: string) {
|
||||||
|
const dialog = await screen.findByRole("dialog");
|
||||||
|
const fields = within(dialog).getAllByRole("textbox");
|
||||||
|
fireEvent.change(fields[0], { target: { value: next } });
|
||||||
|
}
|
||||||
|
|
||||||
it("opens core value detail modal when a preset chip is clicked", async () => {
|
it("opens core value detail modal when a preset chip is clicked", async () => {
|
||||||
renderWithProviders(<CoreValuesSelectScreen />);
|
renderWithProviders(<CoreValuesSelectScreen />);
|
||||||
fireEvent.click(screen.getByText("Accessibility"));
|
fireEvent.click(screen.getByText("Accessibility"));
|
||||||
@@ -20,20 +26,36 @@ describe("CoreValuesSelectScreen", () => {
|
|||||||
expect(
|
expect(
|
||||||
within(dialog).getByRole("button", { name: "Add Value" }),
|
within(dialog).getByRole("button", { name: "Add Value" }),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(
|
|
||||||
screen.queryByRole("menuitem", { name: "Customize" }),
|
|
||||||
).not.toBeInTheDocument();
|
|
||||||
fireEvent.click(within(dialog).getByRole("button", { name: "More options" }));
|
fireEvent.click(within(dialog).getByRole("button", { name: "More options" }));
|
||||||
expect(
|
expect(
|
||||||
screen.queryByRole("menuitem", { name: "Customize" }),
|
screen.getByRole("menuitem", { name: "Customize" }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByRole("menuitem", { name: "Duplicate" }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.queryByRole("menuitem", { name: "Remove" }),
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("asks to discard when closing a pending value, then unselects on Discard", async () => {
|
it("closes a pending value without confirm when fields are unchanged", async () => {
|
||||||
renderWithProviders(<CoreValuesSelectScreen />);
|
renderWithProviders(<CoreValuesSelectScreen />);
|
||||||
fireEvent.click(screen.getByText("Accessibility"));
|
fireEvent.click(screen.getByText("Accessibility"));
|
||||||
await screen.findByRole("dialog");
|
await screen.findByRole("dialog");
|
||||||
fireEvent.keyDown(document, { key: "Escape" });
|
fireEvent.keyDown(document, { key: "Escape" });
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
screen.queryByRole("button", { name: "Keep editing" }),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("asks to discard when closing a pending value after an edit", async () => {
|
||||||
|
renderWithProviders(<CoreValuesSelectScreen />);
|
||||||
|
fireEvent.click(screen.getByText("Accessibility"));
|
||||||
|
await editMeaningInOpenDialog("Changed meaning");
|
||||||
|
fireEvent.keyDown(document, { key: "Escape" });
|
||||||
expect(
|
expect(
|
||||||
await screen.findByRole("button", { name: "Keep editing" }),
|
await screen.findByRole("button", { name: "Keep editing" }),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
@@ -46,7 +68,7 @@ describe("CoreValuesSelectScreen", () => {
|
|||||||
it("keeps the pending value modal open when Keep editing is chosen", async () => {
|
it("keeps the pending value modal open when Keep editing is chosen", async () => {
|
||||||
renderWithProviders(<CoreValuesSelectScreen />);
|
renderWithProviders(<CoreValuesSelectScreen />);
|
||||||
fireEvent.click(screen.getByText("Accessibility"));
|
fireEvent.click(screen.getByText("Accessibility"));
|
||||||
await screen.findByRole("dialog");
|
await editMeaningInOpenDialog("Changed meaning");
|
||||||
fireEvent.keyDown(document, { key: "Escape" });
|
fireEvent.keyDown(document, { key: "Escape" });
|
||||||
fireEvent.click(await screen.findByRole("button", { name: "Keep editing" }));
|
fireEvent.click(await screen.findByRole("button", { name: "Keep editing" }));
|
||||||
const dialog = await screen.findByRole("dialog");
|
const dialog = await screen.findByRole("dialog");
|
||||||
@@ -78,6 +100,162 @@ describe("CoreValuesSelectScreen", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows Remove only after the value has been added", async () => {
|
||||||
|
renderWithProviders(<CoreValuesSelectScreen />);
|
||||||
|
fireEvent.click(screen.getByText("Accessibility"));
|
||||||
|
const pending = await screen.findByRole("dialog");
|
||||||
|
fireEvent.click(within(pending).getByRole("button", { name: "Add Value" }));
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
fireEvent.click(screen.getByText("Accessibility"));
|
||||||
|
const editing = await screen.findByRole("dialog");
|
||||||
|
fireEvent.click(
|
||||||
|
within(editing).getByRole("button", { name: "More options" }),
|
||||||
|
);
|
||||||
|
expect(screen.getByRole("menuitem", { name: "Remove" })).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByRole("menuitem", { name: "Customize" }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByRole("menuitem", { name: "Duplicate" }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Customize walks name and description before policy details", async () => {
|
||||||
|
renderWithProviders(<CoreValuesSelectScreen />);
|
||||||
|
fireEvent.click(screen.getByText("Accessibility"));
|
||||||
|
const dialog = await screen.findByRole("dialog");
|
||||||
|
fireEvent.click(
|
||||||
|
within(dialog).getByRole("button", { name: "More options" }),
|
||||||
|
);
|
||||||
|
fireEvent.click(screen.getByRole("menuitem", { name: "Customize" }));
|
||||||
|
|
||||||
|
expect(await screen.findByPlaceholderText("Policy name")).toHaveValue(
|
||||||
|
"Accessibility",
|
||||||
|
);
|
||||||
|
expect(screen.queryByText("Custom policy details")).not.toBeInTheDocument();
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Next" }));
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Next" }));
|
||||||
|
expect(
|
||||||
|
await screen.findByText("Custom policy details"),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByRole("button", {
|
||||||
|
name: "What does this value mean to your group?",
|
||||||
|
}),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Customize shows meaning saved from the value modal", async () => {
|
||||||
|
renderWithProviders(<CoreValuesSelectScreen />);
|
||||||
|
fireEvent.click(screen.getByText("Accessibility"));
|
||||||
|
const pending = await screen.findByRole("dialog");
|
||||||
|
const fields = within(pending).getAllByRole("textbox");
|
||||||
|
fireEvent.change(fields[0], {
|
||||||
|
target: { value: "Edited meaning from value modal" },
|
||||||
|
});
|
||||||
|
fireEvent.click(within(pending).getByRole("button", { name: "Add Value" }));
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
fireEvent.click(screen.getByText("Accessibility"));
|
||||||
|
const editing = await screen.findByRole("dialog");
|
||||||
|
fireEvent.click(
|
||||||
|
within(editing).getByRole("button", { name: "More options" }),
|
||||||
|
);
|
||||||
|
fireEvent.click(screen.getByRole("menuitem", { name: "Customize" }));
|
||||||
|
expect(await screen.findByPlaceholderText("Policy name")).toBeInTheDocument();
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Next" }));
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Next" }));
|
||||||
|
fireEvent.click(
|
||||||
|
await screen.findByRole("button", {
|
||||||
|
name: "What does this value mean to your group?",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.getByDisplayValue("Edited meaning from value modal"),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("closing Customize without Finalize returns to the value modal", async () => {
|
||||||
|
renderWithProviders(<CoreValuesSelectScreen />);
|
||||||
|
fireEvent.click(screen.getByText("Accessibility"));
|
||||||
|
const dialog = await screen.findByRole("dialog");
|
||||||
|
fireEvent.click(
|
||||||
|
within(dialog).getByRole("button", { name: "More options" }),
|
||||||
|
);
|
||||||
|
fireEvent.click(screen.getByRole("menuitem", { name: "Customize" }));
|
||||||
|
expect(
|
||||||
|
await screen.findByPlaceholderText("Policy name"),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
fireEvent.keyDown(document, { key: "Escape" });
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.queryByPlaceholderText("Policy name"),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
const valueDialog = screen.getByRole("dialog");
|
||||||
|
expect(
|
||||||
|
within(valueDialog).getByRole("button", { name: "Add Value" }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Customize Finalize shows reordered fields in the value modal", async () => {
|
||||||
|
renderWithProviders(<CoreValuesSelectScreen />);
|
||||||
|
fireEvent.click(screen.getByText("Accessibility"));
|
||||||
|
const dialog = await screen.findByRole("dialog");
|
||||||
|
fireEvent.click(
|
||||||
|
within(dialog).getByRole("button", { name: "More options" }),
|
||||||
|
);
|
||||||
|
fireEvent.click(screen.getByRole("menuitem", { name: "Customize" }));
|
||||||
|
|
||||||
|
fireEvent.click(await screen.findByRole("button", { name: "Next" }));
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Next" }));
|
||||||
|
expect(
|
||||||
|
await screen.findByText("Custom policy details"),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
const handles = screen.getAllByRole("button", {
|
||||||
|
name: "Drag to reorder this field",
|
||||||
|
});
|
||||||
|
const rows = screen.getAllByRole("listitem");
|
||||||
|
const store: Record<string, string> = {};
|
||||||
|
const dataTransfer = {
|
||||||
|
effectAllowed: "all",
|
||||||
|
dropEffect: "move",
|
||||||
|
setData(type: string, value: string) {
|
||||||
|
store[type] = value;
|
||||||
|
},
|
||||||
|
getData(type: string) {
|
||||||
|
return store[type] ?? "";
|
||||||
|
},
|
||||||
|
};
|
||||||
|
fireEvent.pointerDown(handles[0]);
|
||||||
|
fireEvent.dragStart(rows[0], { dataTransfer });
|
||||||
|
fireEvent.drop(rows[1], { dataTransfer });
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Finalize" }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.queryByPlaceholderText("Policy name"),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = screen.getByRole("dialog");
|
||||||
|
const labels = within(result)
|
||||||
|
.getAllByRole("textbox")
|
||||||
|
.map((el) => {
|
||||||
|
const labelledby = el.getAttribute("aria-labelledby");
|
||||||
|
return labelledby
|
||||||
|
? (document.getElementById(labelledby)?.textContent ?? "").trim()
|
||||||
|
: "";
|
||||||
|
});
|
||||||
|
expect(labels[0]).toMatch(/Signals of Violation/);
|
||||||
|
expect(labels[1]).toMatch(/What does this value mean to your group/);
|
||||||
|
});
|
||||||
|
|
||||||
// The "Add value" → custom-chip → modal flow uses a `customPending`
|
// The "Add value" → custom-chip → modal flow uses a `customPending`
|
||||||
// session: dismissing the modal must drop the brand-new chip entirely
|
// session: dismissing the modal must drop the brand-new chip entirely
|
||||||
// (not just unselect it), because the user never confirmed it via
|
// (not just unselect it), because the user never confirmed it via
|
||||||
@@ -118,7 +296,6 @@ describe("CoreValuesSelectScreen", () => {
|
|||||||
expect(countCustomChips(CUSTOM_LABEL)).toBe(1);
|
expect(countCustomChips(CUSTOM_LABEL)).toBe(1);
|
||||||
|
|
||||||
fireEvent.keyDown(document, { key: "Escape" });
|
fireEvent.keyDown(document, { key: "Escape" });
|
||||||
fireEvent.click(await screen.findByRole("button", { name: "Discard" }));
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import { describe, it, expect, vi } from "vitest";
|
||||||
|
import "@testing-library/jest-dom/vitest";
|
||||||
|
import {
|
||||||
|
fireEvent,
|
||||||
|
renderWithProviders as render,
|
||||||
|
screen,
|
||||||
|
} from "../utils/test-utils";
|
||||||
|
import { CustomMethodCardWizardBlocksList } from "../../app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizardBlocksList.container";
|
||||||
|
import type { CustomMethodCardFieldBlock } from "../../lib/create/customMethodCardFieldBlocks";
|
||||||
|
|
||||||
|
function createDataTransfer() {
|
||||||
|
const store: Record<string, string> = {};
|
||||||
|
return {
|
||||||
|
effectAllowed: "all",
|
||||||
|
dropEffect: "move",
|
||||||
|
setData(type: string, value: string) {
|
||||||
|
store[type] = value;
|
||||||
|
},
|
||||||
|
getData(type: string) {
|
||||||
|
return store[type] ?? "";
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldTypeLabels = {
|
||||||
|
text: "Text",
|
||||||
|
badges: "Badges",
|
||||||
|
upload: "Upload",
|
||||||
|
proportion: "Proportion",
|
||||||
|
};
|
||||||
|
|
||||||
|
const blocks: CustomMethodCardFieldBlock[] = [
|
||||||
|
{
|
||||||
|
kind: "text",
|
||||||
|
id: "facet-meaning",
|
||||||
|
blockTitle: "Meaning",
|
||||||
|
placeholderText: "a",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "text",
|
||||||
|
id: "facet-signals",
|
||||||
|
blockTitle: "Signals",
|
||||||
|
placeholderText: "b",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
describe("CustomMethodCardWizardBlocksList", () => {
|
||||||
|
it("reorders when a row is dropped after dragging from the handle", () => {
|
||||||
|
const onBlocksReorder = vi.fn();
|
||||||
|
render(
|
||||||
|
<CustomMethodCardWizardBlocksList
|
||||||
|
blocks={blocks}
|
||||||
|
fieldTypeLabels={fieldTypeLabels}
|
||||||
|
dragHandleAriaLabel="Drag to reorder this field"
|
||||||
|
listLabel="Fields"
|
||||||
|
onBlocksReorder={onBlocksReorder}
|
||||||
|
onEditBlock={() => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handle = screen.getAllByRole("button", {
|
||||||
|
name: "Drag to reorder this field",
|
||||||
|
})[0];
|
||||||
|
const rows = screen.getAllByRole("listitem");
|
||||||
|
const dataTransfer = createDataTransfer();
|
||||||
|
|
||||||
|
fireEvent.pointerDown(handle);
|
||||||
|
fireEvent.dragStart(rows[0], { dataTransfer });
|
||||||
|
fireEvent.dragOver(rows[1], { dataTransfer });
|
||||||
|
fireEvent.drop(rows[1], { dataTransfer });
|
||||||
|
|
||||||
|
expect(onBlocksReorder).toHaveBeenCalledWith([blocks[1], blocks[0]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("edits a block when its title is clicked", () => {
|
||||||
|
const onEditBlock = vi.fn();
|
||||||
|
render(
|
||||||
|
<CustomMethodCardWizardBlocksList
|
||||||
|
blocks={blocks}
|
||||||
|
fieldTypeLabels={fieldTypeLabels}
|
||||||
|
dragHandleAriaLabel="Drag to reorder this field"
|
||||||
|
listLabel="Fields"
|
||||||
|
onBlocksReorder={() => {}}
|
||||||
|
onEditBlock={onEditBlock}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Meaning" }));
|
||||||
|
expect(onEditBlock).toHaveBeenCalledWith(blocks[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not reorder when the drag did not start from the handle", () => {
|
||||||
|
const onBlocksReorder = vi.fn();
|
||||||
|
render(
|
||||||
|
<CustomMethodCardWizardBlocksList
|
||||||
|
blocks={blocks}
|
||||||
|
fieldTypeLabels={fieldTypeLabels}
|
||||||
|
dragHandleAriaLabel="Drag to reorder this field"
|
||||||
|
listLabel="Fields"
|
||||||
|
onBlocksReorder={onBlocksReorder}
|
||||||
|
onEditBlock={() => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const rows = screen.getAllByRole("listitem");
|
||||||
|
const dataTransfer = createDataTransfer();
|
||||||
|
|
||||||
|
fireEvent.dragStart(rows[0], { dataTransfer });
|
||||||
|
fireEvent.drop(rows[1], { dataTransfer });
|
||||||
|
|
||||||
|
expect(onBlocksReorder).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -191,27 +191,7 @@ describe("FinalReviewScreen — prefilled selections", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("FinalReviewScreen — chip detail modal", () => {
|
describe("FinalReviewScreen — chip detail modal", () => {
|
||||||
async function enterMethodCustomizeFromDialog(dialog: HTMLElement) {
|
it("opens the chip modal when a chip is clicked, matching the preset copy", async () => {
|
||||||
fireEvent.click(
|
|
||||||
within(dialog).getByRole("button", { name: /more options/i }),
|
|
||||||
);
|
|
||||||
const customize = await screen.findByRole("menuitem", {
|
|
||||||
name: /^customize$/i,
|
|
||||||
});
|
|
||||||
fireEvent.click(customize);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function enterCoreValueCustomizeFromDialog(dialog: HTMLElement) {
|
|
||||||
fireEvent.click(
|
|
||||||
within(dialog).getByRole("button", { name: /more options/i }),
|
|
||||||
);
|
|
||||||
const customize = await screen.findByRole("menuitem", {
|
|
||||||
name: /^customize$/i,
|
|
||||||
});
|
|
||||||
fireEvent.click(customize);
|
|
||||||
}
|
|
||||||
|
|
||||||
it("opens the read-only detail modal when a chip is clicked, matching the preset copy", async () => {
|
|
||||||
render(<FinalReviewWithCustomizeSelections />);
|
render(<FinalReviewWithCustomizeSelections />);
|
||||||
|
|
||||||
const signalChip = await screen.findByRole("button", { name: "Signal" });
|
const signalChip = await screen.findByRole("button", { name: "Signal" });
|
||||||
@@ -273,35 +253,58 @@ describe("FinalReviewScreen — chip detail modal", () => {
|
|||||||
within(dialog).getByRole("button", { name: /more options/i }),
|
within(dialog).getByRole("button", { name: /more options/i }),
|
||||||
);
|
);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(
|
|
||||||
screen.getByRole("menuitem", { name: /^customize$/i }),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
expect(
|
expect(
|
||||||
screen.getByRole("menuitem", { name: /^duplicate$/i }),
|
screen.getByRole("menuitem", { name: /^duplicate$/i }),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
expect(
|
||||||
|
screen.getByRole("menuitem", { name: /^customize$/i }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it("opens method chip modal read-only until Customize, then enables Save after an edit", async () => {
|
it("values Customize starts on the policy name step", async () => {
|
||||||
|
function CoreValuesHarness() {
|
||||||
|
const { replaceState } = useCreateFlow();
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
replaceState({
|
||||||
|
selectedCoreValueIds: ["1"],
|
||||||
|
coreValuesChipsSnapshot: [
|
||||||
|
{ id: "1", label: "Accessibility", state: "selected" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}, [replaceState]);
|
||||||
|
return <FinalReviewScreen />;
|
||||||
|
}
|
||||||
|
render(<CoreValuesHarness />);
|
||||||
|
fireEvent.click(
|
||||||
|
await screen.findByRole("button", { name: "Accessibility" }),
|
||||||
|
);
|
||||||
|
const dialog = await screen.findByRole("dialog");
|
||||||
|
fireEvent.click(
|
||||||
|
within(dialog).getByRole("button", { name: /more options/i }),
|
||||||
|
);
|
||||||
|
fireEvent.click(
|
||||||
|
await screen.findByRole("menuitem", { name: /^customize$/i }),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
await screen.findByPlaceholderText("Policy name"),
|
||||||
|
).toHaveValue("Accessibility");
|
||||||
|
expect(screen.queryByText("Custom policy details")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("opens method chip modal editable, with Save disabled until a field changes", async () => {
|
||||||
render(<FinalReviewWithCustomizeSelections />);
|
render(<FinalReviewWithCustomizeSelections />);
|
||||||
|
|
||||||
fireEvent.click(await screen.findByRole("button", { name: "Signal" }));
|
fireEvent.click(await screen.findByRole("button", { name: "Signal" }));
|
||||||
const dialog = await screen.findByRole("dialog");
|
const dialog = await screen.findByRole("dialog");
|
||||||
|
|
||||||
expect(
|
const saveButton = within(dialog).getByRole("button", { name: "Save" });
|
||||||
within(dialog).queryByRole("button", { name: "Save" }),
|
expect(saveButton).toBeDisabled();
|
||||||
).not.toBeInTheDocument();
|
|
||||||
|
|
||||||
const principleField = within(dialog).getByRole("textbox", {
|
const principleField = within(dialog).getByRole("textbox", {
|
||||||
name: /core principle/i,
|
name: /core principle/i,
|
||||||
});
|
});
|
||||||
expect(principleField).toBeDisabled();
|
expect(principleField).not.toBeDisabled();
|
||||||
|
|
||||||
await enterMethodCustomizeFromDialog(dialog);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
within(dialog).getByRole("button", { name: "Save" }),
|
|
||||||
).toBeDisabled();
|
|
||||||
|
|
||||||
fireEvent.change(principleField, { target: { value: "Edited principle" } });
|
fireEvent.change(principleField, { target: { value: "Edited principle" } });
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -339,8 +342,8 @@ describe("FinalReviewScreen — chip detail modal", () => {
|
|||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("opens the editable Save modal for a values chip after Customize", async () => {
|
it("opens the editable Save modal for a values chip", async () => {
|
||||||
// Customize / plain custom-rule path: snapshot is set, sections is not.
|
// Plain custom-rule path: snapshot is set, sections is not.
|
||||||
function CoreValuesHarness() {
|
function CoreValuesHarness() {
|
||||||
const { replaceState } = useCreateFlow();
|
const { replaceState } = useCreateFlow();
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
@@ -359,10 +362,6 @@ describe("FinalReviewScreen — chip detail modal", () => {
|
|||||||
await screen.findByRole("button", { name: "Accessibility" }),
|
await screen.findByRole("button", { name: "Accessibility" }),
|
||||||
);
|
);
|
||||||
const dialog = await screen.findByRole("dialog");
|
const dialog = await screen.findByRole("dialog");
|
||||||
expect(
|
|
||||||
within(dialog).queryByRole("button", { name: "Save" }),
|
|
||||||
).not.toBeInTheDocument();
|
|
||||||
await enterCoreValueCustomizeFromDialog(dialog);
|
|
||||||
expect(
|
expect(
|
||||||
within(dialog).getByRole("button", { name: "Save" }),
|
within(dialog).getByRole("button", { name: "Save" }),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
@@ -371,7 +370,7 @@ describe("FinalReviewScreen — chip detail modal", () => {
|
|||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("opens Save for values chip after Customize (use-without-changes seeded snapshot)", async () => {
|
it("opens Save for a values chip (use-without-changes seeded snapshot)", async () => {
|
||||||
// Mirrors the post-fix payload from `handleUseTemplateWithoutChanges`:
|
// Mirrors the post-fix payload from `handleUseTemplateWithoutChanges`:
|
||||||
// template Values section is stripped from `sections`, snapshot +
|
// template Values section is stripped from `sections`, snapshot +
|
||||||
// selected ids are seeded so the chip carries an `overrideKey`.
|
// selected ids are seeded so the chip carries an `overrideKey`.
|
||||||
@@ -401,10 +400,6 @@ describe("FinalReviewScreen — chip detail modal", () => {
|
|||||||
await screen.findByRole("button", { name: "Accessibility" }),
|
await screen.findByRole("button", { name: "Accessibility" }),
|
||||||
);
|
);
|
||||||
const dialog = await screen.findByRole("dialog");
|
const dialog = await screen.findByRole("dialog");
|
||||||
expect(
|
|
||||||
within(dialog).queryByRole("button", { name: "Save" }),
|
|
||||||
).not.toBeInTheDocument();
|
|
||||||
await enterCoreValueCustomizeFromDialog(dialog);
|
|
||||||
expect(
|
expect(
|
||||||
within(dialog).getByRole("button", { name: "Save" }),
|
within(dialog).getByRole("button", { name: "Save" }),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
@@ -418,21 +413,10 @@ describe("FinalReviewScreen — chip detail modal", () => {
|
|||||||
*
|
*
|
||||||
* 1. Save starts disabled (no edits yet → nothing to persist).
|
* 1. Save starts disabled (no edits yet → nothing to persist).
|
||||||
* 2. Editing any field flips Save on; clicking it writes the typed
|
* 2. Editing any field flips Save on; clicking it writes the typed
|
||||||
* `{group}MethodDetailsById[id]` entry into create-flow state and
|
* `{group}MethodDetailsById[id]` entry into create-flow state.
|
||||||
* closes the modal.
|
|
||||||
* 3. Closing without Save discards every typed change.
|
* 3. Closing without Save discards every typed change.
|
||||||
*/
|
*/
|
||||||
describe("FinalReviewScreen — chip edit modal save semantics", () => {
|
describe("FinalReviewScreen — chip edit modal save semantics", () => {
|
||||||
async function enterMethodCustomizeFromDialog(dialog: HTMLElement) {
|
|
||||||
fireEvent.click(
|
|
||||||
within(dialog).getByRole("button", { name: /more options/i }),
|
|
||||||
);
|
|
||||||
const customize = await screen.findByRole("menuitem", {
|
|
||||||
name: /^customize$/i,
|
|
||||||
});
|
|
||||||
fireEvent.click(customize);
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseSelections: CreateFlowState = {
|
const baseSelections: CreateFlowState = {
|
||||||
title: "Oak Park Commons",
|
title: "Oak Park Commons",
|
||||||
selectedCommunicationMethodIds: ["signal"],
|
selectedCommunicationMethodIds: ["signal"],
|
||||||
@@ -452,7 +436,6 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
|
|||||||
|
|
||||||
fireEvent.click(await screen.findByRole("button", { name: "Signal" }));
|
fireEvent.click(await screen.findByRole("button", { name: "Signal" }));
|
||||||
const dialog = await screen.findByRole("dialog");
|
const dialog = await screen.findByRole("dialog");
|
||||||
await enterMethodCustomizeFromDialog(dialog);
|
|
||||||
const saveButton = within(dialog).getByRole("button", { name: "Save" });
|
const saveButton = within(dialog).getByRole("button", { name: "Save" });
|
||||||
expect(saveButton).toBeDisabled();
|
expect(saveButton).toBeDisabled();
|
||||||
const principleField = within(dialog).getByRole("textbox", {
|
const principleField = within(dialog).getByRole("textbox", {
|
||||||
@@ -483,7 +466,6 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
|
|||||||
|
|
||||||
fireEvent.click(await screen.findByRole("button", { name: "Signal" }));
|
fireEvent.click(await screen.findByRole("button", { name: "Signal" }));
|
||||||
const dialog = await screen.findByRole("dialog");
|
const dialog = await screen.findByRole("dialog");
|
||||||
await enterMethodCustomizeFromDialog(dialog);
|
|
||||||
const principleField = within(dialog).getByRole("textbox", {
|
const principleField = within(dialog).getByRole("textbox", {
|
||||||
name: /core principle/i,
|
name: /core principle/i,
|
||||||
});
|
});
|
||||||
@@ -492,13 +474,6 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
|
|||||||
});
|
});
|
||||||
fireEvent.click(within(dialog).getByRole("button", { name: "Save" }));
|
fireEvent.click(within(dialog).getByRole("button", { name: "Save" }));
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(
|
|
||||||
within(screen.getByRole("dialog")).queryByRole("button", {
|
|
||||||
name: "Save",
|
|
||||||
}),
|
|
||||||
).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(
|
expect(
|
||||||
latest.communicationMethodDetailsById?.signal?.corePrinciple,
|
latest.communicationMethodDetailsById?.signal?.corePrinciple,
|
||||||
@@ -519,7 +494,6 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
|
|||||||
|
|
||||||
fireEvent.click(await screen.findByRole("button", { name: "Signal" }));
|
fireEvent.click(await screen.findByRole("button", { name: "Signal" }));
|
||||||
const dialog = await screen.findByRole("dialog");
|
const dialog = await screen.findByRole("dialog");
|
||||||
await enterMethodCustomizeFromDialog(dialog);
|
|
||||||
const principleField = within(dialog).getByRole("textbox", {
|
const principleField = within(dialog).getByRole("textbox", {
|
||||||
name: /core principle/i,
|
name: /core principle/i,
|
||||||
});
|
});
|
||||||
@@ -626,7 +600,6 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
|
|||||||
await screen.findByRole("button", { name: "Custom Comm" }),
|
await screen.findByRole("button", { name: "Custom Comm" }),
|
||||||
);
|
);
|
||||||
const dialog = await screen.findByRole("dialog");
|
const dialog = await screen.findByRole("dialog");
|
||||||
await enterMethodCustomizeFromDialog(dialog);
|
|
||||||
expect(
|
expect(
|
||||||
within(dialog).queryByText(/no custom fields yet/i),
|
within(dialog).queryByText(/no custom fields yet/i),
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
@@ -670,7 +643,6 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
|
|||||||
await screen.findByRole("button", { name: "Custom Comm" }),
|
await screen.findByRole("button", { name: "Custom Comm" }),
|
||||||
);
|
);
|
||||||
const dialog = await screen.findByRole("dialog");
|
const dialog = await screen.findByRole("dialog");
|
||||||
await enterMethodCustomizeFromDialog(dialog);
|
|
||||||
const notesField = within(dialog).getByRole("textbox", { name: /notes/i });
|
const notesField = within(dialog).getByRole("textbox", { name: /notes/i });
|
||||||
fireEvent.change(notesField, { target: { value: "Saved detail" } });
|
fireEvent.change(notesField, { target: { value: "Saved detail" } });
|
||||||
fireEvent.click(within(dialog).getByRole("button", { name: "Save" }));
|
fireEvent.click(within(dialog).getByRole("button", { name: "Save" }));
|
||||||
@@ -685,6 +657,149 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
|
|||||||
});
|
});
|
||||||
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Customize opens the policy wizard prefilled with the chip title", async () => {
|
||||||
|
render(
|
||||||
|
<FinalReviewWithStateProbe
|
||||||
|
onState={() => {}}
|
||||||
|
initial={baseSelections}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(await screen.findByRole("button", { name: "Signal" }));
|
||||||
|
const dialog = await screen.findByRole("dialog");
|
||||||
|
fireEvent.click(
|
||||||
|
within(dialog).getByRole("button", { name: /more options/i }),
|
||||||
|
);
|
||||||
|
fireEvent.click(
|
||||||
|
await screen.findByRole("menuitem", { name: /^customize$/i }),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await screen.findByPlaceholderText("Policy name"),
|
||||||
|
).toHaveValue("Signal");
|
||||||
|
fireEvent.keyDown(document, { key: "Escape" });
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByPlaceholderText("Policy name")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
within(screen.getByRole("dialog")).getByText("Signal"),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Customize wizard Finalize persists a renamed chip title", async () => {
|
||||||
|
let latest: CreateFlowState = {};
|
||||||
|
render(
|
||||||
|
<FinalReviewWithStateProbe
|
||||||
|
onState={(s) => {
|
||||||
|
latest = s;
|
||||||
|
}}
|
||||||
|
initial={baseSelections}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(await screen.findByRole("button", { name: "Signal" }));
|
||||||
|
const dialog = await screen.findByRole("dialog");
|
||||||
|
fireEvent.click(
|
||||||
|
within(dialog).getByRole("button", { name: /more options/i }),
|
||||||
|
);
|
||||||
|
fireEvent.click(
|
||||||
|
await screen.findByRole("menuitem", { name: /^customize$/i }),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await screen.findByPlaceholderText("Policy name"),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
const nameInput = screen.getByPlaceholderText("Policy name");
|
||||||
|
fireEvent.change(nameInput, { target: { value: "Custom Signal header" } });
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Next" }));
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Next" }));
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Finalize" }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(latest.customMethodCardMetaById?.signal?.label).toBe(
|
||||||
|
"Custom Signal header",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Customize Finalize shows reordered field blocks in the chip modal", async () => {
|
||||||
|
let latest: CreateFlowState = {};
|
||||||
|
render(
|
||||||
|
<FinalReviewWithStateProbe
|
||||||
|
onState={(s) => {
|
||||||
|
latest = s;
|
||||||
|
}}
|
||||||
|
initial={baseSelections}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(await screen.findByRole("button", { name: "Signal" }));
|
||||||
|
const dialog = await screen.findByRole("dialog");
|
||||||
|
fireEvent.click(
|
||||||
|
within(dialog).getByRole("button", { name: /more options/i }),
|
||||||
|
);
|
||||||
|
fireEvent.click(
|
||||||
|
await screen.findByRole("menuitem", { name: /^customize$/i }),
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(await screen.findByRole("button", { name: "Next" }));
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Next" }));
|
||||||
|
expect(
|
||||||
|
await screen.findByText("Custom policy details"),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
const handles = screen.getAllByRole("button", {
|
||||||
|
name: "Drag to reorder this field",
|
||||||
|
});
|
||||||
|
const rows = screen.getAllByRole("listitem");
|
||||||
|
const store: Record<string, string> = {};
|
||||||
|
const dataTransfer = {
|
||||||
|
effectAllowed: "all",
|
||||||
|
dropEffect: "move",
|
||||||
|
setData(type: string, value: string) {
|
||||||
|
store[type] = value;
|
||||||
|
},
|
||||||
|
getData(type: string) {
|
||||||
|
return store[type] ?? "";
|
||||||
|
},
|
||||||
|
};
|
||||||
|
fireEvent.pointerDown(handles[0]);
|
||||||
|
fireEvent.dragStart(rows[0], { dataTransfer });
|
||||||
|
fireEvent.drop(rows[2], { dataTransfer });
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Finalize" }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.queryByPlaceholderText("Policy name"),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
latest.customMethodCardFieldBlocksById?.signal?.map((b) => b.id),
|
||||||
|
).toEqual([
|
||||||
|
"facet-logisticsAdmin",
|
||||||
|
"facet-codeOfConduct",
|
||||||
|
"facet-corePrinciple",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = screen.getByRole("dialog");
|
||||||
|
const labels = within(result)
|
||||||
|
.getAllByRole("textbox")
|
||||||
|
.map((el) => {
|
||||||
|
const labelledby = el.getAttribute("aria-labelledby");
|
||||||
|
return labelledby
|
||||||
|
? (document.getElementById(labelledby)?.textContent ?? "").trim()
|
||||||
|
: "";
|
||||||
|
});
|
||||||
|
expect(labels[0]).toMatch(/Logistics, Admin/);
|
||||||
|
expect(labels[1]).toMatch(/Code of Conduct/);
|
||||||
|
expect(labels[2]).toMatch(/Core Principle/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function FinalReviewEditPublishedWithStateProbe({
|
function FinalReviewEditPublishedWithStateProbe({
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ describe("Create flow communication-methods page", () => {
|
|||||||
expect(within(dialog).getByText("Add Platform")).toBeInTheDocument();
|
expect(within(dialog).getByText("Add Platform")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("re-opening a selected method shows no modal primary; Remove is in the kebab", async () => {
|
test("re-opening a selected method shows Save; Remove is in the kebab", async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
render(<CommunicationMethodsScreen />);
|
render(<CommunicationMethodsScreen />);
|
||||||
|
|
||||||
@@ -59,6 +59,9 @@ describe("Create flow communication-methods page", () => {
|
|||||||
expect(
|
expect(
|
||||||
within(dialogAgain).queryByRole("button", { name: "Add Platform" }),
|
within(dialogAgain).queryByRole("button", { name: "Add Platform" }),
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
within(dialogAgain).getByRole("button", { name: "Save" }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
await user.click(within(dialogAgain).getByRole("button", { name: "More options" }));
|
await user.click(within(dialogAgain).getByRole("button", { name: "More options" }));
|
||||||
expect(screen.getByRole("menuitem", { name: "Remove" })).toBeInTheDocument();
|
expect(screen.getByRole("menuitem", { name: "Remove" })).toBeInTheDocument();
|
||||||
@@ -106,7 +109,7 @@ describe("Create flow communication-methods page", () => {
|
|||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("unselected preset method fields are disabled until Customize", async () => {
|
test("unselected preset method fields are editable without Customize", async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
render(<CommunicationMethodsScreen />);
|
render(<CommunicationMethodsScreen />);
|
||||||
|
|
||||||
@@ -117,13 +120,10 @@ describe("Create flow communication-methods page", () => {
|
|||||||
|
|
||||||
const dialog = screen.getByRole("dialog");
|
const dialog = screen.getByRole("dialog");
|
||||||
const textbox = within(dialog).getAllByRole("textbox")[0];
|
const textbox = within(dialog).getAllByRole("textbox")[0];
|
||||||
expect(textbox).toBeDisabled();
|
expect(textbox).not.toBeDisabled();
|
||||||
|
|
||||||
await user.click(within(dialog).getByRole("button", { name: "More options" }));
|
await user.click(within(dialog).getByRole("button", { name: "More options" }));
|
||||||
await user.click(screen.getByRole("menuitem", { name: "Customize" }));
|
expect(screen.getByRole("menuitem", { name: "Customize" })).toBeInTheDocument();
|
||||||
expect(
|
|
||||||
within(screen.getByRole("dialog")).getAllByRole("textbox")[0],
|
|
||||||
).not.toBeDisabled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("renders without error", () => {
|
test("renders without error", () => {
|
||||||
@@ -196,7 +196,7 @@ describe("Create flow communication-methods page", () => {
|
|||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("opening Create modal for custom policy shows saved field blocks read-only until Customize", async () => {
|
test("opening Create modal for custom policy shows saved field blocks editable", async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const initial = {
|
const initial = {
|
||||||
selectedCommunicationMethodIds: [CUSTOM_POLICY_ID],
|
selectedCommunicationMethodIds: [CUSTOM_POLICY_ID],
|
||||||
@@ -227,20 +227,11 @@ describe("Create flow communication-methods page", () => {
|
|||||||
const textboxesBefore = within(dialog).getAllByRole("textbox");
|
const textboxesBefore = within(dialog).getAllByRole("textbox");
|
||||||
expect(textboxesBefore).toHaveLength(1);
|
expect(textboxesBefore).toHaveLength(1);
|
||||||
const textarea = textboxesBefore[0];
|
const textarea = textboxesBefore[0];
|
||||||
expect(textarea).toBeDisabled();
|
expect(textarea).not.toBeDisabled();
|
||||||
expect(textarea).toHaveValue("Enter norms here");
|
expect(textarea).toHaveValue("Enter norms here");
|
||||||
|
|
||||||
await user.click(within(dialog).getByRole("button", { name: "More options" }));
|
|
||||||
await user.click(screen.getByRole("menuitem", { name: "Customize" }));
|
|
||||||
|
|
||||||
const guidelinesAfter = within(screen.getByRole("dialog")).getAllByRole(
|
|
||||||
"textbox",
|
|
||||||
)[2];
|
|
||||||
expect(guidelinesAfter).not.toBeDisabled();
|
|
||||||
expect(guidelinesAfter).toHaveValue("Enter norms here");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("opening Create modal for custom policy shows badge options as chips read-only until Customize", async () => {
|
test("opening Create modal for custom policy shows badge options as interactive chips", async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const initial = {
|
const initial = {
|
||||||
selectedCommunicationMethodIds: [CUSTOM_POLICY_ID],
|
selectedCommunicationMethodIds: [CUSTOM_POLICY_ID],
|
||||||
@@ -268,22 +259,14 @@ describe("Create flow communication-methods page", () => {
|
|||||||
|
|
||||||
const dialog = screen.getByRole("dialog");
|
const dialog = screen.getByRole("dialog");
|
||||||
expect(within(dialog).getByText("Choose channels")).toBeInTheDocument();
|
expect(within(dialog).getByText("Choose channels")).toBeInTheDocument();
|
||||||
const alpha = within(dialog).getByRole("button", { name: /^Alpha$/ });
|
const alpha = within(dialog).getByRole("button", {
|
||||||
const beta = within(dialog).getByRole("button", { name: /^Beta$/ });
|
|
||||||
expect(alpha).toBeDisabled();
|
|
||||||
expect(beta).toBeDisabled();
|
|
||||||
|
|
||||||
await user.click(within(dialog).getByRole("button", { name: "More options" }));
|
|
||||||
await user.click(screen.getByRole("menuitem", { name: "Customize" }));
|
|
||||||
|
|
||||||
const alphaAfter = within(screen.getByRole("dialog")).getByRole("button", {
|
|
||||||
name: /Deselect Alpha/,
|
name: /Deselect Alpha/,
|
||||||
});
|
});
|
||||||
const betaAfter = within(screen.getByRole("dialog")).getByRole("button", {
|
const beta = within(dialog).getByRole("button", {
|
||||||
name: /Deselect Beta/,
|
name: /Deselect Beta/,
|
||||||
});
|
});
|
||||||
expect(alphaAfter).not.toBeDisabled();
|
expect(alpha).not.toBeDisabled();
|
||||||
expect(betaAfter).not.toBeDisabled();
|
expect(beta).not.toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("editing custom policy field blocks updates draft state after Save", async () => {
|
test("editing custom policy field blocks updates draft state after Save", async () => {
|
||||||
@@ -322,10 +305,8 @@ describe("Create flow communication-methods page", () => {
|
|||||||
});
|
});
|
||||||
await user.click(policyTiles[0]);
|
await user.click(policyTiles[0]);
|
||||||
const dialog = screen.getByRole("dialog");
|
const dialog = screen.getByRole("dialog");
|
||||||
await user.click(within(dialog).getByRole("button", { name: "More options" }));
|
|
||||||
await user.click(screen.getByRole("menuitem", { name: "Customize" }));
|
|
||||||
|
|
||||||
const textarea = within(dialog).getAllByRole("textbox")[2];
|
const textarea = within(dialog).getByRole("textbox");
|
||||||
await user.clear(textarea);
|
await user.clear(textarea);
|
||||||
await user.type(textarea, "Updated norms");
|
await user.type(textarea, "Updated norms");
|
||||||
|
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ describe("Create flow decision-approaches page", () => {
|
|||||||
expect(screen.getByText("SELECTED")).toBeInTheDocument();
|
expect(screen.getByText("SELECTED")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("re-opening a selected approach shows no modal primary; Remove is in the kebab", async () => {
|
test("re-opening a selected approach shows Save; Remove is in the kebab", async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
render(<DecisionApproachesScreen />);
|
render(<DecisionApproachesScreen />);
|
||||||
|
|
||||||
@@ -204,6 +204,9 @@ describe("Create flow decision-approaches page", () => {
|
|||||||
expect(
|
expect(
|
||||||
within(dialogAgain).queryByRole("button", { name: "Add Approach" }),
|
within(dialogAgain).queryByRole("button", { name: "Add Approach" }),
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
within(dialogAgain).getByRole("button", { name: "Save" }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
await user.click(within(dialogAgain).getByRole("button", { name: "More options" }));
|
await user.click(within(dialogAgain).getByRole("button", { name: "More options" }));
|
||||||
expect(screen.getByRole("menuitem", { name: "Remove" })).toBeInTheDocument();
|
expect(screen.getByRole("menuitem", { name: "Remove" })).toBeInTheDocument();
|
||||||
|
|||||||
@@ -214,4 +214,39 @@ describe("applyFinalReviewChipEditPatch", () => {
|
|||||||
"1": { meaning: "m2", signals: "s2" },
|
"1": { meaning: "m2", signals: "s2" },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("merges customMethodCardFieldBlocksById for a core values patch", () => {
|
||||||
|
const state: CreateFlowState = {
|
||||||
|
customMethodCardFieldBlocksById: {
|
||||||
|
other: [
|
||||||
|
{
|
||||||
|
kind: "text",
|
||||||
|
id: "x",
|
||||||
|
blockTitle: "T",
|
||||||
|
placeholderText: "keep",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const patch: FinalReviewChipEditPatch = {
|
||||||
|
groupKey: "coreValues",
|
||||||
|
overrideKey: "1",
|
||||||
|
value: { meaning: "m", signals: "s" },
|
||||||
|
customMethodCardFieldBlocks: [
|
||||||
|
{
|
||||||
|
kind: "text",
|
||||||
|
id: "facet-meaning",
|
||||||
|
blockTitle: "Meaning",
|
||||||
|
placeholderText: "m",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = applyFinalReviewChipEditPatch(state, patch);
|
||||||
|
|
||||||
|
expect(result.customMethodCardFieldBlocksById).toEqual({
|
||||||
|
other: state.customMethodCardFieldBlocksById?.other,
|
||||||
|
"1": patch.customMethodCardFieldBlocks,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,278 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
buildMethodCardWizardInitialValues,
|
||||||
|
coreValueDetailsFromWizardFieldBlocks,
|
||||||
|
facetDetailsToWizardFieldBlocks,
|
||||||
|
} from "../../lib/create/methodCardWizardPrefill";
|
||||||
|
import type { CustomMethodCardFieldBlock } from "../../lib/create/customMethodCardFieldBlocks";
|
||||||
|
|
||||||
|
const cardId = "signal";
|
||||||
|
const blocks: CustomMethodCardFieldBlock[] = [
|
||||||
|
{ kind: "text", id: "b1", blockTitle: "Notes", placeholderText: "…" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const commHeadings = {
|
||||||
|
corePrinciple: "Core Principle & Scope",
|
||||||
|
logisticsAdmin: "Logistics, Admin & Norms",
|
||||||
|
codeOfConduct: "Code of Conduct",
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("buildMethodCardWizardInitialValues", () => {
|
||||||
|
it("uses fallback title and description when meta is missing", () => {
|
||||||
|
expect(
|
||||||
|
buildMethodCardWizardInitialValues({
|
||||||
|
cardId,
|
||||||
|
fallbackTitle: "Signal",
|
||||||
|
fallbackDescription: "Encrypted messaging.",
|
||||||
|
meta: {},
|
||||||
|
persistedBlocks: {},
|
||||||
|
draftFieldBlocks: null,
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
title: "Signal",
|
||||||
|
description: "Encrypted messaging.",
|
||||||
|
fieldBlocks: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps facet body fields onto wizard blocks when none exist yet", () => {
|
||||||
|
expect(
|
||||||
|
buildMethodCardWizardInitialValues({
|
||||||
|
cardId,
|
||||||
|
fallbackTitle: "Signal",
|
||||||
|
fallbackDescription: "Encrypted messaging.",
|
||||||
|
meta: {},
|
||||||
|
persistedBlocks: {},
|
||||||
|
draftFieldBlocks: null,
|
||||||
|
facetPrefill: {
|
||||||
|
group: "communication",
|
||||||
|
draft: {
|
||||||
|
corePrinciple: "Privacy first.",
|
||||||
|
logisticsAdmin: "Admins steward access.",
|
||||||
|
codeOfConduct: "No leaks.",
|
||||||
|
},
|
||||||
|
headings: commHeadings,
|
||||||
|
},
|
||||||
|
}).fieldBlocks,
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
kind: "text",
|
||||||
|
id: "facet-corePrinciple",
|
||||||
|
blockTitle: "Core Principle & Scope",
|
||||||
|
placeholderText: "Privacy first.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "text",
|
||||||
|
id: "facet-logisticsAdmin",
|
||||||
|
blockTitle: "Logistics, Admin & Norms",
|
||||||
|
placeholderText: "Admins steward access.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "text",
|
||||||
|
id: "facet-codeOfConduct",
|
||||||
|
blockTitle: "Code of Conduct",
|
||||||
|
placeholderText: "No leaks.",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prefers persisted meta title and description over fallbacks", () => {
|
||||||
|
expect(
|
||||||
|
buildMethodCardWizardInitialValues({
|
||||||
|
cardId,
|
||||||
|
fallbackTitle: "Signal",
|
||||||
|
fallbackDescription: "Encrypted messaging.",
|
||||||
|
meta: {
|
||||||
|
[cardId]: { label: "Our Signal", supportText: "Private ops." },
|
||||||
|
},
|
||||||
|
persistedBlocks: { [cardId]: blocks },
|
||||||
|
draftFieldBlocks: null,
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
title: "Our Signal",
|
||||||
|
description: "Private ops.",
|
||||||
|
fieldBlocks: blocks,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("overlays current facet field values onto matching persisted blocks", () => {
|
||||||
|
const persisted: CustomMethodCardFieldBlock[] = [
|
||||||
|
{
|
||||||
|
kind: "text",
|
||||||
|
id: "facet-corePrinciple",
|
||||||
|
blockTitle: "Core Principle & Scope",
|
||||||
|
placeholderText: "Stale principle",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "text",
|
||||||
|
id: "extra-notes",
|
||||||
|
blockTitle: "Notes",
|
||||||
|
placeholderText: "keep",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
expect(
|
||||||
|
buildMethodCardWizardInitialValues({
|
||||||
|
cardId,
|
||||||
|
fallbackTitle: "Signal",
|
||||||
|
fallbackDescription: "Encrypted messaging.",
|
||||||
|
meta: {},
|
||||||
|
persistedBlocks: { [cardId]: persisted },
|
||||||
|
draftFieldBlocks: null,
|
||||||
|
facetPrefill: {
|
||||||
|
group: "communication",
|
||||||
|
draft: {
|
||||||
|
corePrinciple: "Updated principle",
|
||||||
|
logisticsAdmin: "Updated logistics",
|
||||||
|
codeOfConduct: "Updated coc",
|
||||||
|
},
|
||||||
|
headings: commHeadings,
|
||||||
|
},
|
||||||
|
}).fieldBlocks,
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
kind: "text",
|
||||||
|
id: "facet-corePrinciple",
|
||||||
|
blockTitle: "Core Principle & Scope",
|
||||||
|
placeholderText: "Updated principle",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "text",
|
||||||
|
id: "extra-notes",
|
||||||
|
blockTitle: "Notes",
|
||||||
|
placeholderText: "keep",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "text",
|
||||||
|
id: "facet-logisticsAdmin",
|
||||||
|
blockTitle: "Logistics, Admin & Norms",
|
||||||
|
placeholderText: "Updated logistics",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "text",
|
||||||
|
id: "facet-codeOfConduct",
|
||||||
|
blockTitle: "Code of Conduct",
|
||||||
|
placeholderText: "Updated coc",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prefers in-modal field-block drafts over persisted blocks", () => {
|
||||||
|
const draft: CustomMethodCardFieldBlock[] = [
|
||||||
|
{ kind: "proportion", id: "p1", blockTitle: "Share", defaultPercent: 40 },
|
||||||
|
];
|
||||||
|
expect(
|
||||||
|
buildMethodCardWizardInitialValues({
|
||||||
|
cardId,
|
||||||
|
fallbackTitle: "Signal",
|
||||||
|
fallbackDescription: "Encrypted messaging.",
|
||||||
|
meta: {
|
||||||
|
[cardId]: { label: "Our Signal", supportText: "Private ops." },
|
||||||
|
},
|
||||||
|
persistedBlocks: { [cardId]: blocks },
|
||||||
|
draftFieldBlocks: draft,
|
||||||
|
}).fieldBlocks,
|
||||||
|
).toEqual(draft);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("facetDetailsToWizardFieldBlocks", () => {
|
||||||
|
it("returns no blocks when facet copy is empty", () => {
|
||||||
|
expect(
|
||||||
|
facetDetailsToWizardFieldBlocks({
|
||||||
|
group: "communication",
|
||||||
|
draft: {
|
||||||
|
corePrinciple: " ",
|
||||||
|
logisticsAdmin: "",
|
||||||
|
codeOfConduct: "",
|
||||||
|
},
|
||||||
|
headings: commHeadings,
|
||||||
|
}),
|
||||||
|
).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps decision scope chips to a badge block", () => {
|
||||||
|
const blocksOut = facetDetailsToWizardFieldBlocks({
|
||||||
|
group: "decisionApproaches",
|
||||||
|
draft: {
|
||||||
|
corePrinciple: "Momentum.",
|
||||||
|
applicableScope: ["Daily Operations"],
|
||||||
|
selectedApplicableScope: [],
|
||||||
|
stepByStepInstructions: "Post a deadline.",
|
||||||
|
consensusLevel: 100,
|
||||||
|
objectionsDeadlocks: "Any member can block.",
|
||||||
|
},
|
||||||
|
headings: {
|
||||||
|
corePrinciple: "Core Principle",
|
||||||
|
applicableScope: "Applicable Scope",
|
||||||
|
stepByStepInstructions: "Step-by-Step Instructions",
|
||||||
|
consensusLevel: "Consensus Level",
|
||||||
|
objectionsDeadlocks: "Objections & Deadlocks",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(blocksOut.find((b) => b.kind === "badges")).toMatchObject({
|
||||||
|
kind: "badges",
|
||||||
|
blockTitle: "Applicable Scope",
|
||||||
|
options: ["Daily Operations"],
|
||||||
|
});
|
||||||
|
expect(blocksOut.find((b) => b.kind === "proportion")).toMatchObject({
|
||||||
|
defaultPercent: 100,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps core value meaning and signals onto text blocks", () => {
|
||||||
|
expect(
|
||||||
|
facetDetailsToWizardFieldBlocks({
|
||||||
|
group: "coreValues",
|
||||||
|
draft: {
|
||||||
|
meaning: "Everyone can join.",
|
||||||
|
signals: "Inaccessible venues.",
|
||||||
|
},
|
||||||
|
headings: {
|
||||||
|
meaning: "What does this value mean to your group?",
|
||||||
|
signals: "Signals of Violation",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
kind: "text",
|
||||||
|
id: "facet-meaning",
|
||||||
|
blockTitle: "What does this value mean to your group?",
|
||||||
|
placeholderText: "Everyone can join.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "text",
|
||||||
|
id: "facet-signals",
|
||||||
|
blockTitle: "Signals of Violation",
|
||||||
|
placeholderText: "Inaccessible venues.",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("coreValueDetailsFromWizardFieldBlocks", () => {
|
||||||
|
it("reads meaning and signals from facet block ids", () => {
|
||||||
|
expect(
|
||||||
|
coreValueDetailsFromWizardFieldBlocks(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
kind: "text",
|
||||||
|
id: "facet-meaning",
|
||||||
|
blockTitle: "Meaning",
|
||||||
|
placeholderText: "Updated meaning",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "text",
|
||||||
|
id: "facet-signals",
|
||||||
|
blockTitle: "Signals",
|
||||||
|
placeholderText: "Updated signals",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{ meaning: "old m", signals: "old s", supportText: "keep" },
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
meaning: "Updated meaning",
|
||||||
|
signals: "Updated signals",
|
||||||
|
supportText: "keep",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -113,4 +113,16 @@ describe("usesWizardFieldBlocksModalBody", () => {
|
|||||||
}),
|
}),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("is true for catalog ids when the wizard draft has blocks", () => {
|
||||||
|
expect(
|
||||||
|
usesWizardFieldBlocksModalBody({
|
||||||
|
methodId: "signal",
|
||||||
|
meta: {},
|
||||||
|
fieldBlocksById: {},
|
||||||
|
modalEditUnlocked: false,
|
||||||
|
draftFieldBlocks: blocks,
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user