Files
calendar/packages/features/form-builder/FormBuilder.tsx
T
fc592a17ac feat: Add 'Disable on Prefill' option for fields (#15715)
* feat:add prefilcheck check box in the field creation

* chore:formatted the li8 translations

* feat:add e2e test case for this feature

* refactor:reanmed the key from prefilledCheck to disableOnPrefill and moved the readOnly logic to formbuilderField

* chore:fixed the type check

* fix:if prefilled has error. do not disabble it

* chore:removed the e2e tests for prefill in e2e

* chore:revert back to old changes

* chore:remove unwanted code frome2e

* Support name field as well

* refactor:handled the feedback changes. still unit test cases pending

* feat: add unit tests for  disable field on prefill logic

* chore: resolved the type check and typos

* chore:renamed the files and type clean up

* Apply suggestions from code review

* chore:renaming test files and moving the disableonprefill hook to formbuilderFeild.

* refactor: add unit test for serach params and form values check and minor code formatting and file name changes

* fix: validating the input value partially at the intialising phase. so that form response values and  from ui values are same and making sure invalid data not sent to backend on submit.

* chore:fix the type check issue

* fix: field value valdiation at initilisation and add the  unit test cases for that change

* chore:add comments to the tests

* chore:resolve type check

* chore: moved the tests to the formBuilder

* Simplify codebase by ensuring select and multiselect changes the value when no option is found

* Relocate and refactor tests

* handle special cases

* Dont disable for dirty fields

* Update schema.prisma

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Hariom <hariombalhara@gmail.com>
2024-08-18 17:34:59 +00:00

800 lines
30 KiB
TypeScript

import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useEffect, useState } from "react";
import type { SubmitHandler, UseFormReturn } from "react-hook-form";
import { Controller, useFieldArray, useForm, useFormContext } from "react-hook-form";
import type { z } from "zod";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import {
Badge,
BooleanToggleGroupField,
Button,
CheckboxField,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
Form,
Icon,
Input,
InputField,
Label,
SelectField,
showToast,
Switch,
} from "@calcom/ui";
import { fieldTypesConfigMap } from "./fieldTypes";
import { fieldsThatSupportLabelAsSafeHtml } from "./fieldsThatSupportLabelAsSafeHtml";
import type { fieldsSchema } from "./schema";
import { getFieldIdentifier } from "./utils/getFieldIdentifier";
import { getConfig as getVariantsConfig } from "./utils/variantsConfig";
type RhfForm = {
fields: z.infer<typeof fieldsSchema>;
};
type RhfFormFields = RhfForm["fields"];
type RhfFormField = RhfFormFields[number];
function getCurrentFieldType(fieldForm: UseFormReturn<RhfFormField>) {
return fieldTypesConfigMap[fieldForm.watch("type") || "text"];
}
function isRequiredField(field: RhfFormField) {
// 'location' must be shown as required in the UI(but at backend it is not required because not specifying it defaults to CalVideo)
// So, handle it in UI only instead of updating bookingFields config in getBookingFields
// TODO: FormBuilder must not be made aware of BookingFields, so move this "location" check to outside FormBuilder where the component is used.
return field.name === "location" ? true : field.required;
}
/**
* It works with a react-hook-form only.
* `formProp` specifies the name of the property in the react-hook-form that has the fields. This is where fields would be updated.
*/
export const FormBuilder = function FormBuilder({
title,
description,
addFieldLabel,
formProp,
disabled,
LockedIcon,
dataStore,
}: {
formProp: string;
title: string;
description: string;
addFieldLabel: string;
disabled: boolean;
LockedIcon: false | JSX.Element;
/**
* A readonly dataStore that is used to lookup the options for the fields. It works in conjunction with the field.getOptionAt property which acts as the key in options
*/
dataStore: {
options: Record<string, { label: string; value: string; inputPlaceholder?: string }[]>;
};
}) {
// I would have liked to give Form Builder it's own Form but nested Forms aren't something that browsers support.
// So, this would reuse the same Form as the parent form.
const fieldsForm = useFormContext<RhfForm>();
const [parent] = useAutoAnimate<HTMLUListElement>();
const { t } = useLocale();
const { fields, swap, remove, update, append } = useFieldArray({
control: fieldsForm.control,
// HACK: It allows any property name to be used for instead of `fields` property name
name: formProp as unknown as "fields",
});
const [fieldDialog, setFieldDialog] = useState({
isOpen: false,
fieldIndex: -1,
data: {} as RhfFormField | null,
});
const addField = () => {
setFieldDialog({
isOpen: true,
fieldIndex: -1,
data: null,
});
};
const editField = (index: number, data: RhfFormField) => {
setFieldDialog({
isOpen: true,
fieldIndex: index,
data,
});
};
const removeField = (index: number) => {
remove(index);
};
return (
<div>
<div>
<div className="text-default text-sm font-semibold leading-none ltr:mr-1 rtl:ml-1">
{title}
{LockedIcon}
</div>
<p className="text-subtle mt-0.5 max-w-[280px] break-words text-sm sm:max-w-[500px]">{description}</p>
<ul ref={parent} className="border-subtle divide-subtle mt-4 divide-y rounded-md border">
{fields.map((field, index) => {
let options = field.options ?? null;
const sources = [...(field.sources || [])];
const isRequired = isRequiredField(field);
if (!options && field.getOptionsAt) {
options = dataStore.options[field.getOptionsAt as keyof typeof dataStore] ?? [];
// TODO: The dataStore should itself provide the label directly
// This is important because FormBuilder isn't aware of what a location is. It is a generic Form Builder
const sourceLabel = field.getOptionsAt === "locations" ? "Location" : field.getOptionsAt;
options.forEach((option) => {
sources.push({
id: option.value,
label: sourceLabel,
type: "system",
});
});
}
if (fieldsThatSupportLabelAsSafeHtml.includes(field.type)) {
field = { ...field, labelAsSafeHtml: markdownToSafeHTML(field.label ?? "") };
}
const numOptions = options?.length ?? 0;
const firstOptionInput =
field.optionsInputs?.[options?.[0]?.value as keyof typeof field.optionsInputs];
const doesFirstOptionHaveInput = !!firstOptionInput;
// If there is only one option and it doesn't have an input required, we don't show the Field for it.
// Because booker doesn't see this in UI, there is no point showing it in FormBuilder to configure it.
if (field.hideWhenJustOneOption && numOptions <= 1 && !doesFirstOptionHaveInput) {
return null;
}
const fieldType = fieldTypesConfigMap[field.type];
const isFieldEditableSystemButOptional = field.editable === "system-but-optional";
const isFieldEditableSystemButHidden = field.editable === "system-but-hidden";
const isFieldEditableSystem = field.editable === "system";
const isUserField =
!isFieldEditableSystem && !isFieldEditableSystemButOptional && !isFieldEditableSystemButHidden;
if (!fieldType) {
throw new Error(`Invalid field type - ${field.type}`);
}
const groupedBySourceLabel = sources.reduce((groupBy, source) => {
const item = groupBy[source.label] || [];
if (source.type === "user" || source.type === "default") {
return groupBy;
}
item.push(source);
groupBy[source.label] = item;
return groupBy;
}, {} as Record<string, NonNullable<(typeof field)["sources"]>>);
return (
<li
key={field.name}
data-testid={`field-${field.name}`}
className="hover:bg-muted group relative flex items-center justify-between p-4 ">
{!disabled && (
<>
{index >= 1 && (
<button
type="button"
className="bg-default text-muted hover:text-emphasis disabled:hover:text-muted border-subtle hover:border-emphasis invisible absolute -left-[12px] -ml-4 -mt-4 mb-4 hidden h-6 w-6 scale-0 items-center justify-center rounded-md border p-1 transition-all hover:shadow disabled:hover:border-inherit disabled:hover:shadow-none group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex"
onClick={() => swap(index, index - 1)}>
<Icon name="arrow-up" className="h-5 w-5" />
</button>
)}
{index < fields.length - 1 && (
<button
type="button"
className="bg-default text-muted hover:border-emphasis border-subtle hover:text-emphasis disabled:hover:text-muted invisible absolute -left-[12px] -ml-4 mt-8 hidden h-6 w-6 scale-0 items-center justify-center rounded-md border p-1 transition-all hover:shadow disabled:hover:border-inherit disabled:hover:shadow-none group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex"
onClick={() => swap(index, index + 1)}>
<Icon name="arrow-down" className="h-5 w-5" />
</button>
)}
</>
)}
<div>
<div className="flex flex-col lg:flex-row lg:items-center">
<div className="text-default text-sm font-semibold ltr:mr-2 rtl:ml-2">
<FieldLabel field={field} />
</div>
<div className="flex items-center space-x-2">
{field.hidden ? (
// Hidden field can't be required, so we don't need to show the Optional badge
<Badge variant="grayWithoutHover">{t("hidden")}</Badge>
) : (
<Badge variant="grayWithoutHover" data-testid={isRequired ? "required" : "optional"}>
{isRequired ? t("required") : t("optional")}
</Badge>
)}
{Object.entries(groupedBySourceLabel).map(([sourceLabel, sources], key) => (
// We don't know how to pluralize `sourceLabel` because it can be anything
<Badge key={key} variant="blue">
{sources.length} {sources.length === 1 ? sourceLabel : `${sourceLabel}s`}
</Badge>
))}
</div>
</div>
<p className="text-subtle max-w-[280px] break-words pt-1 text-sm sm:max-w-[500px]">
{fieldType.label}
</p>
</div>
{field.editable !== "user-readonly" && !disabled && (
<div className="flex items-center space-x-2">
{!isFieldEditableSystem && !isFieldEditableSystemButHidden && !disabled && (
<Switch
data-testid="toggle-field"
disabled={isFieldEditableSystem}
checked={!field.hidden}
onCheckedChange={(checked) => {
update(index, { ...field, hidden: !checked });
}}
classNames={{ container: "p-2 hover:bg-subtle rounded" }}
tooltip={t("show_on_booking_page")}
/>
)}
{isUserField && (
<Button
data-testid="delete-field-action"
color="destructive"
disabled={!isUserField}
variant="icon"
onClick={() => {
removeField(index);
}}
StartIcon="trash-2"
/>
)}
<Button
data-testid="edit-field-action"
color="secondary"
onClick={() => {
editField(index, field);
}}>
{t("edit")}
</Button>
</div>
)}
</li>
);
})}
</ul>
{!disabled && (
<Button
color="minimal"
data-testid="add-field"
onClick={addField}
className="mt-4"
StartIcon="plus">
{addFieldLabel}
</Button>
)}
</div>
{/* Move this Dialog in another component and it would take with it fieldForm */}
{fieldDialog.isOpen && (
<FieldEditDialog
dialog={fieldDialog}
onOpenChange={(isOpen) =>
setFieldDialog({
isOpen,
fieldIndex: -1,
data: null,
})
}
handleSubmit={(data: Parameters<SubmitHandler<RhfFormField>>[0]) => {
const type = data.type || "text";
const isNewField = !fieldDialog.data;
if (isNewField && fields.some((f) => f.name === data.name)) {
showToast(t("form_builder_field_already_exists"), "error");
return;
}
if (fieldDialog.data) {
update(fieldDialog.fieldIndex, data);
} else {
const field: RhfFormField = {
...data,
type,
sources: [
{
label: "User",
type: "user",
id: "user",
fieldRequired: data.required,
},
],
};
field.editable = field.editable || "user";
append(field);
}
setFieldDialog({
isOpen: false,
fieldIndex: -1,
data: null,
});
}}
/>
)}
</div>
);
};
function Options({
label = "Options",
value,
// eslint-disable-next-line @typescript-eslint/no-empty-function
onChange = () => {},
className = "",
readOnly = false,
}: {
label?: string;
value: { label: string; value: string }[];
onChange?: (value: { label: string; value: string }[]) => void;
className?: string;
readOnly?: boolean;
}) {
const [animationRef] = useAutoAnimate<HTMLUListElement>();
if (!value) {
onChange([
{
label: "Option 1",
value: "Option 1",
},
{
label: "Option 2",
value: "Option 2",
},
]);
}
return (
<div className={className}>
<Label>{label}</Label>
<div className="bg-muted rounded-md p-4">
<ul ref={animationRef}>
{value?.map((option, index) => (
<li key={index}>
<div className="flex items-center">
<Input
required
value={option.label}
onChange={(e) => {
// Right now we use label of the option as the value of the option. It allows us to not separately lookup the optionId to know the optionValue
// It has the same drawback that if the label is changed, the value of the option will change. It is not a big deal for now.
value.splice(index, 1, {
label: e.target.value,
value: e.target.value.trim(),
});
onChange(value);
}}
readOnly={readOnly}
placeholder={`Enter Option ${index + 1}`}
/>
{value.length > 2 && !readOnly && (
<Button
type="button"
className="-ml-8 mb-2 hover:!bg-transparent focus:!bg-transparent focus:!outline-none focus:!ring-0"
size="sm"
color="minimal"
StartIcon="x"
onClick={() => {
if (!value) {
return;
}
const newOptions = [...value];
newOptions.splice(index, 1);
onChange(newOptions);
}}
/>
)}
</div>
</li>
))}
</ul>
{!readOnly && (
<Button
color="minimal"
onClick={() => {
value.push({ label: "", value: "" });
onChange(value);
}}
StartIcon="plus">
Add an Option
</Button>
)}
</div>
</div>
);
}
function FieldEditDialog({
dialog,
onOpenChange,
handleSubmit,
}: {
dialog: { isOpen: boolean; fieldIndex: number; data: RhfFormField | null };
onOpenChange: (isOpen: boolean) => void;
handleSubmit: SubmitHandler<RhfFormField>;
}) {
const { t } = useLocale();
const fieldForm = useForm<RhfFormField>({
defaultValues: dialog.data || {},
// resolver: zodResolver(fieldSchema),
});
useEffect(() => {
if (!fieldForm.getValues("type")) {
return;
}
const variantsConfig = getVariantsConfig({
type: fieldForm.getValues("type"),
variantsConfig: fieldForm.getValues("variantsConfig"),
});
// We need to set the variantsConfig in the RHF instead of using a derived value because RHF won't have the variantConfig for the variant that's not rendered yet.
fieldForm.setValue("variantsConfig", variantsConfig);
}, [fieldForm]);
const isFieldEditMode = !!dialog.data;
const fieldType = getCurrentFieldType(fieldForm);
const variantsConfig = fieldForm.watch("variantsConfig");
const fieldTypes = Object.values(fieldTypesConfigMap);
return (
<Dialog open={dialog.isOpen} onOpenChange={onOpenChange}>
<DialogContent className="max-h-none p-0" data-testid="edit-field-dialog">
<Form id="form-builder" form={fieldForm} handleSubmit={handleSubmit}>
<div className="h-auto max-h-[85vh] overflow-auto px-8 pb-7 pt-8">
<DialogHeader title={t("add_a_booking_question")} subtitle={t("booking_questions_description")} />
<SelectField
defaultValue={fieldTypesConfigMap.text}
data-testid="test-field-type"
id="test-field-type"
isDisabled={
fieldForm.getValues("editable") === "system" ||
fieldForm.getValues("editable") === "system-but-optional"
}
onChange={(e) => {
const value = e?.value;
if (!value) {
return;
}
fieldForm.setValue("type", value, { shouldDirty: true });
}}
value={fieldTypesConfigMap[fieldForm.getValues("type")]}
options={fieldTypes.filter((f) => !f.systemOnly)}
label={t("input_type")}
/>
{(() => {
if (!variantsConfig) {
return (
<>
<InputField
required
{...fieldForm.register("name")}
containerClassName="mt-6"
onChange={(e) => {
fieldForm.setValue("name", getFieldIdentifier(e.target.value || ""), {
shouldDirty: true,
});
}}
disabled={
fieldForm.getValues("editable") === "system" ||
fieldForm.getValues("editable") === "system-but-optional"
}
label={t("identifier")}
/>
<CheckboxField
description={t("disable_input_if_prefilled")}
{...fieldForm.register("disableOnPrefill", { setValueAs: Boolean })}
/>
<InputField
{...fieldForm.register("label")}
// System fields have a defaultLabel, so there a label is not required
required={
!["system", "system-but-optional"].includes(fieldForm.getValues("editable") || "")
}
placeholder={t(fieldForm.getValues("defaultLabel") || "")}
containerClassName="mt-6"
label={t("label")}
/>
{fieldType?.isTextType ? (
<InputField
{...fieldForm.register("placeholder")}
containerClassName="mt-6"
label={t("placeholder")}
placeholder={t(fieldForm.getValues("defaultPlaceholder") || "")}
/>
) : null}
{fieldType?.needsOptions && !fieldForm.getValues("getOptionsAt") ? (
<Controller
name="options"
render={({ field: { value, onChange } }) => {
return <Options onChange={onChange} value={value} className="mt-6" />;
}}
/>
) : null}
{!!fieldType?.supportsLengthCheck ? (
<FieldWithLengthCheckSupport containerClassName="mt-6" fieldForm={fieldForm} />
) : null}
<Controller
name="required"
control={fieldForm.control}
render={({ field: { onChange } }) => {
const isRequired = isRequiredField(fieldForm.getValues());
return (
<BooleanToggleGroupField
data-testid="field-required"
disabled={fieldForm.getValues("editable") === "system"}
value={isRequired}
onValueChange={(val) => {
onChange(val);
}}
label={t("required")}
/>
);
}}
/>
</>
);
}
if (!fieldType.isTextType) {
throw new Error("Variants are currently supported only with text type");
}
return <VariantFields variantsConfig={variantsConfig} fieldForm={fieldForm} />;
})()}
</div>
<DialogFooter className="relative rounded px-8" showDivider>
<DialogClose color="secondary">{t("cancel")}</DialogClose>
<Button data-testid="field-add-save" type="submit">
{isFieldEditMode ? t("save") : t("add")}
</Button>
</DialogFooter>
</Form>
</DialogContent>
</Dialog>
);
}
function FieldWithLengthCheckSupport({
fieldForm,
containerClassName = "",
className,
...rest
}: {
fieldForm: UseFormReturn<RhfFormField>;
containerClassName?: string;
} & React.HTMLAttributes<HTMLDivElement>) {
const { t } = useLocale();
const fieldType = getCurrentFieldType(fieldForm);
if (!fieldType.supportsLengthCheck) {
return null;
}
const supportsLengthCheck = fieldType.supportsLengthCheck;
const maxAllowedMaxLength = supportsLengthCheck.maxLength;
return (
<div className={classNames("grid grid-cols-2 gap-4", className)} {...rest}>
<InputField
{...fieldForm.register("minLength", {
valueAsNumber: true,
})}
defaultValue={0}
containerClassName={containerClassName}
label={t("min_characters")}
type="number"
onChange={(e) => {
fieldForm.setValue("minLength", parseInt(e.target.value ?? 0));
// Ensure that maxLength field adjusts its restrictions
fieldForm.trigger("maxLength");
}}
min={0}
max={fieldForm.getValues("maxLength") || maxAllowedMaxLength}
/>
<InputField
{...fieldForm.register("maxLength", {
valueAsNumber: true,
})}
defaultValue={maxAllowedMaxLength}
containerClassName={containerClassName}
label={t("max_characters")}
type="number"
onChange={(e) => {
if (!supportsLengthCheck) {
return;
}
fieldForm.setValue("maxLength", parseInt(e.target.value ?? maxAllowedMaxLength));
// Ensure that minLength field adjusts its restrictions
fieldForm.trigger("minLength");
}}
min={fieldForm.getValues("minLength") || 0}
max={maxAllowedMaxLength}
/>
</div>
);
}
/**
* Shows the label of the field, taking into account the current variant selected
*/
function FieldLabel({ field }: { field: RhfFormField }) {
const { t } = useLocale();
const fieldTypeConfig = fieldTypesConfigMap[field.type];
const fieldTypeConfigVariantsConfig = fieldTypeConfig?.variantsConfig;
const fieldTypeConfigVariants = fieldTypeConfigVariantsConfig?.variants;
const variantsConfig = field.variantsConfig;
const variantsConfigVariants = variantsConfig?.variants;
const defaultVariant = fieldTypeConfigVariantsConfig?.defaultVariant;
if (!fieldTypeConfigVariants || !variantsConfig) {
if (fieldsThatSupportLabelAsSafeHtml.includes(field.type)) {
return (
<span
dangerouslySetInnerHTML={{
// Derive from field.label because label might change in b/w and field.labelAsSafeHtml will not be updated.
__html: markdownToSafeHTML(field.label || "") || t(field.defaultLabel || ""),
}}
/>
);
} else {
return <span>{field.label || t(field.defaultLabel || "")}</span>;
}
}
const variant = field.variant || defaultVariant;
if (!variant) {
throw new Error(
`Field has \`variantsConfig\` but no \`defaultVariant\`${JSON.stringify(fieldTypeConfigVariantsConfig)}`
);
}
const label =
variantsConfigVariants?.[variant as keyof typeof fieldTypeConfigVariants]?.fields?.[0]?.label || "";
return <span>{t(label)}</span>;
}
function VariantSelector() {
// Implement a Variant selector for cases when there are more than 2 variants
return null;
}
function VariantFields({
fieldForm,
variantsConfig,
}: {
fieldForm: UseFormReturn<RhfFormField>;
variantsConfig: RhfFormField["variantsConfig"];
}) {
const { t } = useLocale();
if (!variantsConfig) {
throw new Error("VariantFields component needs variantsConfig");
}
const fieldTypeConfigVariantsConfig = fieldTypesConfigMap[fieldForm.getValues("type")]?.variantsConfig;
if (!fieldTypeConfigVariantsConfig) {
throw new Error("Coniguration Issue: FieldType doesn't have `variantsConfig`");
}
const variantToggleLabel = t(fieldTypeConfigVariantsConfig.toggleLabel || "");
const defaultVariant = fieldTypeConfigVariantsConfig.defaultVariant;
const variantNames = Object.keys(variantsConfig.variants);
const otherVariants = variantNames.filter((v) => v !== defaultVariant);
if (otherVariants.length > 1 && variantToggleLabel) {
throw new Error("More than one other variant. Remove toggleLabel ");
}
const otherVariant = otherVariants[0];
const variantName = fieldForm.watch("variant") || defaultVariant;
const variantFields = variantsConfig.variants[variantName as keyof typeof variantsConfig].fields;
/**
* A variant that has just one field can be shown in a simpler way in UI.
*/
const isSimpleVariant = variantFields.length === 1;
const isDefaultVariant = variantName === defaultVariant;
const supportsVariantToggle = variantNames.length === 2;
return (
<>
{supportsVariantToggle ? (
<Switch
checked={!isDefaultVariant}
label={variantToggleLabel}
data-testid="variant-toggle"
onCheckedChange={(checked) => {
fieldForm.setValue("variant", checked ? otherVariant : defaultVariant);
}}
classNames={{ container: "p-2 mt-2 sm:hover:bg-muted rounded" }}
tooltip={t("Toggle Variant")}
/>
) : (
<VariantSelector />
)}
<InputField
required
{...fieldForm.register("name")}
containerClassName="mt-6"
disabled={
fieldForm.getValues("editable") === "system" ||
fieldForm.getValues("editable") === "system-but-optional"
}
label={t("identifier")}
/>
<CheckboxField
description={t("disable_input_if_prefilled")}
{...fieldForm.register("disableOnPrefill", { setValueAs: Boolean })}
/>
<ul
className={classNames(
!isSimpleVariant ? "border-subtle divide-subtle mt-2 divide-y rounded-md border" : ""
)}>
{variantFields.map((f, index) => {
const rhfVariantFieldPrefix = `variantsConfig.variants.${variantName}.fields.${index}` as const;
const fieldTypeConfigVariants =
fieldTypeConfigVariantsConfig.variants[
variantName as keyof typeof fieldTypeConfigVariantsConfig.variants
];
const appUiFieldConfig =
fieldTypeConfigVariants.fieldsMap[f.name as keyof typeof fieldTypeConfigVariants.fieldsMap];
return (
<li className={classNames(!isSimpleVariant ? "p-4" : "")} key={f.name}>
{!isSimpleVariant && (
<Label className="flex justify-between">
<span>{`Field ${index + 1}`}</span>
<span className="text-muted">{`${fieldForm.getValues("name")}.${f.name}`}</span>
</Label>
)}
<InputField
{...fieldForm.register(`${rhfVariantFieldPrefix}.label`)}
value={f.label || ""}
placeholder={t(appUiFieldConfig?.defaultLabel || "")}
containerClassName="mt-6"
label={t("label")}
/>
<InputField
{...fieldForm.register(`${rhfVariantFieldPrefix}.placeholder`)}
key={f.name}
value={f.placeholder || ""}
containerClassName="mt-6"
label={t("placeholder")}
placeholder={t(appUiFieldConfig?.defaultPlaceholder || "")}
/>
<Controller
name={`${rhfVariantFieldPrefix}.required`}
control={fieldForm.control}
render={({ field: { onChange } }) => {
return (
<BooleanToggleGroupField
data-testid="field-required"
disabled={!appUiFieldConfig?.canChangeRequirability}
value={f.required}
onValueChange={(val) => {
onChange(val);
}}
label={t("required")}
/>
);
}}
/>
</li>
);
})}
</ul>
</>
);
}