* add org to create workflow button * add save button for testing in dev mode * add select all to multi select dropdown * fix select all * pass isOrg to WorkflowDetailsPage * add checkbox to apply to all including future * fix count text on select * WIP schema * shows teams in dropdown * add team option to UI * [WIP] refactor for update handler * filter out "all" from activeOn * fix type error * create more helper functions * create reusable function for scheduling all booking reminders * finish update workflows for orgs (without testing) * fix isAuthorized check for orgs * fix getting newActiveOn * move all helper functions to util file * more code clean up * fix deleting reminders for org workflows * fix adding and editing steps * update form data when workflow is saved * code clean up * fixing creating/deleting reminders when user is member of several teams * fix setting activeOn for teams in useffect * add on delete cascade * make multiSelectSchenbox searchable * set activeOn correctly when checkbox true * WIP * fix type errors in MultiSelectCheckboxes * implemented scheduling org-wide workfow notifications (not yet tested) * fix type errors * type error * add missing changes from merge conflict * remote not needed include statement * fix type errors * code clean up + some todo comments * support org workflows for cancelled workflows * delete reminders from removed members * remove reminders if isActiveOnAll is turned off * fix unti test and type error * code clean up * create basis for testing to book org team event * create org workflow with active team * fix getting active org workflows on team + test setup * fix creating workflow step for tests * fix first org test * add test for user event type with org workflow active * use deleteAllWorkflowReminders everywhere * add test for deleteRemindersFromRemovedActiveOn * fix type errors * make all tests pass * fix type error * fix getSchedule test * code clean up * add missing import * fix type error * fix tests * code clean up * fix imports * update reminders when trigger was changed * check for teamId before userId in reminderScheduler * move getOrgIdFromMemberOrTeamId to different folder * code clean up * fix tests * test setup for scheduleBookingReminders * fix typo * add tests for scheduleBookingReminders * fix prisma default import * fix workflowStep type * add scheduleBookingReminders test for sms * return dummy sid for scheduleSMS testMode * clean up + fixes * add lost changes from merge * get teamId and userId from incoming evt object * removing not needed select * add org support for scheduleMandatory email reminder * add other teams to dropdown * move getAllWorkflows to seperate file and call it in parent function * include org wide workflows in createNewSeat * some fixes + code clean up * add new team to select text count when including future teams is checked * fix upsert and remove sms reminder field * correctly update activeOn if 'including future ...' is enabled * list active Org workflows in event workflows settings * fix sms reminder field in all handlers * add helper function to check if step was edited * fix active on badge on workflow * fix type error * fix double reminders * add teamId: null for userWorkflow query * fix activeOnAll with managed event types * add missing teamId in getAllWorkflows * add a dafaut to prisma param * fix managed event types on select all user workflows * code clean up * better variable name * small fixes in update handler * fix test name to match function name * add info badge * fix workflow count in event type settings * fix getting bookings from children manged event types * delete reminders when user is not part of any time no more * implement feedback * fix disbale workflow in event type settings * fix remove member * create new function getAllWorkflowsFromEventType * add some removed code * use promise.allSettled when deleting workflow reminders * create new function deleteWorkflowRemindersOfRemovedMember.ts * fix userId param * delete org worklfows when team is disbanded * don't trigger active on all workflow if not part of any team * fix active on count badge * add test for deleteWorkflowRemidnersOfRemovedMember * trigger workflow also if not member of any subteam * fix failing test * remove unused code * use testId for go back button * fixes for managed event types & activateEventTypeHandler * code clean up * don't activate workflow on locked managed event type * fix type error * type error * more type fixes * feedback --------- Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: Hariom Balhara <hariombalhara@gmail.com> Co-authored-by: Keith Williams <keithwillcode@gmail.com> Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
358 lines
11 KiB
TypeScript
358 lines
11 KiB
TypeScript
import type { EventTypeCustomInput, EventType } from "@prisma/client";
|
|
import type { z } from "zod";
|
|
|
|
import { SMS_REMINDER_NUMBER_FIELD } from "@calcom/features/bookings/lib/SystemField";
|
|
import type { Workflow } from "@calcom/features/ee/workflows/lib/types";
|
|
import { fieldsThatSupportLabelAsSafeHtml } from "@calcom/features/form-builder/fieldsThatSupportLabelAsSafeHtml";
|
|
import { getFieldIdentifier } from "@calcom/features/form-builder/utils/getFieldIdentifier";
|
|
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
|
import slugify from "@calcom/lib/slugify";
|
|
import { EventTypeCustomInputType } from "@calcom/prisma/enums";
|
|
import {
|
|
BookingFieldTypeEnum,
|
|
customInputSchema,
|
|
eventTypeBookingFields,
|
|
EventTypeMetaDataSchema,
|
|
} from "@calcom/prisma/zod-utils";
|
|
|
|
type Fields = z.infer<typeof eventTypeBookingFields>;
|
|
|
|
if (typeof window !== "undefined") {
|
|
// 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.");
|
|
}
|
|
|
|
/**
|
|
* PHONE -> Phone
|
|
*/
|
|
function upperCaseToCamelCase(upperCaseString: string) {
|
|
return upperCaseString[0].toUpperCase() + upperCaseString.slice(1).toLowerCase();
|
|
}
|
|
|
|
export const getSmsReminderNumberField = () =>
|
|
({
|
|
name: SMS_REMINDER_NUMBER_FIELD,
|
|
type: "phone",
|
|
defaultLabel: "number_text_notifications",
|
|
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,
|
|
disableBookingTitle,
|
|
customInputs,
|
|
metadata,
|
|
workflows,
|
|
}: {
|
|
bookingFields: Fields | EventType["bookingFields"];
|
|
disableGuests: boolean;
|
|
disableBookingTitle?: boolean;
|
|
customInputs: EventTypeCustomInput[] | z.infer<typeof customInputSchema>[];
|
|
metadata: EventType["metadata"] | z.infer<typeof EventTypeMetaDataSchema>;
|
|
workflows: {
|
|
workflow: Workflow;
|
|
}[];
|
|
}) => {
|
|
const parsedMetaData = EventTypeMetaDataSchema.parse(metadata || {});
|
|
const parsedBookingFields = eventTypeBookingFields.parse(bookingFields || []);
|
|
const parsedCustomInputs = customInputSchema.array().parse(customInputs || []);
|
|
workflows = workflows || [];
|
|
return ensureBookingInputsHaveSystemFields({
|
|
bookingFields: parsedBookingFields,
|
|
disableGuests,
|
|
disableBookingTitle,
|
|
additionalNotesRequired: parsedMetaData?.additionalNotesRequired || false,
|
|
customInputs: parsedCustomInputs,
|
|
workflows,
|
|
});
|
|
};
|
|
|
|
export const ensureBookingInputsHaveSystemFields = ({
|
|
bookingFields,
|
|
disableGuests,
|
|
disableBookingTitle,
|
|
additionalNotesRequired,
|
|
customInputs,
|
|
workflows,
|
|
}: {
|
|
bookingFields: Fields;
|
|
disableGuests: boolean;
|
|
disableBookingTitle?: boolean;
|
|
additionalNotesRequired: boolean;
|
|
customInputs: z.infer<typeof customInputSchema>[];
|
|
workflows: {
|
|
workflow: Workflow;
|
|
}[];
|
|
}) => {
|
|
// If bookingFields is set already, the migration is done.
|
|
const hideBookingTitle = disableBookingTitle ?? true;
|
|
const handleMigration = !bookingFields.length;
|
|
const CustomInputTypeToFieldType = {
|
|
[EventTypeCustomInputType.TEXT]: BookingFieldTypeEnum.text,
|
|
[EventTypeCustomInputType.TEXTLONG]: BookingFieldTypeEnum.textarea,
|
|
[EventTypeCustomInputType.NUMBER]: BookingFieldTypeEnum.number,
|
|
[EventTypeCustomInputType.BOOL]: BookingFieldTypeEnum.boolean,
|
|
[EventTypeCustomInputType.RADIO]: BookingFieldTypeEnum.radio,
|
|
[EventTypeCustomInputType.PHONE]: BookingFieldTypeEnum.phone,
|
|
};
|
|
|
|
const smsNumberSources = [] as NonNullable<(typeof bookingFields)[number]["sources"]>;
|
|
workflows.forEach((workflow) => {
|
|
workflow.workflow.steps.forEach((step) => {
|
|
if (step.action === "SMS_ATTENDEE" || step.action === "WHATSAPP_ATTENDEE") {
|
|
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",
|
|
// This is the `name` of the main field
|
|
name: "name",
|
|
editable: "system",
|
|
// This Label is used in Email only as of now.
|
|
defaultLabel: "your_name",
|
|
required: true,
|
|
sources: [
|
|
{
|
|
label: "Default",
|
|
id: "default",
|
|
type: "default",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
defaultLabel: "email_address",
|
|
type: "email",
|
|
name: "email",
|
|
required: true,
|
|
editable: "system",
|
|
sources: [
|
|
{
|
|
label: "Default",
|
|
id: "default",
|
|
type: "default",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
defaultLabel: "location",
|
|
type: "radioInput",
|
|
name: "location",
|
|
editable: "system",
|
|
hideWhenJustOneOption: true,
|
|
required: false,
|
|
getOptionsAt: "locations",
|
|
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 = [
|
|
{
|
|
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",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
defaultLabel: "additional_notes",
|
|
type: "textarea",
|
|
name: "notes",
|
|
editable: "system-but-optional",
|
|
required: additionalNotesRequired,
|
|
defaultPlaceholder: "share_additional_notes",
|
|
sources: [
|
|
{
|
|
label: "Default",
|
|
id: "default",
|
|
type: "default",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
defaultLabel: "additional_guests",
|
|
type: "multiemail",
|
|
editable: "system-but-optional",
|
|
name: "guests",
|
|
defaultPlaceholder: "email",
|
|
required: false,
|
|
hidden: disableGuests,
|
|
sources: [
|
|
{
|
|
label: "Default",
|
|
id: "default",
|
|
type: "default",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
defaultLabel: "reason_for_reschedule",
|
|
type: "textarea",
|
|
editable: "system-but-optional",
|
|
name: "rescheduleReason",
|
|
defaultPlaceholder: "reschedule_placeholder",
|
|
required: false,
|
|
views: [
|
|
{
|
|
id: "reschedule",
|
|
label: "Reschedule View",
|
|
},
|
|
],
|
|
sources: [
|
|
{
|
|
label: "Default",
|
|
id: "default",
|
|
type: "default",
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const missingSystemBeforeFields = [];
|
|
for (const field of systemBeforeFields) {
|
|
const existingBookingFieldIndex = bookingFields.findIndex(
|
|
(f) => getFieldIdentifier(f.name) === getFieldIdentifier(field.name)
|
|
);
|
|
// Only do a push, we must not update existing system fields as user could have modified any property in it,
|
|
if (existingBookingFieldIndex === -1) {
|
|
missingSystemBeforeFields.push(field);
|
|
} 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],
|
|
};
|
|
}
|
|
}
|
|
|
|
bookingFields = missingSystemBeforeFields.concat(bookingFields);
|
|
|
|
// 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
|
|
if (
|
|
smsNumberSources.length &&
|
|
!bookingFields.find((f) => getFieldIdentifier(f.name) !== getFieldIdentifier(SMS_REMINDER_NUMBER_FIELD))
|
|
) {
|
|
const indexForLocation = bookingFields.findIndex(
|
|
(f) => getFieldIdentifier(f.name) === getFieldIdentifier("location")
|
|
);
|
|
// Add the SMS Reminder Number field after `location` field always
|
|
bookingFields.splice(indexForLocation + 1, 0, {
|
|
...getSmsReminderNumberField(),
|
|
sources: smsNumberSources,
|
|
});
|
|
}
|
|
|
|
// Backward Compatibility: If we are migrating from old system, we need to map `customInputs` to `bookingFields`
|
|
if (handleMigration) {
|
|
customInputs.forEach((input, index) => {
|
|
const label = input.label || `${upperCaseToCamelCase(input.type)}`;
|
|
bookingFields.push({
|
|
label: label,
|
|
editable: "user",
|
|
// Custom Input's slugified label was being used as query param for prefilling. So, make that the name of the field
|
|
// 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}`),
|
|
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) {
|
|
const existingBookingFieldIndex = bookingFields.findIndex(
|
|
(f) => getFieldIdentifier(f.name) === getFieldIdentifier(field.name)
|
|
);
|
|
// Only do a push, we must not update existing system fields as user could have modified any property in it,
|
|
if (existingBookingFieldIndex === -1) {
|
|
missingSystemAfterFields.push(field);
|
|
} 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],
|
|
};
|
|
}
|
|
}
|
|
|
|
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),
|
|
};
|
|
});
|
|
|
|
return eventTypeBookingFields.brand<"HAS_SYSTEM_FIELDS">().parse(bookingFields);
|
|
};
|