Files
cal.diy/packages/features/bookings/lib/getBookingFields.ts
T
c4104af8a2 feat: org-wide workflows (#15083)
* 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>
2024-07-08 17:44:11 +05:30

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);
};