2023-03-02 18:15:28 +00:00
import { useAutoAnimate } from "@formkit/auto-animate/react" ;
import { ErrorMessage } from "@hookform/error-message" ;
import { useState } from "react" ;
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 {
Label ,
Badge ,
Button ,
Dialog ,
DialogClose ,
DialogContent ,
DialogHeader ,
DialogFooter ,
Form ,
BooleanToggleGroupField ,
SelectField ,
InputField ,
showToast ,
2023-04-05 18:14:46 +00:00
Switch ,
2023-03-02 18:15:28 +00:00
} from "@calcom/ui" ;
2023-04-12 15:26:31 +00:00
import { ArrowDown , ArrowUp , X , Plus , Trash2 , Info } from "@calcom/ui/components/icon" ;
2023-03-02 18:15:28 +00:00
import { Components } from "./Components" ;
import type { fieldsSchema } from "./FormBuilderFieldsSchema" ;
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 ,
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
} ) {
const FieldTypesMap : Record <
string ,
{
value : RhfForm [ "fields" ] [ number ] [ "type" ] ;
label : string ;
needsOptions? : boolean ;
systemOnly? : boolean ;
isTextType? : boolean ;
}
> = {
name : {
label : "Name" ,
value : "name" ,
isTextType : true ,
} ,
email : {
label : "Email" ,
value : "email" ,
isTextType : true ,
} ,
phone : {
label : "Phone" ,
value : "phone" ,
isTextType : true ,
} ,
text : {
label : "Short Text" ,
value : "text" ,
isTextType : true ,
} ,
number : {
label : "Number" ,
value : "number" ,
isTextType : true ,
} ,
textarea : {
label : "Long Text" ,
value : "textarea" ,
isTextType : true ,
} ,
select : {
label : "Select" ,
value : "select" ,
needsOptions : true ,
isTextType : true ,
} ,
multiselect : {
label : "MultiSelect" ,
value : "multiselect" ,
needsOptions : true ,
isTextType : false ,
} ,
multiemail : {
label : "Multiple Emails" ,
value : "multiemail" ,
isTextType : true ,
} ,
radioInput : {
label : "Radio Input" ,
value : "radioInput" ,
isTextType : false ,
systemOnly : true ,
2023-04-06 08:17:53 +00:00
// This is false currently because we don't want to show the options for Location field right now. It is the only field with type radioInput.
// needsOptions: true,
2023-03-02 18:15:28 +00:00
} ,
checkbox : {
label : "Checkbox Group" ,
value : "checkbox" ,
needsOptions : true ,
isTextType : false ,
} ,
radio : {
label : "Radio Group" ,
value : "radio" ,
needsOptions : true ,
isTextType : false ,
} ,
boolean : {
label : "Checkbox" ,
value : "boolean" ,
isTextType : false ,
} ,
} ;
const FieldTypes = Object . values ( FieldTypesMap ) ;
// 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 fieldForm = useForm < RhfFormField > ( ) ;
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" ,
} ) ;
function OptionsField ( {
label = "Options" ,
value ,
2023-04-06 08:17:53 +00:00
// eslint-disable-next-line @typescript-eslint/no-empty-function
onChange = ( ) = > { } ,
2023-03-02 18:15:28 +00:00
className = "" ,
readOnly = false ,
} : {
label? : string ;
value : { label : string ; value : string } [ ] ;
2023-04-06 08:17:53 +00:00
onChange ? : ( value : { label : string ; value : string } [ ] ) = > void ;
2023-03-02 18:15:28 +00:00
className? : string ;
readOnly? : boolean ;
} ) {
const [ animationRef ] = useAutoAnimate < HTMLUListElement > ( ) ;
if ( ! value ) {
onChange ( [
{
label : "Option 1" ,
value : "Option 1" ,
} ,
{
label : "Option 2" ,
value : "Option 2" ,
} ,
] ) ;
}
2023-05-30 14:13:17 +00:00
const crossButton = ( index : number ) = > {
return (
< Button
type = "button"
className = "ml-3 p-0 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 ) ;
} }
/ >
) ;
} ;
2023-03-02 18:15:28 +00:00
return (
< div className = { className } >
< Label > { label } < / Label >
2023-04-05 18:14:46 +00:00
< div className = "bg-muted rounded-md p-4" >
2023-03-02 18:15:28 +00:00
< ul ref = { animationRef } >
{ value ? . map ( ( option , index ) = > (
< li key = { index } >
< div className = "flex items-center" >
2023-05-30 14:13:17 +00:00
< InputField
2023-03-02 18:15:28 +00:00
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 ,
2023-05-17 08:47:48 +00:00
value : e.target.value.trim ( ) ,
2023-03-02 18:15:28 +00:00
} ) ;
onChange ( value ) ;
} }
2023-05-30 14:13:17 +00:00
addOnSuffix = { value . length > 2 && ! readOnly && crossButton ( index ) }
2023-03-02 18:15:28 +00:00
readOnly = { readOnly }
placeholder = { ` Enter Option ${ index + 1 } ` }
2023-05-30 14:13:17 +00:00
containerClassName = "w-full"
2023-03-02 18:15:28 +00:00
/ >
< / div >
< / li >
) ) }
< / ul >
{ ! readOnly && (
< Button
color = "minimal"
onClick = { ( ) = > {
value . push ( { label : "" , value : "" } ) ;
onChange ( value ) ;
} }
2023-04-12 15:26:31 +00:00
StartIcon = { Plus } >
2023-03-02 18:15:28 +00:00
Add an Option
< / Button >
) }
< / div >
< / div >
) ;
}
const [ fieldDialog , setFieldDialog ] = useState ( {
isOpen : false ,
fieldIndex : - 1 ,
} ) ;
const addField = ( ) = > {
fieldForm . reset ( { } ) ;
setFieldDialog ( {
isOpen : true ,
fieldIndex : - 1 ,
} ) ;
} ;
const editField = ( index : number , data : RhfFormField ) = > {
fieldForm . reset ( data ) ;
setFieldDialog ( {
isOpen : true ,
fieldIndex : index ,
} ) ;
} ;
const removeField = ( index : number ) = > {
remove ( index ) ;
} ;
2023-03-06 23:27:29 +00:00
const fieldType = FieldTypesMap [ fieldForm . watch ( "type" ) || "text" ] ;
2023-03-02 18:15:28 +00:00
const isFieldEditMode = fieldDialog . fieldIndex !== - 1 ;
2023-04-06 08:17:53 +00:00
2023-03-02 18:15:28 +00:00
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-03-02 18:15:28 +00:00
const fieldType = FieldTypesMap [ 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-13 11:57:30 +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-03-02 18:15:28 +00:00
{ field . label || t ( field . defaultLabel || "" ) }
< / 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 >
< Dialog
open = { fieldDialog . isOpen }
onOpenChange = { ( isOpen ) = >
setFieldDialog ( {
isOpen ,
fieldIndex : - 1 ,
} )
} >
2023-05-30 15:44:24 +00:00
< DialogContent className = "max-h-none p-0" data - testid = "edit-field-dialog" >
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-03-02 18:15:28 +00:00
< Form
2023-05-30 15:44:24 +00:00
id = "form-builder"
2023-03-02 18:15:28 +00:00
form = { fieldForm }
handleSubmit = { ( data ) = > {
2023-03-06 23:27:29 +00:00
const type = data . type || "text" ;
2023-03-02 18:15:28 +00:00
const isNewField = fieldDialog . fieldIndex == - 1 ;
if ( isNewField && fields . some ( ( f ) = > f . name === data . name ) ) {
showToast ( t ( "form_builder_field_already_exists" ) , "error" ) ;
return ;
}
if ( fieldDialog . fieldIndex !== - 1 ) {
update ( fieldDialog . fieldIndex , data ) ;
} else {
const field : RhfFormField = {
. . . data ,
2023-03-06 23:27:29 +00:00
type ,
2023-03-02 18:15:28 +00:00
sources : [
{
label : "User" ,
type : "user" ,
id : "user" ,
fieldRequired : data.required ,
} ,
] ,
} ;
field . editable = field . editable || "user" ;
append ( field ) ;
}
setFieldDialog ( {
isOpen : false ,
fieldIndex : - 1 ,
} ) ;
} } >
< SelectField
2023-03-06 23:27:29 +00:00
defaultValue = { FieldTypes [ 3 ] } // "text" as defaultValue
2023-03-07 17:40:47 +00:00
id = "test-field-type"
2023-03-02 18:15:28 +00:00
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 = { FieldTypesMap [ fieldForm . getValues ( "type" ) ] }
options = { FieldTypes . filter ( ( f ) = > ! f . systemOnly ) }
2023-04-06 08:17:53 +00:00
label = { t ( "input_type" ) }
2023-04-13 21:45:18 +00:00
classNames = { {
2023-06-08 14:25:23 +00:00
menuList : ( ) = > "min-h-[22.25rem] " ,
2023-04-13 21:45:18 +00:00
} }
2023-03-02 18:15:28 +00:00
/ >
< InputField
required
{ . . . fieldForm . register ( "name" ) }
containerClassName = "mt-6"
disabled = {
fieldForm . getValues ( "editable" ) === "system" ||
fieldForm . getValues ( "editable" ) === "system-but-optional"
}
2023-06-05 09:47:08 +00:00
label = { t ( "identifier" ) }
2023-03-02 18:15:28 +00:00
/ >
< 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"
2023-03-10 14:11:41 +00:00
label = { t ( "label" ) }
2023-03-02 18:15:28 +00:00
/ >
{ fieldType ? . isTextType ? (
< InputField
{ . . . fieldForm . register ( "placeholder" ) }
containerClassName = "mt-6"
2023-03-10 14:11:41 +00:00
label = { t ( "placeholder" ) }
2023-03-02 18:15:28 +00:00
placeholder = { t ( fieldForm . getValues ( "defaultPlaceholder" ) || "" ) }
/ >
) : null }
2023-04-06 08:17:53 +00:00
{ fieldType ? . needsOptions && ! fieldForm . getValues ( "getOptionsAt" ) ? (
2023-03-02 18:15:28 +00:00
< Controller
name = "options"
render = { ( { field : { value , onChange } } ) = > {
return < OptionsField onChange = { onChange } value = { value } className = "mt-6" / > ;
} }
/ >
) : null }
2023-04-06 08:17:53 +00:00
{ /* TODO: Maybe we should show location options in readOnly mode in Booking Questions. Right now options are not shown in Manage Booking Questions UI for location Booking Question */ }
{ / * { f i e l d F o r m . g e t V a l u e s ( " g e t O p t i o n s A t " ) ? (
< OptionsField
readOnly = { true }
value = { dataStore . options [ fieldForm . getValues ( "getOptionsAt" ) as keyof typeof dataStore ] }
className = "mt-6"
/ >
) : null } * / }
2023-03-02 18:15:28 +00:00
< Controller
name = "required"
control = { fieldForm . control }
render = { ( { field : { value , onChange } } ) = > {
return (
< BooleanToggleGroupField
2023-03-16 05:10:20 +00:00
data - testid = "field-required"
2023-03-02 18:15:28 +00:00
disabled = { fieldForm . getValues ( "editable" ) === "system" }
value = { value }
onValueChange = { ( val ) = > {
onChange ( val ) ;
} }
2023-03-10 14:11:41 +00:00
label = { t ( "required" ) }
2023-03-02 18:15:28 +00:00
/ >
) ;
} }
/ >
< / Form >
< / div >
2023-06-30 04:06:01 +00:00
< DialogFooter className = "relative mt-5 rounded px-8 pb-8" showDivider >
2023-05-30 15:44:24 +00:00
< DialogClose color = "secondary" > { t ( "cancel" ) } < / DialogClose >
< Button data - testid = "field-add-save" type = "submit" form = "form-builder" >
{ isFieldEditMode ? t ( "save" ) : t ( "add" ) }
< / Button >
< / DialogFooter >
2023-03-02 18:15:28 +00:00
< / DialogContent >
< / Dialog >
< / div >
) ;
} ;
// TODO: Add consistent `label` support to all the components and then remove the usage of WithLabel.
// Label should be handled by each Component itself.
const WithLabel = ( {
field ,
children ,
readOnly ,
} : {
field : Partial < RhfFormField > ;
readOnly : boolean ;
children : React.ReactNode ;
} ) = > {
return (
< div >
{ /* multiemail doesnt show label initially. It is shown on clicking CTA */ }
{ /* boolean type doesn't have a label overall, the radio has it's own label */ }
{ /* Component itself managing it's label should remove these checks */ }
{ field . type !== "boolean" && field . type !== "multiemail" && field . label && (
< div className = "mb-2 flex items-center" >
2023-04-06 08:17:53 +00:00
< Label className = "!mb-0" >
< span > { field . label } < / span >
2023-06-22 22:25:37 +00:00
< span className = "text-emphasis -mb-1 ml-1 text-sm font-medium leading-none" >
2023-04-06 08:17:53 +00:00
{ ! readOnly && field . required ? "*" : "" }
< / span >
< / Label >
2023-03-02 18:15:28 +00:00
< / div >
) }
{ children }
< / div >
) ;
} ;
type ValueProps =
| {
value : string [ ] ;
setValue : ( value : string [ ] ) = > void ;
}
| {
value : string ;
setValue : ( value : string ) = > void ;
}
| {
value : {
value : string ;
optionValue : string ;
} ;
setValue : ( value : { value : string ; optionValue : string } ) = > void ;
}
| {
value : boolean ;
setValue : ( value : boolean ) = > void ;
} ;
export const ComponentForField = ( {
field ,
value ,
setValue ,
readOnly ,
} : {
field : Omit < RhfFormField , " editable " | " label " > & {
// Label is optional because radioInput doesn't have a label
label? : string ;
} ;
readOnly : boolean ;
} & ValueProps ) = > {
2023-03-06 23:27:29 +00:00
const fieldType = field . type || "text" ;
2023-03-02 18:15:28 +00:00
const componentConfig = Components [ fieldType ] ;
const isValueOfPropsType = ( val : unknown , propsType : typeof componentConfig . propsType ) = > {
const propsTypeConditionMap = {
boolean : typeof val === "boolean" ,
multiselect : val instanceof Array && val . every ( ( v ) = > typeof v === "string" ) ,
objectiveWithInput : typeof val === "object" && val !== null ? "value" in val : false ,
select : typeof val === "string" ,
text : typeof val === "string" ,
textList : val instanceof Array && val . every ( ( v ) = > typeof v === "string" ) ,
} as const ;
if ( ! propsTypeConditionMap [ propsType ] ) throw new Error ( ` Unknown propsType ${ propsType } ` ) ;
return propsTypeConditionMap [ propsType ] ;
} ;
// If possible would have wanted `isValueOfPropsType` to narrow the type of `value` and `setValue` accordingly, but can't seem to do it.
// So, code following this uses type assertion to tell TypeScript that everything has been validated
if ( value !== undefined && ! isValueOfPropsType ( value , componentConfig . propsType ) ) {
throw new Error (
` Value ${ value } is not valid for type ${ componentConfig . propsType } for field ${ field . name } `
) ;
}
if ( componentConfig . propsType === "text" ) {
return (
< WithLabel field = { field } readOnly = { readOnly } >
< componentConfig.factory
placeholder = { field . placeholder }
2023-04-15 13:22:51 +00:00
name = { field . name }
2023-03-02 18:15:28 +00:00
label = { field . label }
readOnly = { readOnly }
value = { value as string }
setValue = { setValue as ( arg : typeof value ) = > void }
/ >
< / WithLabel >
) ;
}
if ( componentConfig . propsType === "boolean" ) {
return (
< WithLabel field = { field } readOnly = { readOnly } >
< componentConfig.factory
2023-04-15 13:22:51 +00:00
name = { field . name }
2023-03-02 18:15:28 +00:00
label = { field . label }
readOnly = { readOnly }
value = { value as boolean }
setValue = { setValue as ( arg : typeof value ) = > void }
placeholder = { field . placeholder }
/ >
< / WithLabel >
) ;
}
if ( componentConfig . propsType === "textList" ) {
return (
< WithLabel field = { field } readOnly = { readOnly } >
< componentConfig.factory
placeholder = { field . placeholder }
2023-04-15 13:22:51 +00:00
name = { field . name }
2023-03-02 18:15:28 +00:00
label = { field . label }
readOnly = { readOnly }
value = { value as string [ ] }
setValue = { setValue as ( arg : typeof value ) = > void }
/ >
< / WithLabel >
) ;
}
if ( componentConfig . propsType === "select" ) {
if ( ! field . options ) {
throw new Error ( "Field options is not defined" ) ;
}
return (
< WithLabel field = { field } readOnly = { readOnly } >
< componentConfig.factory
readOnly = { readOnly }
value = { value as string }
2023-04-15 13:22:51 +00:00
name = { field . name }
2023-03-02 18:15:28 +00:00
placeholder = { field . placeholder }
setValue = { setValue as ( arg : typeof value ) = > void }
options = { field . options . map ( ( o ) = > ( { . . . o , title : o.label } ) ) }
/ >
< / WithLabel >
) ;
}
if ( componentConfig . propsType === "multiselect" ) {
if ( ! field . options ) {
throw new Error ( "Field options is not defined" ) ;
}
return (
< WithLabel field = { field } readOnly = { readOnly } >
< componentConfig.factory
placeholder = { field . placeholder }
2023-04-15 13:22:51 +00:00
name = { field . name }
2023-03-02 18:15:28 +00:00
readOnly = { readOnly }
value = { value as string [ ] }
setValue = { setValue as ( arg : typeof value ) = > void }
options = { field . options . map ( ( o ) = > ( { . . . o , title : o.label } ) ) }
/ >
< / WithLabel >
) ;
}
if ( componentConfig . propsType === "objectiveWithInput" ) {
if ( ! field . options ) {
throw new Error ( "Field options is not defined" ) ;
}
if ( ! field . optionsInputs ) {
throw new Error ( "Field optionsInputs is not defined" ) ;
}
return field . options . length ? (
< WithLabel field = { field } readOnly = { readOnly } >
< componentConfig.factory
placeholder = { field . placeholder }
readOnly = { readOnly }
name = { field . name }
value = { value as { value : string ; optionValue : string } }
setValue = { setValue as ( arg : typeof value ) = > void }
optionsInputs = { field . optionsInputs }
options = { field . options }
2023-04-06 08:17:53 +00:00
required = { field . required }
2023-03-02 18:15:28 +00:00
/ >
< / WithLabel >
) : null ;
}
throw new Error ( ` Field ${ field . name } does not have a valid propsType ` ) ;
} ;
export const FormBuilderField = ( {
field ,
readOnly ,
className ,
} : {
field : RhfFormFields [ number ] ;
readOnly : boolean ;
className : string ;
} ) = > {
const { t } = useLocale ( ) ;
const { control , formState } = useFormContext ( ) ;
return (
2023-03-07 17:40:47 +00:00
< div data - fob - field - name = { field . name } className = { classNames ( className , field . hidden ? "hidden" : "" ) } >
2023-03-02 18:15:28 +00:00
< Controller
control = { control }
// Make it a variable
name = { ` responses. ${ field . name } ` }
2023-04-06 08:17:53 +00:00
render = { ( { field : { value , onChange } , fieldState : { error } } ) = > {
2023-03-02 18:15:28 +00:00
return (
< div >
< ComponentForField
field = { field }
value = { value }
readOnly = { readOnly }
setValue = { ( val : unknown ) = > {
onChange ( val ) ;
} }
/ >
< ErrorMessage
name = "responses"
errors = { formState . errors }
2023-04-06 08:17:53 +00:00
render = { ( { message } : { message : string | undefined } ) = > {
message = message || "" ;
// If the error comes due to parsing the `responses` object(which can have error for any field), we need to identify the field that has the error from the message
const name = message . replace ( /\{([^}]+)\}.*/ , "$1" ) ;
const isResponsesErrorForThisField = name === field . name ;
// If the error comes for the specific property of responses(Possible for system fields), then also we would go ahead and show the error
if ( ! isResponsesErrorForThisField && ! error ) {
2023-03-02 18:15:28 +00:00
return null ;
}
message = message . replace ( /\{[^}]+\}(.*)/ , "$1" ) . trim ( ) ;
if ( field . hidden ) {
console . error ( ` Error message for hidden field: ${ field . name } => ${ message } ` ) ;
}
return (
< div
2023-03-07 17:40:47 +00:00
data - testid = { ` error-message- ${ field . name } ` }
2023-03-02 18:15:28 +00:00
className = "mt-2 flex items-center text-sm text-red-700 " >
2023-04-12 15:26:31 +00:00
< Info className = "h-3 w-3 ltr:mr-2 rtl:ml-2" / >
2023-04-06 08:17:53 +00:00
< p > { t ( message || "invalid_input" ) } < / p >
2023-03-02 18:15:28 +00:00
< / div >
) ;
} }
/ >
< / div >
) ;
} }
/ >
< / div >
) ;
} ;