2023-03-02 18:15:28 +00:00
import type { EventTypeCustomInput , EventType , Prisma , Workflow } from "@prisma/client" ;
2023-07-20 05:03:50 +00:00
import type { z } from "zod" ;
2023-03-02 18:15:28 +00:00
2023-08-16 18:03:21 +00:00
import { SMS_REMINDER_NUMBER_FIELD } from "@calcom/features/bookings/lib/SystemField" ;
2023-07-21 18:00:28 +00:00
import { fieldsThatSupportLabelAsSafeHtml } from "@calcom/features/form-builder/fieldsThatSupportLabelAsSafeHtml" ;
2023-08-21 17:11:47 +00:00
import { getFieldIdentifier } from "@calcom/features/form-builder/utils/getFieldIdentifier" ;
2023-07-21 18:00:28 +00:00
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML" ;
2023-03-02 18:15:28 +00:00
import slugify from "@calcom/lib/slugify" ;
2023-05-02 11:44:05 +00:00
import { EventTypeCustomInputType } from "@calcom/prisma/enums" ;
2023-03-02 18:15:28 +00:00
import {
2023-07-20 05:03:50 +00:00
BookingFieldTypeEnum ,
2023-03-02 18:15:28 +00:00
customInputSchema ,
eventTypeBookingFields ,
EventTypeMetaDataSchema ,
} from "@calcom/prisma/zod-utils" ;
2023-07-20 05:03:50 +00:00
type Fields = z . infer < typeof eventTypeBookingFields > ;
if ( typeof window !== "undefined" ) {
2023-08-16 18:03:21 +00:00
// This file imports some costly dependencies, so we want to make sure it's not imported on the client side.
throw new Error ( "`getBookingFields` must not be imported on the client side." ) ;
2023-07-20 05:03:50 +00:00
}
2023-03-22 04:40:49 +00:00
/ * *
* PHONE - > Phone
* /
function upperCaseToCamelCase ( upperCaseString : string ) {
return upperCaseString [ 0 ] . toUpperCase ( ) + upperCaseString . slice ( 1 ) . toLowerCase ( ) ;
}
2023-03-02 18:15:28 +00:00
export const getSmsReminderNumberField = ( ) = >
( {
name : SMS_REMINDER_NUMBER_FIELD ,
type : "phone" ,
2023-07-11 15:48:44 +00:00
defaultLabel : "number_text_notifications" ,
2023-03-02 18:15:28 +00:00
defaultPlaceholder : "enter_phone_number" ,
editable : "system" ,
} as const ) ;
export const getSmsReminderNumberSource = ( {
workflowId ,
isSmsReminderNumberRequired ,
} : {
workflowId : Workflow [ "id" ] ;
isSmsReminderNumberRequired : boolean ;
} ) = > ( {
id : "" + workflowId ,
type : "workflow" ,
label : "Workflow" ,
fieldRequired : isSmsReminderNumberRequired ,
editUrl : ` /workflows/ ${ workflowId } ` ,
} ) ;
/ * *
* This fn is the key to ensure on the fly mapping of customInputs to bookingFields and ensuring that all the systems fields are present and correctly ordered in bookingFields
* /
export const getBookingFieldsWithSystemFields = ( {
bookingFields ,
disableGuests ,
2023-07-27 08:52:46 +00:00
disableBookingTitle ,
2023-03-02 18:15:28 +00:00
customInputs ,
metadata ,
workflows ,
} : {
bookingFields : Fields | EventType [ "bookingFields" ] ;
disableGuests : boolean ;
2023-07-27 08:52:46 +00:00
disableBookingTitle? : boolean ;
2023-03-02 18:15:28 +00:00
customInputs : EventTypeCustomInput [ ] | z . infer < typeof customInputSchema > [ ] ;
metadata : EventType [ "metadata" ] | z . infer < typeof EventTypeMetaDataSchema > ;
workflows : Prisma.EventTypeGetPayload < {
select : {
workflows : {
select : {
workflow : {
select : {
id : true ;
steps : true ;
} ;
} ;
} ;
} ;
} ;
} > [ "workflows" ] ;
} ) = > {
const parsedMetaData = EventTypeMetaDataSchema . parse ( metadata || { } ) ;
const parsedBookingFields = eventTypeBookingFields . parse ( bookingFields || [ ] ) ;
const parsedCustomInputs = customInputSchema . array ( ) . parse ( customInputs || [ ] ) ;
workflows = workflows || [ ] ;
return ensureBookingInputsHaveSystemFields ( {
bookingFields : parsedBookingFields ,
disableGuests ,
2023-07-27 08:52:46 +00:00
disableBookingTitle ,
2023-03-02 18:15:28 +00:00
additionalNotesRequired : parsedMetaData?.additionalNotesRequired || false ,
customInputs : parsedCustomInputs ,
workflows ,
} ) ;
} ;
export const ensureBookingInputsHaveSystemFields = ( {
bookingFields ,
disableGuests ,
2023-07-27 08:52:46 +00:00
disableBookingTitle ,
2023-03-02 18:15:28 +00:00
additionalNotesRequired ,
customInputs ,
workflows ,
} : {
bookingFields : Fields ;
disableGuests : boolean ;
2023-07-27 08:52:46 +00:00
disableBookingTitle? : boolean ;
2023-03-02 18:15:28 +00:00
additionalNotesRequired : boolean ;
customInputs : z.infer < typeof customInputSchema > [ ] ;
workflows : Prisma.EventTypeGetPayload < {
select : {
workflows : {
select : {
workflow : {
select : {
id : true ;
steps : true ;
} ;
} ;
} ;
} ;
} ;
} > [ "workflows" ] ;
} ) = > {
// If bookingFields is set already, the migration is done.
2023-07-27 08:52:46 +00:00
const hideBookingTitle = disableBookingTitle ? ? true ;
2023-03-02 18:15:28 +00:00
const handleMigration = ! bookingFields . length ;
const CustomInputTypeToFieldType = {
2023-07-20 05:03:50 +00:00
[ EventTypeCustomInputType . TEXT ] : BookingFieldTypeEnum . text ,
[ EventTypeCustomInputType . TEXTLONG ] : BookingFieldTypeEnum . textarea ,
[ EventTypeCustomInputType . NUMBER ] : BookingFieldTypeEnum . number ,
[ EventTypeCustomInputType . BOOL ] : BookingFieldTypeEnum . boolean ,
[ EventTypeCustomInputType . RADIO ] : BookingFieldTypeEnum . radio ,
[ EventTypeCustomInputType . PHONE ] : BookingFieldTypeEnum . phone ,
2023-03-02 18:15:28 +00:00
} ;
const smsNumberSources = [ ] as NonNullable < ( typeof bookingFields ) [ number ] [ "sources" ] > ;
workflows . forEach ( ( workflow ) = > {
workflow . workflow . steps . forEach ( ( step ) = > {
2023-07-11 15:48:44 +00:00
if ( step . action === "SMS_ATTENDEE" || step . action === "WHATSAPP_ATTENDEE" ) {
2023-03-02 18:15:28 +00:00
const workflowId = workflow . workflow . id ;
smsNumberSources . push (
getSmsReminderNumberSource ( {
workflowId ,
isSmsReminderNumberRequired : ! ! step . numberRequired ,
} )
) ;
}
} ) ;
} ) ;
// These fields should be added before other user fields
const systemBeforeFields : typeof bookingFields = [
{
type : "name" ,
2023-07-20 05:03:50 +00:00
// This is the `name` of the main field
2023-03-02 18:15:28 +00:00
name : "name" ,
2023-03-21 10:47:17 +00:00
editable : "system" ,
2023-07-20 05:03:50 +00:00
// This Label is used in Email only as of now.
defaultLabel : "your_name" ,
2023-03-02 18:15:28 +00:00
required : true ,
sources : [
{
label : "Default" ,
id : "default" ,
type : "default" ,
} ,
] ,
} ,
{
defaultLabel : "email_address" ,
type : "email" ,
name : "email" ,
required : true ,
2023-03-21 10:47:17 +00:00
editable : "system" ,
2023-03-02 18:15:28 +00:00
sources : [
{
label : "Default" ,
id : "default" ,
type : "default" ,
} ,
] ,
} ,
{
defaultLabel : "location" ,
type : "radioInput" ,
name : "location" ,
2023-03-21 10:47:17 +00:00
editable : "system" ,
2023-04-06 08:17:53 +00:00
hideWhenJustOneOption : true ,
2023-03-02 18:15:28 +00:00
required : false ,
2023-04-06 08:17:53 +00:00
getOptionsAt : "locations" ,
2023-03-02 18:15:28 +00:00
optionsInputs : {
attendeeInPerson : {
type : "address" ,
required : true ,
placeholder : "" ,
} ,
phone : {
type : "phone" ,
required : true ,
placeholder : "" ,
} ,
} ,
sources : [
{
label : "Default" ,
id : "default" ,
type : "default" ,
} ,
] ,
} ,
] ;
// These fields should be added after other user fields
const systemAfterFields : typeof bookingFields = [
2023-07-27 08:52:46 +00:00
{
defaultLabel : "what_is_this_meeting_about" ,
type : "text" ,
name : "title" ,
editable : "system-but-optional" ,
required : true ,
hidden : hideBookingTitle ,
defaultPlaceholder : "" ,
sources : [
{
label : "Default" ,
id : "default" ,
type : "default" ,
} ,
] ,
} ,
2023-03-02 18:15:28 +00:00
{
defaultLabel : "additional_notes" ,
type : "textarea" ,
name : "notes" ,
2023-03-21 10:47:17 +00:00
editable : "system-but-optional" ,
2023-03-02 18:15:28 +00:00
required : additionalNotesRequired ,
defaultPlaceholder : "share_additional_notes" ,
sources : [
{
label : "Default" ,
id : "default" ,
type : "default" ,
} ,
] ,
} ,
{
defaultLabel : "additional_guests" ,
type : "multiemail" ,
2023-03-21 10:47:17 +00:00
editable : "system-but-optional" ,
2023-03-02 18:15:28 +00:00
name : "guests" ,
required : false ,
hidden : disableGuests ,
sources : [
{
label : "Default" ,
id : "default" ,
type : "default" ,
} ,
] ,
} ,
{
2023-06-08 13:37:54 +00:00
defaultLabel : "reason_for_reschedule" ,
2023-03-02 18:15:28 +00:00
type : "textarea" ,
2023-03-21 10:47:17 +00:00
editable : "system-but-optional" ,
2023-03-02 18:15:28 +00:00
name : "rescheduleReason" ,
defaultPlaceholder : "reschedule_placeholder" ,
required : false ,
2023-03-16 05:10:20 +00:00
views : [
{
id : "reschedule" ,
label : "Reschedule View" ,
} ,
] ,
2023-03-02 18:15:28 +00:00
sources : [
{
label : "Default" ,
id : "default" ,
type : "default" ,
} ,
] ,
} ,
] ;
const missingSystemBeforeFields = [ ] ;
for ( const field of systemBeforeFields ) {
2023-08-21 17:11:47 +00:00
const existingBookingFieldIndex = bookingFields . findIndex (
( f ) = > getFieldIdentifier ( f . name ) === getFieldIdentifier ( field . name )
) ;
2023-03-02 18:15:28 +00:00
// Only do a push, we must not update existing system fields as user could have modified any property in it,
2023-03-16 05:10:20 +00:00
if ( existingBookingFieldIndex === - 1 ) {
2023-03-02 18:15:28 +00:00
missingSystemBeforeFields . push ( field ) ;
2023-03-16 05:10:20 +00:00
} else {
// Adding the fields from Code first and then fields from DB. Allows, the code to push new properties to the field
bookingFields [ existingBookingFieldIndex ] = {
. . . field ,
. . . bookingFields [ existingBookingFieldIndex ] ,
} ;
2023-03-02 18:15:28 +00:00
}
}
bookingFields = missingSystemBeforeFields . concat ( bookingFields ) ;
2023-03-09 19:39:42 +00:00
// Backward Compatibility for SMS Reminder Number
// Note: We still need workflows in `getBookingFields` due to Backward Compatibility. If we do a one time entry for all event-types, we can remove workflows from `getBookingFields`
// Also, note that even if Workflows don't explicity add smsReminderNumber field to bookingFields, it would be added as a side effect of this backward compatibility logic
2023-08-21 17:11:47 +00:00
if (
smsNumberSources . length &&
! bookingFields . find ( ( f ) = > getFieldIdentifier ( f . name ) !== getFieldIdentifier ( SMS_REMINDER_NUMBER_FIELD ) )
) {
const indexForLocation = bookingFields . findIndex (
( f ) = > getFieldIdentifier ( f . name ) === getFieldIdentifier ( "location" )
) ;
2023-03-09 19:39:42 +00:00
// Add the SMS Reminder Number field after `location` field always
bookingFields . splice ( indexForLocation + 1 , 0 , {
. . . getSmsReminderNumberField ( ) ,
sources : smsNumberSources ,
} ) ;
}
2023-03-02 18:15:28 +00:00
// Backward Compatibility: If we are migrating from old system, we need to map `customInputs` to `bookingFields`
if ( handleMigration ) {
2023-03-21 18:44:18 +00:00
customInputs . forEach ( ( input , index ) = > {
2023-03-22 04:40:49 +00:00
const label = input . label || ` ${ upperCaseToCamelCase ( input . type ) } ` ;
2023-03-02 18:15:28 +00:00
bookingFields . push ( {
2023-03-22 04:40:49 +00:00
label : label ,
2023-03-02 18:15:28 +00:00
editable : "user" ,
// Custom Input's slugified label was being used as query param for prefilling. So, make that the name of the field
2023-03-21 18:44:18 +00:00
// Also Custom Input's label could have been empty string as well. But it's not possible to have empty name. So generate a name automatically.
name : slugify ( input . label || ` ${ input . type } - ${ index + 1 } ` ) ,
2023-03-02 18:15:28 +00:00
placeholder : input.placeholder ,
type : CustomInputTypeToFieldType [ input . type ] ,
required : input.required ,
options : input.options
? input . options . map ( ( o ) = > {
return {
. . . o ,
// Send the label as the value without any trimming or lowercase as this is what customInput are doing. It maintains backward compatibility
value : o.label ,
} ;
} )
: [ ] ,
} ) ;
} ) ;
}
const missingSystemAfterFields = [ ] ;
for ( const field of systemAfterFields ) {
2023-08-21 17:11:47 +00:00
const existingBookingFieldIndex = bookingFields . findIndex (
( f ) = > getFieldIdentifier ( f . name ) === getFieldIdentifier ( field . name )
) ;
2023-03-02 18:15:28 +00:00
// Only do a push, we must not update existing system fields as user could have modified any property in it,
2023-03-16 05:10:20 +00:00
if ( existingBookingFieldIndex === - 1 ) {
2023-03-02 18:15:28 +00:00
missingSystemAfterFields . push ( field ) ;
2023-03-16 05:10:20 +00:00
} else {
bookingFields [ existingBookingFieldIndex ] = {
// Adding the fields from Code first and then fields from DB. Allows, the code to push new properties to the field
. . . field ,
. . . bookingFields [ existingBookingFieldIndex ] ,
} ;
2023-03-02 18:15:28 +00:00
}
}
2023-07-21 18:00:28 +00:00
bookingFields = bookingFields . concat ( missingSystemAfterFields ) . map ( ( f ) = > {
return {
. . . f ,
// TODO: This has to be a FormBuilder feature and not be specific to bookingFields. Either use zod transform in FormBuilder to add labelAsSafeHtml automatically or add a getter for fields that would do this.
. . . ( fieldsThatSupportLabelAsSafeHtml . includes ( f . type )
? { labelAsSafeHtml : markdownToSafeHTML ( f . label || null ) || "" }
: null ) ,
} ;
} ) ;
2023-03-02 18:15:28 +00:00
return eventTypeBookingFields . brand < "HAS_SYSTEM_FIELDS" > ( ) . parse ( bookingFields ) ;
} ;