2023-03-02 18:15:28 +00:00
import { useAutoAnimate } from "@formkit/auto-animate/react" ;
2023-07-20 05:03:50 +00:00
import { useState , useEffect } from "react" ;
import { Controller , useFieldArray , useFormContext , useForm } from "react-hook-form" ;
import type { UseFormReturn , SubmitHandler } from "react-hook-form" ;
2023-03-02 18:15:28 +00:00
import type { z } from "zod" ;
import { classNames } from "@calcom/lib" ;
import { useLocale } from "@calcom/lib/hooks/useLocale" ;
2023-07-21 18:00:28 +00:00
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML" ;
2023-03-02 18:15:28 +00:00
import {
Label ,
Badge ,
Button ,
Dialog ,
DialogClose ,
DialogContent ,
DialogHeader ,
DialogFooter ,
Form ,
BooleanToggleGroupField ,
SelectField ,
InputField ,
2023-07-20 05:03:50 +00:00
Input ,
2023-04-05 18:14:46 +00:00
Switch ,
2023-07-20 05:03:50 +00:00
showToast ,
2023-03-02 18:15:28 +00:00
} from "@calcom/ui" ;
2023-07-20 05:03:50 +00:00
import { ArrowDown , ArrowUp , X , Plus , Trash2 } from "@calcom/ui/components/icon" ;
2023-03-02 18:15:28 +00:00
2023-07-20 05:03:50 +00:00
import { fieldTypesConfigMap } from "./fieldTypes" ;
2023-07-21 18:00:28 +00:00
import { fieldsThatSupportLabelAsSafeHtml } from "./fieldsThatSupportLabelAsSafeHtml" ;
2023-07-20 05:03:50 +00:00
import type { fieldsSchema } from "./schema" ;
import { getVariantsConfig } from "./utils" ;
2023-08-21 17:11:47 +00:00
import { getFieldIdentifier } from "./utils/getFieldIdentifier" ;
2023-03-02 18:15:28 +00:00
type RhfForm = {
fields : z.infer < typeof fieldsSchema > ;
} ;
type RhfFormFields = RhfForm [ "fields" ] ;
2023-07-20 05:03:50 +00:00
2023-03-02 18:15:28 +00:00
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 ,
2023-04-13 02:10:23 +00:00
disabled ,
LockedIcon ,
2023-04-06 08:17:53 +00:00
dataStore ,
2023-03-02 18:15:28 +00:00
} : {
formProp : string ;
title : string ;
description : string ;
addFieldLabel : string ;
2023-04-13 02:10:23 +00:00
disabled : boolean ;
LockedIcon : false | JSX . Element ;
2023-04-06 08:17:53 +00:00
/ * *
* 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 } [ ] > ;
} ;
2023-03-02 18:15:28 +00:00
} ) {
// 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 ( ) ;
2023-07-20 05:03:50 +00:00
2023-03-02 18:15:28 +00:00
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 ,
2023-07-20 05:03:50 +00:00
data : { } as RhfFormField | null ,
2023-03-02 18:15:28 +00:00
} ) ;
const addField = ( ) = > {
setFieldDialog ( {
isOpen : true ,
fieldIndex : - 1 ,
2023-07-20 05:03:50 +00:00
data : null ,
2023-03-02 18:15:28 +00:00
} ) ;
} ;
const editField = ( index : number , data : RhfFormField ) = > {
setFieldDialog ( {
isOpen : true ,
fieldIndex : index ,
2023-07-20 05:03:50 +00:00
data ,
2023-03-02 18:15:28 +00:00
} ) ;
} ;
const removeField = ( index : number ) = > {
remove ( index ) ;
} ;
return (
< div >
< div >
2023-04-13 02:10:23 +00:00
< div className = "text-default text-sm font-semibold ltr:mr-1 rtl:ml-1" >
{ title }
{ LockedIcon }
< / div >
2023-04-05 18:14:46 +00:00
< p className = "text-subtle max-w-[280px] break-words py-1 text-sm sm:max-w-[500px]" > { description } < / p >
2023-04-13 02:10:23 +00:00
< ul className = "border-default divide-subtle mt-2 divide-y rounded-md border" >
2023-03-02 18:15:28 +00:00
{ fields . map ( ( field , index ) = > {
2023-04-06 08:17:53 +00:00
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 ;
}
2023-07-20 05:03:50 +00:00
const fieldType = fieldTypesConfigMap [ field . type ] ;
2023-03-10 14:11:41 +00:00
const isRequired = field . required ;
2023-04-06 08:17:53 +00:00
const isFieldEditableSystemButOptional = field . editable === "system-but-optional" ;
2023-07-13 11:57:30 +00:00
const isFieldEditableSystemButHidden = field . editable === "system-but-hidden" ;
2023-04-06 08:17:53 +00:00
const isFieldEditableSystem = field . editable === "system" ;
2023-07-18 08:53:01 +00:00
const isUserField =
! isFieldEditableSystem && ! isFieldEditableSystemButOptional && ! isFieldEditableSystemButHidden ;
2023-03-02 18:15:28 +00:00
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
2023-03-16 05:25:37 +00:00
key = { field . name }
2023-03-07 17:40:47 +00:00
data - testid = { ` field- ${ field . name } ` }
2023-04-06 08:17:53 +00:00
className = "hover:bg-muted group relative flex items-center justify-between p-4 " >
2023-04-13 02:10:23 +00:00
{ ! disabled && (
< >
{ index >= 1 && (
< button
type = "button"
2023-06-22 22:25:37 +00:00
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"
2023-04-13 02:10:23 +00:00
onClick = { ( ) = > swap ( index , index - 1 ) } >
< ArrowUp className = "h-5 w-5" / >
< / button >
) }
{ index < fields . length - 1 && (
< button
type = "button"
2023-06-22 22:25:37 +00:00
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"
2023-04-13 02:10:23 +00:00
onClick = { ( ) = > swap ( index , index + 1 ) } >
< ArrowDown className = "h-5 w-5" / >
< / button >
) }
< / >
2023-03-16 05:25:37 +00:00
) }
2023-04-13 02:10:23 +00:00
2023-03-02 18:15:28 +00:00
< div >
< div className = "flex flex-col lg:flex-row lg:items-center" >
2023-04-06 08:17:53 +00:00
< div className = "text-default text-sm font-semibold ltr:mr-2 rtl:ml-2" >
2023-07-20 05:03:50 +00:00
< FieldLabel field = { field } / >
2023-03-02 18:15:28 +00:00
< / div >
< div className = "flex items-center space-x-2" >
2023-03-10 14:11:41 +00:00
{ field . hidden ? (
// Hidden field can't be required, so we don't need to show the Optional badge
2023-04-06 08:17:53 +00:00
< Badge variant = "grayWithoutHover" > { t ( "hidden" ) } < / Badge >
2023-03-10 14:11:41 +00:00
) : (
2023-04-06 08:17:53 +00:00
< Badge variant = "grayWithoutHover" > { isRequired ? t ( "required" ) : t ( "optional" ) } < / Badge >
2023-03-10 14:11:41 +00:00
) }
2023-03-02 18:15:28 +00:00
{ 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 >
2023-04-06 08:17:53 +00:00
< p className = "text-subtle max-w-[280px] break-words pt-1 text-sm sm:max-w-[500px]" >
2023-03-02 18:15:28 +00:00
{ fieldType . label }
< / p >
< / div >
2023-04-13 02:10:23 +00:00
{ field . editable !== "user-readonly" && ! disabled && (
2023-03-02 18:15:28 +00:00
< div className = "flex items-center space-x-2" >
2023-07-13 11:57:30 +00:00
{ ! isFieldEditableSystem && ! isFieldEditableSystemButHidden && ! disabled && (
2023-04-06 08:17:53 +00:00
< Switch
data - testid = "toggle-field"
disabled = { isFieldEditableSystem }
checked = { ! field . hidden }
onCheckedChange = { ( checked ) = > {
update ( index , { . . . field , hidden : ! checked } ) ;
} }
2023-06-05 22:24:10 +00:00
classNames = { { container : "p-2 hover:bg-subtle rounded" } }
2023-04-06 08:17:53 +00:00
tooltip = { t ( "show_on_booking_page" ) }
/ >
) }
{ isUserField && (
< Button
color = "destructive"
disabled = { ! isUserField }
variant = "icon"
onClick = { ( ) = > {
removeField ( index ) ;
} }
2023-04-12 15:26:31 +00:00
StartIcon = { Trash2 }
2023-04-06 08:17:53 +00:00
/ >
) }
2023-03-02 18:15:28 +00:00
< Button
2023-03-16 05:10:20 +00:00
data - testid = "edit-field-action"
2023-03-02 18:15:28 +00:00
color = "secondary"
onClick = { ( ) = > {
editField ( index , field ) ;
} } >
2023-04-06 08:17:53 +00:00
{ t ( "edit" ) }
2023-03-02 18:15:28 +00:00
< / Button >
< / div >
) }
< / li >
) ;
} ) }
< / ul >
2023-04-13 02:10:23 +00:00
{ ! disabled && (
< Button
color = "minimal"
data - testid = "add-field"
onClick = { addField }
className = "mt-4"
StartIcon = { Plus } >
{ addFieldLabel }
< / Button >
) }
2023-03-02 18:15:28 +00:00
< / div >
2023-07-20 05:03:50 +00:00
{ /* 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 } >
2023-06-22 22:25:37 +00:00
< div className = "h-auto max-h-[85vh] overflow-auto px-8 pb-7 pt-8" >
2023-06-09 09:38:18 +00:00
< DialogHeader title = { t ( "add_a_booking_question" ) } subtitle = { t ( "booking_questions_description" ) } / >
2023-07-20 05:03:50 +00:00
< 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 ) {
2023-03-02 18:15:28 +00:00
return ;
}
2023-07-20 05:03:50 +00:00
fieldForm . setValue ( "type" , value ) ;
} }
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"
2023-08-21 17:11:47 +00:00
onChange = { ( e ) = > {
fieldForm . setValue ( "name" , getFieldIdentifier ( e . target . value || "" ) ) ;
} }
2023-07-20 05:03:50 +00:00
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" ) }
/ >
) ;
2023-03-02 18:15:28 +00:00
} }
/ >
2023-07-20 05:03:50 +00:00
< / >
) ;
}
if ( ! fieldType . isTextType ) {
throw new Error ( "Variants are currently supported only with text type" ) ;
}
return < VariantFields variantsConfig = { variantsConfig } fieldForm = { fieldForm } / > ;
} ) ( ) }
2023-03-02 18:15:28 +00:00
< / div >
2023-07-20 05:03:50 +00:00
2023-07-18 08:53:01 +00:00
< DialogFooter className = "relative rounded px-8" showDivider >
2023-05-30 15:44:24 +00:00
< DialogClose color = "secondary" > { t ( "cancel" ) } < / DialogClose >
2023-07-20 05:03:50 +00:00
< Button data - testid = "field-add-save" type = "submit" >
2023-05-30 15:44:24 +00:00
{ isFieldEditMode ? t ( "save" ) : t ( "add" ) }
< / Button >
< / DialogFooter >
2023-07-20 05:03:50 +00:00
< / Form >
< / DialogContent >
< / Dialog >
2023-03-02 18:15:28 +00:00
) ;
2023-07-20 05:03:50 +00:00
}
2023-03-02 18:15:28 +00:00
2023-07-20 05:03:50 +00:00
/ * *
* 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 ;
2023-08-11 06:33:38 +00:00
const variantsConfigVariants = variantsConfig ? . variants ;
2023-07-20 05:03:50 +00:00
const defaultVariant = fieldTypeConfigVariantsConfig ? . defaultVariant ;
if ( ! fieldTypeConfigVariants || ! variantsConfig ) {
2023-07-21 18:00:28 +00:00
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 > ;
}
2023-03-02 18:15:28 +00:00
}
2023-07-20 05:03:50 +00:00
const variant = field . variant || defaultVariant ;
if ( ! variant ) {
throw new Error (
"Field has `variantsConfig` but no `defaultVariant`" + JSON . stringify ( fieldTypeConfigVariantsConfig )
2023-03-02 18:15:28 +00:00
) ;
}
2023-08-11 06:33:38 +00:00
const label =
variantsConfigVariants ? . [ variant as keyof typeof fieldTypeConfigVariants ] ? . fields ? . [ 0 ] ? . label || "" ;
return < span > { t ( label ) } < / span > ;
2023-07-20 05:03:50 +00:00
}
2023-03-02 18:15:28 +00:00
2023-07-20 05:03:50 +00:00
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" ) ;
2023-03-02 18:15:28 +00:00
}
2023-07-20 05:03:50 +00:00
const fieldTypeConfigVariantsConfig = fieldTypesConfigMap [ fieldForm . getValues ( "type" ) ] ? . variantsConfig ;
2023-03-02 18:15:28 +00:00
2023-07-20 05:03:50 +00:00
if ( ! fieldTypeConfigVariantsConfig ) {
throw new Error ( "Coniguration Issue: FieldType doesn't have `variantsConfig`" ) ;
2023-03-02 18:15:28 +00:00
}
2023-07-20 05:03:50 +00:00
const variantToggleLabel = t ( fieldTypeConfigVariantsConfig . toggleLabel || "" ) ;
2023-03-02 18:15:28 +00:00
2023-07-20 05:03:50 +00:00
const defaultVariant = fieldTypeConfigVariantsConfig . defaultVariant ;
2023-03-02 18:15:28 +00:00
2023-07-20 05:03:50 +00:00
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 " ) ;
2023-03-02 18:15:28 +00:00
}
2023-07-20 05:03:50 +00:00
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" ) }
2023-03-02 18:15:28 +00:00
/ >
2023-07-20 05:03:50 +00:00
) : (
< VariantSelector / >
) }
2023-03-02 18:15:28 +00:00
2023-07-20 05:03:50 +00:00
< InputField
required
{ . . . fieldForm . register ( "name" ) }
containerClassName = "mt-6"
disabled = {
fieldForm . getValues ( "editable" ) === "system" ||
fieldForm . getValues ( "editable" ) === "system-but-optional"
}
label = { t ( "identifier" ) }
/ >
2023-03-02 18:15:28 +00:00
2023-07-20 05:03:50 +00:00
< 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 ] ;
2023-03-02 18:15:28 +00:00
return (
2023-07-20 05:03:50 +00:00
< 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" ) }
2023-03-02 18:15:28 +00:00
/ >
2023-07-20 05:03:50 +00:00
< 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 } } ) = > {
2023-03-02 18:15:28 +00:00
return (
2023-07-20 05:03:50 +00:00
< BooleanToggleGroupField
data - testid = "field-required"
disabled = { ! appUiFieldConfig ? . canChangeRequirability }
value = { f . required }
onValueChange = { ( val ) = > {
onChange ( val ) ;
} }
label = { t ( "required" ) }
/ >
2023-03-02 18:15:28 +00:00
) ;
} }
/ >
2023-07-20 05:03:50 +00:00
< / li >
2023-03-02 18:15:28 +00:00
) ;
2023-07-20 05:03:50 +00:00
} ) }
< / ul >
< / >
2023-03-02 18:15:28 +00:00
) ;
2023-07-20 05:03:50 +00:00
}