Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | 45x 68x 68x 68x 68x 68x 68x 68x 68x | import { Stack } from "@mui/material";
import { DotsSixVerticalIcon, TrashIcon } from "@phosphor-icons/react";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { useFormContext } from "react-hook-form";
import GiveIconButton from "@shared/IconButton/GiveIconButton";
import GiveText from "@shared/Text/GiveText";
import { HFGiveInput } from "@shared/HFInputs/HFGiveInput/HFGiveInput";
import HFGiveSwitch from "@shared/HFInputs/HFGiveSwitch/HFGiveSwitch";
import { useAppTheme } from "@theme/v2/Provider";
import {
CUSTOM_FIELD_LABEL_REQUIRED,
MAX_CUSTOM_FIELD_LABEL_LEN,
} from "../customFields.helpers";
type Props = {
index: number;
/** RHF field-array key — the dnd-kit sortable id for this row. */
sortableId: string;
onDelete: () => void;
/**
* AC017 — called when the merchant edits this row's label. On a published
* form the parent warns once and marks the session for a new-draft save so a
* label change never patches the live form in place.
*/
onLabelEditIntent?: () => void;
};
const CustomFieldRow = ({
index,
sortableId,
onDelete,
onLabelEditIntent,
}: Props) => {
const { palette } = useAppTheme();
const { watch } = useFormContext();
const {
isDragging,
attributes,
listeners,
setNodeRef,
transform,
transition,
} = useSortable({ id: sortableId });
const style = {
zIndex: Number(isDragging),
transform: CSS.Translate.toString(transform),
transition,
};
const labelName = `Checkout.customFields.fields.${index}.label`;
const requiredName = `Checkout.customFields.fields.${index}.isRequired`;
const isLabelEmpty = !((watch(labelName) as string) ?? "").trim();
return (
<Stack
ref={setNodeRef}
style={style}
data-testid={`custom-field-row-${index}`}
sx={{
gap: "20px",
pb: "12px",
borderBottom: `1px solid ${palette.border?.primary}`,
}}
>
{/*
Details row — drag handle, the label input (with its "Label Text" label
above), and delete, bottom-aligned so the icon buttons sit beside the
input field rather than its label. The 8px bottom margin centers the
32px icon buttons on the 48px input.
*/}
<Stack direction="row" alignItems="flex-end" spacing={1}>
<GiveIconButton
Icon={DotsSixVerticalIcon}
color={palette.icon?.["icon-secondary"]}
size="small"
variant="ghost"
aria-label={`Reorder field ${index + 1}`}
style={{
cursor: isDragging ? "grabbing" : "grab",
marginBottom: "8px",
}}
{...attributes}
{...listeners}
/>
<Stack flexGrow={1} minWidth="200px">
<HFGiveInput
name={labelName}
label="Label Text"
maxLength={MAX_CUSTOM_FIELD_LABEL_LEN}
error={isLabelEmpty}
helper={isLabelEmpty ? CUSTOM_FIELD_LABEL_REQUIRED : ""}
onValidate={onLabelEditIntent}
/>
</Stack>
<GiveIconButton
Icon={TrashIcon}
size="small"
variant="ghost"
color={palette.primitive?.error?.["50"]}
onClick={onDelete}
aria-label={`Delete field ${index + 1}`}
style={{ marginBottom: "8px" }}
/>
</Stack>
{/* Required label + toggle, beneath the input. */}
<Stack direction="row" alignItems="center" spacing={1}>
<GiveText variant="bodyS" color="secondary">
Required
</GiveText>
<HFGiveSwitch name={requiredName} />
</Stack>
</Stack>
);
};
export default CustomFieldRow;
|