* perf: move to disable prisma client extension inference * Prisma doesn't like it when you pass Record<string, unknown> * API v1 type fixes * Missed one * Fix unit test fail due to faulty expect * Assigning to prisma InputJsonValue/Array must be done as object, not interface * Fix @calcom/web ts error, teams not defined * Run eslint formatter * fixed the routingFormHelpers file causing a failing app store e2e test
571 lines
18 KiB
TypeScript
571 lines
18 KiB
TypeScript
import { uuid } from "short-uuid";
|
|
|
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
|
import { prisma } from "@calcom/prisma";
|
|
|
|
import { createAttributes } from "./organizationHelpers";
|
|
import { createRoundRobinTeamEventType } from "./teamHelpers";
|
|
|
|
/**
|
|
* Configuration for form fields
|
|
*/
|
|
interface FieldConfig {
|
|
id?: string;
|
|
type: "text" | "email" | "select" | "multiselect" | "number";
|
|
label: string;
|
|
identifier?: string;
|
|
required?: boolean;
|
|
options?: { id: string; label: string }[];
|
|
selectText?: string;
|
|
}
|
|
|
|
/**
|
|
* Configuration for form routes
|
|
*/
|
|
interface RouteConfig {
|
|
id?: string;
|
|
action: {
|
|
type: "eventTypeRedirectUrl" | "customPageMessage" | "externalRedirectUrl";
|
|
value: string;
|
|
};
|
|
isFallback?: boolean;
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
queryValue?: any;
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
attributesQueryValue?: any;
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
fallbackAttributesQueryValue?: any;
|
|
}
|
|
|
|
/**
|
|
* Configuration for team events in attribute routing
|
|
*/
|
|
interface TeamEventConfig {
|
|
title: string;
|
|
slug: string;
|
|
schedulingType?: "ROUND_ROBIN" | "COLLECTIVE";
|
|
assignAllTeamMembers?: boolean;
|
|
length?: number;
|
|
description?: string;
|
|
}
|
|
|
|
/**
|
|
* Configuration for creating test routing forms
|
|
*/
|
|
interface CreateRoutingFormConfig {
|
|
userId: number;
|
|
teamId: number;
|
|
/**
|
|
* Whether to setup with attribute routing or not
|
|
*/
|
|
formType: "default" | "attributeRouting";
|
|
|
|
// Basic form settings
|
|
form?: {
|
|
name?: string;
|
|
description?: string;
|
|
fields?: FieldConfig[];
|
|
routes?: RouteConfig[];
|
|
};
|
|
|
|
// For attribute routing (required when formType is "attributeRouting")
|
|
attributeRouting?: {
|
|
orgId?: number; // If not provided, will find from userId
|
|
attributes: {
|
|
name: string;
|
|
type: "SINGLE_SELECT" | "MULTI_SELECT" | "TEXT" | "NUMBER";
|
|
options?: string[];
|
|
}[];
|
|
assignments: {
|
|
memberIndex: number;
|
|
attributeValues: Record<string, string[]>;
|
|
}[];
|
|
teamEvents: TeamEventConfig[];
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Creates a test routing form with configurable options
|
|
*
|
|
* The routes are not configurable at the moment
|
|
* The hardcoded routes are:
|
|
* - Route-1: Skills:Javascript routes to team/team-javascript selecting only those team members who have JavaScript in their skills
|
|
* - Route-2: Skills:Sales routes to team/team-sales selecting only those team members who have Sales in their skills
|
|
* - Route-3: Fallback Message
|
|
*
|
|
* The hardcoded attributes are:
|
|
* - Location
|
|
* - Skills
|
|
* @param config - Configuration object containing all routing form settings
|
|
* @returns The created routing form
|
|
*/
|
|
export async function createRoutingForm(config: CreateRoutingFormConfig) {
|
|
const { userId, teamId, formType } = config;
|
|
|
|
if (formType === "attributeRouting") {
|
|
// Validate required config
|
|
if (!config.attributeRouting) {
|
|
throw new Error("attributeRouting config is required when formType is 'attributeRouting'");
|
|
}
|
|
|
|
// Find org membership
|
|
const orgMembership = await prisma.membership.findFirstOrThrow({
|
|
where: {
|
|
userId,
|
|
team: {
|
|
isOrganization: true,
|
|
},
|
|
},
|
|
});
|
|
if (!orgMembership) {
|
|
throw new Error("Organization membership not found");
|
|
}
|
|
|
|
const orgId = config.attributeRouting.orgId || orgMembership.teamId;
|
|
const team = await prisma.team.findUniqueOrThrow({
|
|
where: {
|
|
id: teamId,
|
|
},
|
|
});
|
|
|
|
// Use provided team events configuration
|
|
const teamEventsConfig = config.attributeRouting.teamEvents;
|
|
|
|
// Create team events
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const teamEvents: { [key: string]: any } = {};
|
|
for (const eventConfig of teamEventsConfig) {
|
|
const eventType = await createRoundRobinTeamEventType({
|
|
teamId,
|
|
eventType: { ...eventConfig, length: eventConfig.length || 60 },
|
|
});
|
|
teamEvents[eventConfig.slug] = eventType;
|
|
}
|
|
|
|
// Use provided attributes and assignments configuration
|
|
const attributes = await createAttributes({
|
|
orgId,
|
|
attributes: config.attributeRouting.attributes,
|
|
assignments: config.attributeRouting.assignments,
|
|
});
|
|
|
|
if (!attributes) {
|
|
throw new Error("Attributes not found");
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
const skillAttribute = attributes.find((attr) => attr.name === "Skills")!;
|
|
|
|
const javascriptEventRoute = {
|
|
id: "8a898988-89ab-4cde-b012-31823f708642",
|
|
value: `team/${team.slug}/team-javascript`,
|
|
};
|
|
|
|
const salesEventRoute = {
|
|
id: "8b2224b2-89ab-4cde-b012-31823f708642",
|
|
value: `team/${team.slug}/team-sales`,
|
|
};
|
|
|
|
const form = {
|
|
name: config.form?.name || "Form with Attribute Routing",
|
|
routes: [javascriptEventRoute, salesEventRoute],
|
|
formFieldLocation: {
|
|
id: "674c169a-e40a-492c-b4bb-6f5213873bd6",
|
|
},
|
|
formFieldSkills: {
|
|
id: "83316968-45bf-4c9d-b5d4-5368a8d2d2a8",
|
|
},
|
|
formFieldEmail: {
|
|
id: "dd28ffcf-7029-401e-bddb-ce2e7496a1c1",
|
|
},
|
|
formFieldName: {
|
|
id: "57734f65-8bbb-4065-9e71-fb7f0b7485f8",
|
|
},
|
|
formFieldRating: {
|
|
id: "f4e9fa6c-5c5d-4d8e-b15c-7f37e9d0c729",
|
|
},
|
|
};
|
|
|
|
const attributeRaw = {
|
|
location: {
|
|
id: "location-id",
|
|
options: [
|
|
{ id: "Location-London-uuid", value: "London" },
|
|
{ id: "Location-New-York-uuid", value: "New York" },
|
|
],
|
|
},
|
|
skills: {
|
|
id: "skills-id",
|
|
options: [
|
|
{ id: "Skills-JavaScript-uuid", value: "JavaScript" },
|
|
{ id: "Skills-Sales-uuid", value: "Sales" },
|
|
],
|
|
},
|
|
};
|
|
|
|
const formFieldSkillsOptions = attributeRaw.skills.options.map((opt) => ({
|
|
id: opt.id,
|
|
label: opt.value,
|
|
}));
|
|
|
|
const formFieldLocationOptions = attributeRaw.location.options.map((opt) => ({
|
|
id: opt.id,
|
|
label: opt.value,
|
|
}));
|
|
|
|
const createdForm = await prisma.app_RoutingForms_Form.create({
|
|
data: {
|
|
id: uuid(),
|
|
routes: [
|
|
{
|
|
id: javascriptEventRoute.id,
|
|
action: {
|
|
type: "eventTypeRedirectUrl",
|
|
value: javascriptEventRoute.value,
|
|
},
|
|
queryValue: {
|
|
id: "aaba9988-cdef-4012-b456-719300f53ef8",
|
|
type: "group",
|
|
children1: {
|
|
"b98b98a8-0123-4456-b89a-b19300f55277": {
|
|
type: "rule",
|
|
properties: {
|
|
field: form.formFieldSkills.id,
|
|
value: [
|
|
formFieldSkillsOptions.filter((opt) => opt.label === "JavaScript").map((opt) => opt.id),
|
|
],
|
|
operator: "multiselect_equals",
|
|
valueSrc: ["value"],
|
|
valueType: ["multiselect"],
|
|
valueError: [null],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
attributesQueryValue: {
|
|
id: "ab99bbb9-89ab-4cde-b012-319300f53ef8",
|
|
type: "group",
|
|
children1: {
|
|
"b98b98a8-0123-4456-b89a-b19300f55277": {
|
|
type: "rule",
|
|
properties: {
|
|
field: skillAttribute.id,
|
|
value: [
|
|
skillAttribute.options.filter((opt) => opt.value === "JavaScript").map((opt) => opt.id),
|
|
],
|
|
operator: "multiselect_some_in",
|
|
valueSrc: ["value"],
|
|
valueType: ["multiselect"],
|
|
valueError: [null],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
id: salesEventRoute.id,
|
|
action: {
|
|
type: "eventTypeRedirectUrl",
|
|
value: salesEventRoute.value,
|
|
},
|
|
queryValue: {
|
|
id: "aaba9948-cdef-4012-b456-719300f53ef8",
|
|
type: "group",
|
|
children1: {
|
|
"c98b98a8-1123-4456-e89a-a19300f55277": {
|
|
type: "rule",
|
|
properties: {
|
|
field: form.formFieldSkills.id,
|
|
value: [
|
|
formFieldSkillsOptions.filter((opt) => opt.label === "Sales").map((opt) => opt.id),
|
|
],
|
|
operator: "multiselect_equals",
|
|
valueSrc: ["value"],
|
|
valueType: ["multiselect"],
|
|
valueError: [null],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
attributesQueryValue: {
|
|
id: "ab988888-89ab-4cde-b012-319300f53ef8",
|
|
type: "group",
|
|
children1: {
|
|
"b98b98a12-0123-4456-b89a-b19300f55277": {
|
|
type: "rule",
|
|
properties: {
|
|
field: skillAttribute.id,
|
|
value: [
|
|
skillAttribute.options.filter((opt) => opt.value === "Sales").map((opt) => opt.id),
|
|
],
|
|
operator: "multiselect_some_in",
|
|
valueSrc: ["value"],
|
|
valueType: ["multiselect"],
|
|
valueError: [null],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
fallbackAttributesQueryValue: {
|
|
id: "a9888488-4567-489a-bcde-f19300f53ef8",
|
|
type: "group",
|
|
},
|
|
},
|
|
{
|
|
id: "148899aa-4567-489a-bcde-f1823f708646",
|
|
action: { type: "customPageMessage", value: "Fallback Message" },
|
|
isFallback: true,
|
|
queryValue: { id: "814899aa-4567-489a-bcde-f1823f708646", type: "group" },
|
|
},
|
|
],
|
|
fields: (
|
|
config.form?.fields ||
|
|
([
|
|
{
|
|
id: form.formFieldLocation.id,
|
|
type: "select",
|
|
label: "Location",
|
|
options: formFieldLocationOptions,
|
|
required: true,
|
|
},
|
|
{
|
|
id: form.formFieldSkills.id,
|
|
type: "multiselect",
|
|
label: "skills",
|
|
options: formFieldSkillsOptions,
|
|
required: true,
|
|
},
|
|
{
|
|
id: form.formFieldEmail.id,
|
|
type: "email",
|
|
label: "Email",
|
|
required: true,
|
|
},
|
|
{
|
|
id: form.formFieldName.id,
|
|
identifier: "name",
|
|
type: "text",
|
|
label: "Name",
|
|
required: false,
|
|
},
|
|
{
|
|
id: form.formFieldRating.id,
|
|
type: "number",
|
|
label: "Rating",
|
|
required: false,
|
|
},
|
|
] satisfies FieldConfig[])
|
|
).map((field) => ({ ...field })),
|
|
team: {
|
|
connect: {
|
|
id: teamId,
|
|
},
|
|
},
|
|
user: {
|
|
connect: {
|
|
id: userId,
|
|
},
|
|
},
|
|
name: form.name,
|
|
},
|
|
});
|
|
console.log(`🎯 Created form ${createdForm.id}`, JSON.stringify(createdForm, null, 2));
|
|
return createdForm;
|
|
} else {
|
|
// Default routing form
|
|
const multiSelectOption2Uuid = "d1302635-9f12-17b1-9153-c3a854649182";
|
|
const multiSelectOption1Uuid = "d1292635-9f12-17b1-9153-c3a854649182";
|
|
const selectOption1Uuid = "d0292635-9f12-17b1-9153-c3a854649182";
|
|
const selectOption2Uuid = "d0302635-9f12-17b1-9153-c3a854649182";
|
|
const multiSelectLegacyFieldUuid = "d4292635-9f12-17b1-9153-c3a854649182";
|
|
const multiSelectFieldUuid = "d9892635-9f12-17b1-9153-c3a854649182";
|
|
const selectFieldUuid = "d1302635-9f12-17b1-9153-c3a854649182";
|
|
const legacySelectFieldUuid = "f0292635-9f12-17b1-9153-c3a854649182";
|
|
|
|
const form = await prisma.app_RoutingForms_Form.create({
|
|
data: {
|
|
routes: (
|
|
config.form?.routes ||
|
|
([
|
|
{
|
|
id: "8a898988-89ab-4cde-b012-31823f708642",
|
|
action: { type: "eventTypeRedirectUrl", value: "pro/30min" },
|
|
queryValue: {
|
|
id: "8a898988-89ab-4cde-b012-31823f708642",
|
|
type: "group",
|
|
children1: {
|
|
"8988bbb8-0123-4456-b89a-b1823f70c5ff": {
|
|
type: "rule",
|
|
properties: {
|
|
field: "c4296635-9f12-47b1-8153-c3a854649182",
|
|
value: ["event-routing"],
|
|
operator: "equal",
|
|
valueSrc: ["value"],
|
|
valueType: ["text"],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
id: "aa8aaba9-cdef-4012-b456-71823f70f7ef",
|
|
action: { type: "customPageMessage", value: "Custom Page Result" },
|
|
queryValue: {
|
|
id: "aa8aaba9-cdef-4012-b456-71823f70f7ef",
|
|
type: "group",
|
|
children1: {
|
|
"b99b8a89-89ab-4cde-b012-31823f718ff5": {
|
|
type: "rule",
|
|
properties: {
|
|
field: "c4296635-9f12-47b1-8153-c3a854649182",
|
|
value: ["custom-page"],
|
|
operator: "equal",
|
|
valueSrc: ["value"],
|
|
valueType: ["text"],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
id: "a8ba9aab-4567-489a-bcde-f1823f71b4ad",
|
|
action: { type: "externalRedirectUrl", value: `${WEBAPP_URL}/pro` },
|
|
queryValue: {
|
|
id: "a8ba9aab-4567-489a-bcde-f1823f71b4ad",
|
|
type: "group",
|
|
children1: {
|
|
"998b9b9a-0123-4456-b89a-b1823f7232b9": {
|
|
type: "rule",
|
|
properties: {
|
|
field: "c4296635-9f12-47b1-8153-c3a854649182",
|
|
value: ["external-redirect"],
|
|
operator: "equal",
|
|
valueSrc: ["value"],
|
|
valueType: ["text"],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
id: "aa8ba8b9-0123-4456-b89a-b182623406d8",
|
|
action: { type: "customPageMessage", value: "Multiselect(Legacy) chosen" },
|
|
queryValue: {
|
|
id: "aa8ba8b9-0123-4456-b89a-b182623406d8",
|
|
type: "group",
|
|
children1: {
|
|
"b98a8abb-cdef-4012-b456-718262343d27": {
|
|
type: "rule",
|
|
properties: {
|
|
field: multiSelectLegacyFieldUuid,
|
|
value: [["Option-2"]],
|
|
operator: "multiselect_equals",
|
|
valueSrc: ["value"],
|
|
valueType: ["multiselect"],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
id: "bb9ea8b9-0123-4456-b89a-b182623406d8",
|
|
action: { type: "customPageMessage", value: "Multiselect chosen" },
|
|
queryValue: {
|
|
id: "aa8ba8b9-0123-4456-b89a-b182623406d8",
|
|
type: "group",
|
|
children1: {
|
|
"b98a8abb-cdef-4012-b456-718262343d27": {
|
|
type: "rule",
|
|
properties: {
|
|
field: multiSelectFieldUuid,
|
|
value: [[multiSelectOption2Uuid]],
|
|
operator: "multiselect_equals",
|
|
valueSrc: ["value"],
|
|
valueType: ["multiselect"],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
id: "898899aa-4567-489a-bcde-f1823f708646",
|
|
action: { type: "customPageMessage", value: "Fallback Message" },
|
|
isFallback: true,
|
|
queryValue: { id: "898899aa-4567-489a-bcde-f1823f708646", type: "group" },
|
|
},
|
|
] satisfies RouteConfig[])
|
|
).map((field) => ({ ...field })),
|
|
fields: (
|
|
config.form?.fields ||
|
|
([
|
|
{
|
|
id: "c4296635-9f12-47b1-8153-c3a854649182",
|
|
type: "text",
|
|
label: "Test field",
|
|
required: true,
|
|
},
|
|
{
|
|
id: multiSelectLegacyFieldUuid,
|
|
type: "multiselect",
|
|
label: "Multi Select(with Legacy `selectText`)",
|
|
identifier: "multi",
|
|
selectText: "Option-1\nOption-2",
|
|
required: false,
|
|
},
|
|
{
|
|
id: multiSelectFieldUuid,
|
|
type: "multiselect",
|
|
label: "Multi Select",
|
|
identifier: "multi-new-format",
|
|
options: [
|
|
{
|
|
id: multiSelectOption1Uuid,
|
|
label: "Option-1",
|
|
},
|
|
{
|
|
id: multiSelectOption2Uuid,
|
|
label: "Option-2",
|
|
},
|
|
],
|
|
required: false,
|
|
},
|
|
{
|
|
id: legacySelectFieldUuid,
|
|
type: "select",
|
|
label: "Legacy Select",
|
|
identifier: "test-select",
|
|
selectText: "Option-1\nOption-2",
|
|
required: false,
|
|
},
|
|
{
|
|
id: selectFieldUuid,
|
|
type: "select",
|
|
label: "Select",
|
|
identifier: "test-select-new-format",
|
|
options: [
|
|
{
|
|
id: selectOption1Uuid,
|
|
label: "Option-1",
|
|
},
|
|
{
|
|
id: selectOption2Uuid,
|
|
label: "Option-2",
|
|
},
|
|
],
|
|
required: false,
|
|
},
|
|
] satisfies FieldConfig[])
|
|
).map((field) => ({ ...field })),
|
|
user: {
|
|
connect: {
|
|
id: userId,
|
|
},
|
|
},
|
|
name: config.form?.name || "Fixture Routing Form",
|
|
},
|
|
});
|
|
|
|
return form;
|
|
}
|
|
}
|