Route kebab Customize through the prefilled policy wizard so existing methods and values can be renamed, reordered, and kept in that field order on the card after Finalize.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+226
-15
@@ -5,8 +5,12 @@ import {
|
||||
useMessages,
|
||||
useTranslation,
|
||||
} from "../../../../contexts/MessagesContext";
|
||||
import { useAsyncConfirm } from "../../../../hooks/useAsyncConfirm";
|
||||
import type { CustomMethodCardFieldBlock } from "../../../../../lib/create/customMethodCardFieldBlocks";
|
||||
import { CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS } from "../../../../../lib/create/customMethodCardWizardConstants";
|
||||
import {
|
||||
CUSTOM_METHOD_CARD_WIZARD_MAX_DESCRIPTION_CHARS,
|
||||
CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS,
|
||||
} from "../../../../../lib/create/customMethodCardWizardConstants";
|
||||
import type { AddCustomFieldType } from "../../../../components/controls/AddCustomField/AddCustomField.types";
|
||||
import type { ModalHeaderMenuItem } from "../../../../components/modals/ModalHeader/ModalHeader.types";
|
||||
import { CustomMethodCardWizardView } from "./CustomMethodCardWizard.view";
|
||||
@@ -17,12 +21,13 @@ import type { CustomMethodCardWizardProps } from "./CustomMethodCardWizard.types
|
||||
* `20066:14748`, `20094:48551`, `20066:14361`).
|
||||
*/
|
||||
const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
||||
({ isOpen, onClose, onFinalize, onPersistCustomUploadFile }) => {
|
||||
({ isOpen, onClose, onFinalize, onPersistCustomUploadFile, initialValues }) => {
|
||||
const m = useMessages();
|
||||
const t = useTranslation("common");
|
||||
const tUpload = useTranslation("create.upload");
|
||||
const w = m.create.customRule.customMethodCardWizard;
|
||||
const menuCopy = m.create.customRule.modalKebabMenu;
|
||||
const { requestConfirm, confirmDialog } = useAsyncConfirm();
|
||||
|
||||
const copy = useMemo(
|
||||
() => ({
|
||||
@@ -68,6 +73,7 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
||||
const [draftFieldBlocks, setDraftFieldBlocks] = useState<
|
||||
CustomMethodCardFieldBlock[]
|
||||
>([]);
|
||||
const [editingBlockId, setEditingBlockId] = useState<string | null>(null);
|
||||
|
||||
const [textBlockTitle, setTextBlockTitle] = useState("");
|
||||
const [textPlaceholderBody, setTextPlaceholderBody] = useState("");
|
||||
@@ -88,6 +94,14 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
||||
const [proportionDefault, setProportionDefault] = useState(50);
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const initialValuesRef = useRef(initialValues);
|
||||
initialValuesRef.current = initialValues;
|
||||
const openSnapshotRef = useRef<{
|
||||
title: string;
|
||||
description: string;
|
||||
fieldBlocks: string;
|
||||
} | null>(null);
|
||||
const fieldModalSnapshotRef = useRef<string | null>(null);
|
||||
|
||||
const resetFieldTypeDrafts = useCallback(() => {
|
||||
setTextBlockTitle("");
|
||||
@@ -112,21 +126,112 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
||||
setPolicyDescription("");
|
||||
setAddFieldExpanded(false);
|
||||
setFieldTypeModal(null);
|
||||
setEditingBlockId(null);
|
||||
setDraftFieldBlocks([]);
|
||||
openSnapshotRef.current = null;
|
||||
fieldModalSnapshotRef.current = null;
|
||||
resetFieldTypeDrafts();
|
||||
}, [resetFieldTypeDrafts]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
reset();
|
||||
return;
|
||||
}
|
||||
}, [isOpen, reset]);
|
||||
const init = initialValuesRef.current;
|
||||
setWizardStep(1);
|
||||
setPolicyTitle(init?.title ?? "");
|
||||
setPolicyDescription(init?.description ?? "");
|
||||
setAddFieldExpanded(false);
|
||||
setFieldTypeModal(null);
|
||||
setEditingBlockId(null);
|
||||
setDraftFieldBlocks(
|
||||
init?.fieldBlocks ? structuredClone(init.fieldBlocks) : [],
|
||||
);
|
||||
openSnapshotRef.current = {
|
||||
title: init?.title ?? "",
|
||||
description: init?.description ?? "",
|
||||
fieldBlocks: JSON.stringify(init?.fieldBlocks ?? []),
|
||||
};
|
||||
fieldModalSnapshotRef.current = null;
|
||||
resetFieldTypeDrafts();
|
||||
}, [isOpen, reset, resetFieldTypeDrafts]);
|
||||
|
||||
const dismiss = useCallback(() => {
|
||||
reset();
|
||||
onClose();
|
||||
}, [onClose, reset]);
|
||||
|
||||
const fieldModalDraftSignature = useCallback(() => {
|
||||
return JSON.stringify({
|
||||
fieldTypeModal,
|
||||
textBlockTitle,
|
||||
textPlaceholderBody,
|
||||
badgeBlockTitle,
|
||||
badgeOptions,
|
||||
uploadBlockTitle,
|
||||
uploadFileName,
|
||||
uploadAssetUrl,
|
||||
proportionBlockTitle,
|
||||
proportionDefault,
|
||||
});
|
||||
}, [
|
||||
badgeBlockTitle,
|
||||
badgeOptions,
|
||||
fieldTypeModal,
|
||||
proportionBlockTitle,
|
||||
proportionDefault,
|
||||
textBlockTitle,
|
||||
textPlaceholderBody,
|
||||
uploadAssetUrl,
|
||||
uploadBlockTitle,
|
||||
uploadFileName,
|
||||
]);
|
||||
|
||||
const isWizardSessionDirty = useCallback(() => {
|
||||
const snap = openSnapshotRef.current;
|
||||
if (!snap) return false;
|
||||
if (policyTitle !== snap.title || policyDescription !== snap.description) {
|
||||
return true;
|
||||
}
|
||||
if (JSON.stringify(draftFieldBlocks) !== snap.fieldBlocks) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
fieldTypeModal &&
|
||||
fieldModalSnapshotRef.current != null &&
|
||||
fieldModalDraftSignature() !== fieldModalSnapshotRef.current
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, [
|
||||
draftFieldBlocks,
|
||||
fieldModalDraftSignature,
|
||||
fieldTypeModal,
|
||||
policyDescription,
|
||||
policyTitle,
|
||||
]);
|
||||
|
||||
const confirmAbandonWizardEdits = useCallback(async () => {
|
||||
if (!isWizardSessionDirty()) {
|
||||
return true;
|
||||
}
|
||||
return requestConfirm({
|
||||
title: menuCopy.discardUnsavedCustomizeChangesTitle,
|
||||
description: menuCopy.discardUnsavedCustomizeChangesDescription,
|
||||
proceedText: menuCopy.discardUnsavedCustomizeChangesProceed,
|
||||
cancelText: menuCopy.discardUnsavedCustomizeChangesCancel,
|
||||
});
|
||||
}, [
|
||||
isWizardSessionDirty,
|
||||
menuCopy.discardUnsavedCustomizeChangesCancel,
|
||||
menuCopy.discardUnsavedCustomizeChangesDescription,
|
||||
menuCopy.discardUnsavedCustomizeChangesProceed,
|
||||
menuCopy.discardUnsavedCustomizeChangesTitle,
|
||||
requestConfirm,
|
||||
]);
|
||||
|
||||
const titleTrim = policyTitle.trim();
|
||||
const descriptionTrim = policyDescription.trim();
|
||||
|
||||
@@ -136,7 +241,7 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
||||
titleTrim.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS;
|
||||
const descriptionOk =
|
||||
descriptionTrim.length > 0 &&
|
||||
descriptionTrim.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS;
|
||||
descriptionTrim.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_DESCRIPTION_CHARS;
|
||||
if (wizardStep === 1) return titleOk;
|
||||
if (wizardStep === 2) return descriptionOk;
|
||||
return titleOk && descriptionOk;
|
||||
@@ -213,7 +318,9 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
||||
const shellDescription = fieldModalHeader?.description ?? headerDescription;
|
||||
|
||||
const nextLabel = fieldTypeModal
|
||||
? copy.fieldModals.addField
|
||||
? editingBlockId
|
||||
? copy.fieldModals.saveField
|
||||
: copy.fieldModals.addField
|
||||
: wizardStep === 3
|
||||
? copy.footerFinalize
|
||||
: t("buttons.next");
|
||||
@@ -222,33 +329,124 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
||||
? !fieldModalStepValid
|
||||
: !stepValid;
|
||||
|
||||
const handleShellClose = useCallback(() => {
|
||||
const handleShellClose = useCallback(async () => {
|
||||
if (fieldTypeModal) {
|
||||
if (
|
||||
fieldModalSnapshotRef.current != null &&
|
||||
fieldModalDraftSignature() !== fieldModalSnapshotRef.current &&
|
||||
!(await confirmAbandonWizardEdits())
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setFieldTypeModal(null);
|
||||
setEditingBlockId(null);
|
||||
fieldModalSnapshotRef.current = null;
|
||||
return;
|
||||
}
|
||||
if (!(await confirmAbandonWizardEdits())) {
|
||||
return;
|
||||
}
|
||||
dismiss();
|
||||
}, [dismiss, fieldTypeModal]);
|
||||
}, [
|
||||
confirmAbandonWizardEdits,
|
||||
dismiss,
|
||||
fieldModalDraftSignature,
|
||||
fieldTypeModal,
|
||||
]);
|
||||
|
||||
const kebabMenuItems = useMemo<ModalHeaderMenuItem[]>(() => [], []);
|
||||
|
||||
const handleBack = useCallback(() => {
|
||||
const handleBack = useCallback(async () => {
|
||||
if (fieldTypeModal) {
|
||||
if (
|
||||
fieldModalSnapshotRef.current != null &&
|
||||
fieldModalDraftSignature() !== fieldModalSnapshotRef.current &&
|
||||
!(await confirmAbandonWizardEdits())
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setFieldTypeModal(null);
|
||||
setEditingBlockId(null);
|
||||
fieldModalSnapshotRef.current = null;
|
||||
return;
|
||||
}
|
||||
if (wizardStep === 1) {
|
||||
if (!(await confirmAbandonWizardEdits())) {
|
||||
return;
|
||||
}
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
setWizardStep((s) => (s === 2 ? 1 : 2));
|
||||
}, [dismiss, fieldTypeModal, wizardStep]);
|
||||
}, [
|
||||
confirmAbandonWizardEdits,
|
||||
dismiss,
|
||||
fieldModalDraftSignature,
|
||||
fieldTypeModal,
|
||||
wizardStep,
|
||||
]);
|
||||
|
||||
const handleSelectFieldType = useCallback((ft: AddCustomFieldType) => {
|
||||
setEditingBlockId(null);
|
||||
resetFieldTypeDrafts();
|
||||
setFieldTypeModal(ft);
|
||||
fieldModalSnapshotRef.current = JSON.stringify({
|
||||
fieldTypeModal: ft,
|
||||
textBlockTitle: "",
|
||||
textPlaceholderBody: "",
|
||||
badgeBlockTitle: "",
|
||||
badgeOptions: [],
|
||||
uploadBlockTitle: "",
|
||||
uploadFileName: undefined,
|
||||
uploadAssetUrl: undefined,
|
||||
proportionBlockTitle: "",
|
||||
proportionDefault: 50,
|
||||
});
|
||||
}, [resetFieldTypeDrafts]);
|
||||
|
||||
const handleEditFieldBlock = useCallback(
|
||||
(block: CustomMethodCardFieldBlock) => {
|
||||
resetFieldTypeDrafts();
|
||||
setEditingBlockId(block.id);
|
||||
setAddFieldExpanded(false);
|
||||
switch (block.kind) {
|
||||
case "text":
|
||||
setTextBlockTitle(block.blockTitle);
|
||||
setTextPlaceholderBody(block.placeholderText);
|
||||
break;
|
||||
case "badges":
|
||||
setBadgeBlockTitle(block.blockTitle);
|
||||
setBadgeOptions([...block.options]);
|
||||
break;
|
||||
case "upload":
|
||||
setUploadBlockTitle(block.blockTitle);
|
||||
setUploadFileName(block.fileName);
|
||||
setUploadAssetUrl(block.assetUrl);
|
||||
break;
|
||||
case "proportion":
|
||||
setProportionBlockTitle(block.blockTitle);
|
||||
setProportionDefault(block.defaultPercent);
|
||||
break;
|
||||
}
|
||||
setFieldTypeModal(block.kind);
|
||||
fieldModalSnapshotRef.current = JSON.stringify({
|
||||
fieldTypeModal: block.kind,
|
||||
textBlockTitle: block.kind === "text" ? block.blockTitle : "",
|
||||
textPlaceholderBody:
|
||||
block.kind === "text" ? block.placeholderText : "",
|
||||
badgeBlockTitle: block.kind === "badges" ? block.blockTitle : "",
|
||||
badgeOptions: block.kind === "badges" ? [...block.options] : [],
|
||||
uploadBlockTitle: block.kind === "upload" ? block.blockTitle : "",
|
||||
uploadFileName: block.kind === "upload" ? block.fileName : undefined,
|
||||
uploadAssetUrl: block.kind === "upload" ? block.assetUrl : undefined,
|
||||
proportionBlockTitle:
|
||||
block.kind === "proportion" ? block.blockTitle : "",
|
||||
proportionDefault: block.kind === "proportion" ? block.defaultPercent : 50,
|
||||
});
|
||||
},
|
||||
[resetFieldTypeDrafts],
|
||||
);
|
||||
|
||||
const handleFileChosen = useCallback(
|
||||
async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
@@ -285,9 +483,9 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
||||
);
|
||||
}, []);
|
||||
|
||||
const appendFieldBlock = useCallback(() => {
|
||||
const commitFieldBlock = useCallback(() => {
|
||||
if (!fieldTypeModal || !fieldModalStepValid) return;
|
||||
const id = crypto.randomUUID();
|
||||
const id = editingBlockId ?? crypto.randomUUID();
|
||||
let block: CustomMethodCardFieldBlock;
|
||||
switch (fieldTypeModal) {
|
||||
case "text":
|
||||
@@ -325,11 +523,19 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
||||
defaultPercent: proportionDefault,
|
||||
};
|
||||
}
|
||||
setDraftFieldBlocks((prev) => [...prev, block]);
|
||||
setDraftFieldBlocks((prev) => {
|
||||
if (editingBlockId) {
|
||||
return prev.map((row) => (row.id === editingBlockId ? block : row));
|
||||
}
|
||||
return [...prev, block];
|
||||
});
|
||||
setFieldTypeModal(null);
|
||||
setEditingBlockId(null);
|
||||
fieldModalSnapshotRef.current = null;
|
||||
}, [
|
||||
badgeBlockTitle,
|
||||
badgeOptions,
|
||||
editingBlockId,
|
||||
fieldModalStepValid,
|
||||
fieldTypeModal,
|
||||
proportionBlockTitle,
|
||||
@@ -343,7 +549,7 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
||||
|
||||
const handleNext = useCallback(() => {
|
||||
if (fieldTypeModal) {
|
||||
appendFieldBlock();
|
||||
commitFieldBlock();
|
||||
return;
|
||||
}
|
||||
if (!stepValid) return;
|
||||
@@ -358,7 +564,7 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
||||
}
|
||||
setWizardStep((s) => (s === 1 ? 2 : 3));
|
||||
}, [
|
||||
appendFieldBlock,
|
||||
commitFieldBlock,
|
||||
descriptionTrim,
|
||||
dismiss,
|
||||
draftFieldBlocks,
|
||||
@@ -370,6 +576,7 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CustomMethodCardWizardView
|
||||
isOpen={isOpen}
|
||||
onDismiss={handleShellClose}
|
||||
@@ -380,7 +587,8 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
||||
policyDescription={policyDescription}
|
||||
addFieldExpanded={addFieldExpanded}
|
||||
copy={copy}
|
||||
maxChars={CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS}
|
||||
maxTitleChars={CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS}
|
||||
maxDescriptionChars={CUSTOM_METHOD_CARD_WIZARD_MAX_DESCRIPTION_CHARS}
|
||||
onPolicyTitleChange={setPolicyTitle}
|
||||
onPolicyDescriptionChange={setPolicyDescription}
|
||||
onPressAddCustomField={() => setAddFieldExpanded(true)}
|
||||
@@ -420,10 +628,13 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
||||
stepper={!fieldTypeModal}
|
||||
draftFieldBlocks={draftFieldBlocks}
|
||||
onDraftFieldBlocksReorder={setDraftFieldBlocks}
|
||||
onEditFieldBlock={handleEditFieldBlock}
|
||||
kebabMoreOptionsAriaLabel={menuCopy.triggerAriaLabel}
|
||||
kebabMenuAriaLabel={menuCopy.menuAriaLabel}
|
||||
kebabMenuItems={kebabMenuItems}
|
||||
/>
|
||||
{confirmDialog}
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { RefObject } from "react";
|
||||
import type { AddCustomFieldType } from "../../../../components/controls/AddCustomField/AddCustomField.types";
|
||||
import type { ModalHeaderMenuItem } from "../../../../components/modals/ModalHeader/ModalHeader.types";
|
||||
import type { CustomMethodCardFieldBlock } from "../../../../../lib/create/customMethodCardFieldBlocks";
|
||||
import type { MethodCardWizardInitialValues } from "../../../../../lib/create/methodCardWizardPrefill";
|
||||
|
||||
export interface CustomMethodCardWizardFieldBodiesCopy {
|
||||
requiredHint: string;
|
||||
@@ -47,6 +48,7 @@ export interface CustomMethodCardWizardCopy {
|
||||
footerFinalize: string;
|
||||
fieldModals: {
|
||||
addField: string;
|
||||
saveField: string;
|
||||
requiredHint: string;
|
||||
text: CustomMethodCardWizardFieldBodiesCopy["text"] & {
|
||||
title: string;
|
||||
@@ -70,6 +72,8 @@ export interface CustomMethodCardWizardCopy {
|
||||
export interface CustomMethodCardWizardProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
/** When set, seeds title, description, and field blocks; wizard still starts at step 1. */
|
||||
initialValues?: MethodCardWizardInitialValues | null;
|
||||
/** Called when the user completes step 3; parent assigns id and persists state. */
|
||||
onFinalize: (payload: {
|
||||
title: string;
|
||||
@@ -123,7 +127,8 @@ export interface CustomMethodCardWizardViewProps {
|
||||
policyDescription: string;
|
||||
addFieldExpanded: boolean;
|
||||
copy: CustomMethodCardWizardCopy;
|
||||
maxChars: number;
|
||||
maxTitleChars: number;
|
||||
maxDescriptionChars: number;
|
||||
onPolicyTitleChange: (v: string) => void;
|
||||
onPolicyDescriptionChange: (v: string) => void;
|
||||
onPressAddCustomField: () => void;
|
||||
@@ -136,6 +141,7 @@ export interface CustomMethodCardWizardViewProps {
|
||||
>;
|
||||
draftFieldBlocks: CustomMethodCardFieldBlock[];
|
||||
onDraftFieldBlocksReorder: (_next: CustomMethodCardFieldBlock[]) => void;
|
||||
onEditFieldBlock: (_block: CustomMethodCardFieldBlock) => void;
|
||||
nextDisabled: boolean;
|
||||
nextLabel: string;
|
||||
showBackButton: boolean;
|
||||
|
||||
@@ -19,7 +19,8 @@ function CustomMethodCardWizardViewComponent({
|
||||
policyDescription,
|
||||
addFieldExpanded,
|
||||
copy,
|
||||
maxChars,
|
||||
maxTitleChars,
|
||||
maxDescriptionChars,
|
||||
onPolicyTitleChange,
|
||||
onPolicyDescriptionChange,
|
||||
onPressAddCustomField,
|
||||
@@ -35,6 +36,7 @@ function CustomMethodCardWizardViewComponent({
|
||||
stepper,
|
||||
draftFieldBlocks,
|
||||
onDraftFieldBlocksReorder,
|
||||
onEditFieldBlock,
|
||||
kebabMoreOptionsAriaLabel,
|
||||
kebabMenuAriaLabel,
|
||||
kebabMenuItems,
|
||||
@@ -71,7 +73,7 @@ function CustomMethodCardWizardViewComponent({
|
||||
placeholder={copy.step1.fieldPlaceholder}
|
||||
value={policyTitle}
|
||||
onChange={onPolicyTitleChange}
|
||||
maxLength={maxChars}
|
||||
maxLength={maxTitleChars}
|
||||
/>
|
||||
) : null}
|
||||
{!fieldTypeModal && wizardStep === 2 ? (
|
||||
@@ -80,9 +82,9 @@ function CustomMethodCardWizardViewComponent({
|
||||
formHeader={false}
|
||||
placeholder={copy.step2.fieldPlaceholder}
|
||||
value={policyDescription}
|
||||
maxLength={maxChars}
|
||||
maxLength={maxDescriptionChars}
|
||||
onChange={(e) => onPolicyDescriptionChange(e.target.value)}
|
||||
textHint={`${policyDescription.length}/${maxChars}`}
|
||||
textHint={`${policyDescription.length}/${maxDescriptionChars}`}
|
||||
className="w-full"
|
||||
rows={4}
|
||||
/>
|
||||
@@ -96,6 +98,7 @@ function CustomMethodCardWizardViewComponent({
|
||||
dragHandleAriaLabel={copy.step3BlocksList.dragHandleAriaLabel}
|
||||
listLabel={copy.step3BlocksList.listLabel}
|
||||
onBlocksReorder={onDraftFieldBlocksReorder}
|
||||
onEditBlock={onEditFieldBlock}
|
||||
/>
|
||||
) : null}
|
||||
<AddCustomField
|
||||
|
||||
+22
-3
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { memo, useCallback, useState, type DragEvent } from "react";
|
||||
import { memo, useCallback, useRef, useState, type DragEvent } from "react";
|
||||
import { reorderCustomMethodCardFieldBlocks } from "../../../../../lib/create/reorderCustomMethodCardFieldBlocks";
|
||||
import { CustomMethodCardWizardBlocksListView } from "./CustomMethodCardWizardBlocksList.view";
|
||||
import type { CustomMethodCardWizardBlocksListProps } from "./CustomMethodCardWizardBlocksList.types";
|
||||
@@ -11,19 +11,33 @@ function CustomMethodCardWizardBlocksListContainerComponent({
|
||||
dragHandleAriaLabel,
|
||||
listLabel,
|
||||
onBlocksReorder,
|
||||
onEditBlock,
|
||||
}: CustomMethodCardWizardBlocksListProps) {
|
||||
const [draggingIndex, setDraggingIndex] = useState<number | null>(null);
|
||||
const [overIndex, setOverIndex] = useState<number | null>(null);
|
||||
const draggingIndexRef = useRef<number | null>(null);
|
||||
const dragFromHandleRef = useRef(false);
|
||||
|
||||
const clearDragUi = useCallback(() => {
|
||||
draggingIndexRef.current = null;
|
||||
dragFromHandleRef.current = false;
|
||||
setDraggingIndex(null);
|
||||
setOverIndex(null);
|
||||
}, []);
|
||||
|
||||
const handleHandlePointerDown = useCallback(() => {
|
||||
dragFromHandleRef.current = true;
|
||||
}, []);
|
||||
|
||||
const handleDragStart = useCallback(
|
||||
(index: number) => (e: DragEvent) => {
|
||||
if (!dragFromHandleRef.current) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
e.dataTransfer.effectAllowed = "move";
|
||||
e.dataTransfer.setData("text/plain", String(index));
|
||||
draggingIndexRef.current = index;
|
||||
setDraggingIndex(index);
|
||||
},
|
||||
[],
|
||||
@@ -40,8 +54,11 @@ function CustomMethodCardWizardBlocksListContainerComponent({
|
||||
const handleDrop = useCallback(
|
||||
(index: number) => (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
const from = Number.parseInt(e.dataTransfer.getData("text/plain"), 10);
|
||||
if (Number.isNaN(from)) {
|
||||
const fromData = Number.parseInt(e.dataTransfer.getData("text/plain"), 10);
|
||||
const from = Number.isNaN(fromData)
|
||||
? draggingIndexRef.current
|
||||
: fromData;
|
||||
if (from == null || Number.isNaN(from)) {
|
||||
clearDragUi();
|
||||
return;
|
||||
}
|
||||
@@ -66,6 +83,8 @@ function CustomMethodCardWizardBlocksListContainerComponent({
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleDrop}
|
||||
onDragEnd={clearDragUi}
|
||||
onHandlePointerDown={handleHandlePointerDown}
|
||||
onEditBlock={onEditBlock}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+2
@@ -8,6 +8,7 @@ export interface CustomMethodCardWizardBlocksListProps {
|
||||
dragHandleAriaLabel: string;
|
||||
listLabel: string;
|
||||
onBlocksReorder: (_next: CustomMethodCardFieldBlock[]) => void;
|
||||
onEditBlock: (_block: CustomMethodCardFieldBlock) => void;
|
||||
}
|
||||
|
||||
export interface CustomMethodCardWizardBlocksListViewProps
|
||||
@@ -18,4 +19,5 @@ export interface CustomMethodCardWizardBlocksListViewProps
|
||||
onDragOver: (_index: number) => (_e: DragEvent) => void;
|
||||
onDrop: (_index: number) => (_e: DragEvent) => void;
|
||||
onDragEnd: () => void;
|
||||
onHandlePointerDown: () => void;
|
||||
}
|
||||
|
||||
+18
-7
@@ -38,6 +38,8 @@ function CustomMethodCardWizardBlocksListViewComponent({
|
||||
onDragOver,
|
||||
onDrop,
|
||||
onDragEnd,
|
||||
onHandlePointerDown,
|
||||
onEditBlock,
|
||||
}: CustomMethodCardWizardBlocksListViewProps) {
|
||||
return (
|
||||
<ul
|
||||
@@ -53,6 +55,7 @@ function CustomMethodCardWizardBlocksListViewComponent({
|
||||
return (
|
||||
<li
|
||||
key={block.id}
|
||||
draggable
|
||||
aria-grabbed={isDragging ? true : undefined}
|
||||
className={`group flex min-h-[52px] items-stretch gap-2 rounded-[var(--measures-radius-medium,8px)] border-2 bg-[var(--color-surface-default-secondary)] py-2 pl-1 pr-3 transition-[border-color,box-shadow,opacity] ${
|
||||
isDropTarget
|
||||
@@ -61,19 +64,26 @@ function CustomMethodCardWizardBlocksListViewComponent({
|
||||
? "relative z-20 border-[var(--color-border-default-primary)] opacity-60 shadow-[0_4px_12px_rgba(0,0,0,0.12)]"
|
||||
: "border-[var(--color-border-default-primary)] hover:border-[var(--color-content-default-secondary)]"
|
||||
}`}
|
||||
onDragStart={onDragStart(index)}
|
||||
onDragOver={onDragOver(index)}
|
||||
onDrop={onDrop(index)}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
draggable
|
||||
onDragStart={onDragStart(index)}
|
||||
onDragEnd={onDragEnd}
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={dragHandleAriaLabel}
|
||||
className="flex shrink-0 cursor-grab touch-manipulation items-center self-stretch rounded-[var(--measures-radius-200,8px)] border-0 bg-transparent px-1 text-[var(--color-content-default-secondary)] transition-colors active:cursor-grabbing focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--color-border-invert-primary)] group-hover:text-[var(--color-content-default-primary)]"
|
||||
onPointerDown={onHandlePointerDown}
|
||||
className="flex shrink-0 cursor-grab touch-manipulation items-center self-stretch rounded-[var(--measures-radius-200,8px)] px-1 text-[var(--color-content-default-secondary)] transition-colors active:cursor-grabbing focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--color-border-invert-primary)] group-hover:text-[var(--color-content-default-primary)]"
|
||||
>
|
||||
<DragHandleGlyph />
|
||||
</button>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={title}
|
||||
onClick={() => onEditBlock(block)}
|
||||
className="flex min-w-0 flex-1 items-center gap-2 self-stretch rounded-[var(--measures-radius-200,8px)] border-0 bg-transparent px-0 text-left cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--color-border-invert-primary)]"
|
||||
>
|
||||
<span className="flex h-8 w-8 shrink-0 items-center justify-center self-center">
|
||||
<Icon
|
||||
name={ADD_CUSTOM_FIELD_TYPE_ICONS[kind]}
|
||||
@@ -89,6 +99,7 @@ function CustomMethodCardWizardBlocksListViewComponent({
|
||||
{typeLabel}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user