692 lines
25 KiB
TypeScript
692 lines
25 KiB
TypeScript
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
|
import { useState, useEffect } from "react";
|
|
import { Controller, useFieldArray, useFormContext, useForm } from "react-hook-form";
|
|
import type { UseFormReturn, SubmitHandler } 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 {
|
|
Label,
|
|
Badge,
|
|
Button,
|
|
Dialog,
|
|
DialogClose,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogFooter,
|
|
Form,
|
|
BooleanToggleGroupField,
|
|
SelectField,
|
|
InputField,
|
|
Input,
|
|
Switch,
|
|
showToast,
|
|
} from "@calcom/ui";
|
|
import { ArrowDown, ArrowUp, X, Plus, Trash2 } from "@calcom/ui/components/icon";
|
|
|
|
import { fieldTypesConfigMap } from "./fieldTypes";
|
|
import { fieldsThatSupportLabelAsSafeHtml } from "./fieldsThatSupportLabelAsSafeHtml";
|
|
import type { fieldsSchema } from "./schema";
|
|
import { getVariantsConfig } from "./utils";
|
|
import { getFieldIdentifier } from "./utils/getFieldIdentifier";
|
|
|
|
type RhfForm = {
|
|
fields: z.infer<typeof fieldsSchema>;
|
|
};
|
|
|
|
type RhfFormFields = RhfForm["fields"];
|
|
|
|
type RhfFormField = RhfFormFields[number];
|
|
|
|
/**
|
|
* 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 { 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 ltr:mr-1 rtl:ml-1">
|
|
{title}
|
|
{LockedIcon}
|
|
</div>
|
|
<p className="text-subtle max-w-[280px] break-words py-1 text-sm sm:max-w-[500px]">{description}</p>
|
|
<ul className="border-default divide-subtle mt-2 divide-y rounded-md border">
|
|
{fields.map((field, index) => {
|
|
const options = field.options
|
|
? field.options
|
|
: field.getOptionsAt
|
|
? dataStore.options[field.getOptionsAt as keyof typeof dataStore]
|
|
: [];
|
|
const numOptions = options?.length ?? 0;
|
|
if (field.hideWhenJustOneOption && numOptions <= 1) {
|
|
return null;
|
|
}
|
|
const fieldType = fieldTypesConfigMap[field.type];
|
|
const isRequired = field.required;
|
|
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 sources = field.sources || [];
|
|
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-default 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)}>
|
|
<ArrowUp className="h-5 w-5" />
|
|
</button>
|
|
)}
|
|
{index < fields.length - 1 && (
|
|
<button
|
|
type="button"
|
|
className="bg-default text-muted hover:border-emphasis border-default 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)}>
|
|
<ArrowDown 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">{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
|
|
color="destructive"
|
|
disabled={!isUserField}
|
|
variant="icon"
|
|
onClick={() => {
|
|
removeField(index);
|
|
}}
|
|
StartIcon={Trash2}
|
|
/>
|
|
)}
|
|
<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 = fieldTypesConfigMap[fieldForm.watch("type") || "text"];
|
|
|
|
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}
|
|
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);
|
|
}}
|
|
value={fieldTypesConfigMap[fieldForm.getValues("type")]}
|
|
options={fieldTypes.filter((f) => !f.systemOnly)}
|
|
label={t("input_type")}
|
|
classNames={{
|
|
menuList: () => "min-h-[27.25rem]",
|
|
}}
|
|
/>
|
|
{(() => {
|
|
if (!variantsConfig) {
|
|
return (
|
|
<>
|
|
<InputField
|
|
required
|
|
{...fieldForm.register("name")}
|
|
containerClassName="mt-6"
|
|
onChange={(e) => {
|
|
fieldForm.setValue("name", getFieldIdentifier(e.target.value || ""));
|
|
}}
|
|
disabled={
|
|
fieldForm.getValues("editable") === "system" ||
|
|
fieldForm.getValues("editable") === "system-but-optional"
|
|
}
|
|
label={t("identifier")}
|
|
/>
|
|
<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}
|
|
<Controller
|
|
name="required"
|
|
control={fieldForm.control}
|
|
render={({ field: { value, onChange } }) => {
|
|
return (
|
|
<BooleanToggleGroupField
|
|
data-testid="field-required"
|
|
disabled={fieldForm.getValues("editable") === "system"}
|
|
value={value}
|
|
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>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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")}
|
|
/>
|
|
|
|
<ul
|
|
className={classNames(
|
|
!isSimpleVariant ? "border-default 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>
|
|
</>
|
|
);
|
|
}
|