All files / src/sections/PayBuilder/Checkout/components CustomFieldRow.tsx

100% Statements 9/9
66.66% Branches 4/6
100% Functions 1/1
100% Lines 9/9

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;