Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2cfc63e6e3 | ||
|
|
4f66f3d2c6 | ||
|
|
ca3e7bd97d | ||
|
|
cafa3f9cf3 | ||
|
|
e04faffcbb | ||
|
|
6f1556a2fd | ||
|
|
d72a954896 | ||
|
|
abeb20373c | ||
|
|
422ef2f5b8 | ||
|
|
ec920eb2a8 | ||
|
|
795eecf300 | ||
|
|
8ae3d6d55b | ||
|
|
85070574da | ||
|
|
d1c2872f6e | ||
|
|
42d78f8099 | ||
|
|
f0d7a757d4 |
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
undecided:
|
||||
- "@calcom/prisma"
|
||||
@@ -61,6 +61,8 @@ describe("rateLimitApiKey middleware", () => {
|
||||
reset: Date.now(),
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
(checkRateLimitAndThrowError as any).mockImplementationOnce(({ onRateLimiterResponse }) => {
|
||||
onRateLimiterResponse(rateLimiterResponse);
|
||||
});
|
||||
|
||||
@@ -9,11 +9,25 @@ export const schemaSelectedCalendarBaseBodyParams = SelectedCalendar;
|
||||
|
||||
export const schemaSelectedCalendarPublic = SelectedCalendar.omit({});
|
||||
|
||||
export const schemaSelectedCalendarBodyParams = schemaSelectedCalendarBaseBodyParams.partial({
|
||||
userId: true,
|
||||
});
|
||||
export const schemaSelectedCalendarBodyParams = schemaSelectedCalendarBaseBodyParams
|
||||
.partial({
|
||||
userId: true,
|
||||
})
|
||||
.omit({
|
||||
// id will be set by the database
|
||||
id: true,
|
||||
// No eventTypeId support in API v1
|
||||
eventTypeId: true,
|
||||
});
|
||||
|
||||
export const schemaSelectedCalendarUpdateBodyParams = schemaSelectedCalendarBaseBodyParams.partial();
|
||||
export const schemaSelectedCalendarUpdateBodyParams = schemaSelectedCalendarBaseBodyParams
|
||||
.omit({
|
||||
// id is decided by DB
|
||||
id: true,
|
||||
// No eventTypeId support in API v1
|
||||
eventTypeId: true,
|
||||
})
|
||||
.partial();
|
||||
|
||||
export const selectedCalendarIdSchema = schemaQueryIdAsString.transform((v, ctx) => {
|
||||
/** We can assume the first part is the userId since it's an integer */
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
||||
"start": "PORT=3003 next start",
|
||||
"docker-start-api": "PORT=80 next start",
|
||||
"type-check": "tsc --pretty --noEmit"
|
||||
"type-check": "tsc --pretty --noEmit",
|
||||
"type-check:ci": "tsc-absolute --pretty --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/tsconfig": "*",
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import type { UserWithCalendars } from "@calcom/lib/getConnectedDestinationCalendars";
|
||||
import { getConnectedDestinationCalendars } from "@calcom/lib/getConnectedDestinationCalendars";
|
||||
import { getConnectedDestinationCalendarsAndEnsureDefaultsInDb } from "@calcom/lib/getConnectedDestinationCalendars";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { extractUserIdsFromQuery } from "~/lib/utils/extractUserIdsFromQuery";
|
||||
@@ -104,12 +105,8 @@ async function getHandler(req: NextApiRequest) {
|
||||
|
||||
const userIds = req.query.userId ? extractUserIdsFromQuery(req) : [userId];
|
||||
|
||||
const usersWithCalendars = await prisma.user.findMany({
|
||||
where: { id: { in: userIds } },
|
||||
include: {
|
||||
selectedCalendars: true,
|
||||
destinationCalendar: true,
|
||||
},
|
||||
const usersWithCalendars = await UserRepository.findManyByIdsIncludeDestinationAndSelectedCalendars({
|
||||
ids: userIds,
|
||||
});
|
||||
|
||||
return await getConnectedCalendars(usersWithCalendars);
|
||||
@@ -117,11 +114,12 @@ async function getHandler(req: NextApiRequest) {
|
||||
|
||||
async function getConnectedCalendars(users: UserWithCalendars[]) {
|
||||
const connectedDestinationCalendarsPromises = users.map((user) =>
|
||||
getConnectedDestinationCalendars(user, false, prisma).then((connectedCalendarsResult) =>
|
||||
connectedCalendarsResult.connectedCalendars.map((calendar) => ({
|
||||
userId: user.id,
|
||||
...calendar,
|
||||
}))
|
||||
getConnectedDestinationCalendarsAndEnsureDefaultsInDb({ user, onboarding: false, prisma }).then(
|
||||
(connectedCalendarsResult) =>
|
||||
connectedCalendarsResult.connectedCalendars.map((calendar) => ({
|
||||
userId: user.id,
|
||||
...calendar,
|
||||
}))
|
||||
)
|
||||
);
|
||||
const connectedDestinationCalendars = await Promise.all(connectedDestinationCalendarsPromises);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
|
||||
|
||||
import { selectedCalendarIdSchema } from "~/lib/validations/selected-calendar";
|
||||
|
||||
@@ -49,7 +49,9 @@ import { selectedCalendarIdSchema } from "~/lib/validations/selected-calendar";
|
||||
export async function deleteHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const userId_integration_externalId = selectedCalendarIdSchema.parse(query);
|
||||
await prisma.selectedCalendar.delete({ where: { userId_integration_externalId } });
|
||||
await SelectedCalendarRepository.deleteUserLevel({
|
||||
where: userId_integration_externalId,
|
||||
});
|
||||
return { message: `Selected Calendar with id: ${query.id} deleted successfully` };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
|
||||
|
||||
import { schemaSelectedCalendarPublic, selectedCalendarIdSchema } from "~/lib/validations/selected-calendar";
|
||||
|
||||
@@ -49,8 +49,8 @@ import { schemaSelectedCalendarPublic, selectedCalendarIdSchema } from "~/lib/va
|
||||
export async function getHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const userId_integration_externalId = selectedCalendarIdSchema.parse(query);
|
||||
const data = await prisma.selectedCalendar.findUniqueOrThrow({
|
||||
where: { userId_integration_externalId },
|
||||
const data = await SelectedCalendarRepository.findUserLevelUniqueOrThrow({
|
||||
where: userId_integration_externalId,
|
||||
});
|
||||
return { selected_calendar: schemaSelectedCalendarPublic.parse(data) };
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import type { UpdateArguments } from "@calcom/lib/server/repository/selectedCalendar";
|
||||
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import {
|
||||
@@ -56,7 +58,7 @@ export async function patchHandler(req: NextApiRequest) {
|
||||
const { query, isSystemWideAdmin } = req;
|
||||
const userId_integration_externalId = selectedCalendarIdSchema.parse(query);
|
||||
const { userId: bodyUserId, ...data } = schemaSelectedCalendarUpdateBodyParams.parse(req.body);
|
||||
const args: Prisma.SelectedCalendarUpdateArgs = { where: { userId_integration_externalId }, data };
|
||||
const args: UpdateArguments = { where: { ...userId_integration_externalId }, data };
|
||||
|
||||
if (!isSystemWideAdmin && bodyUserId)
|
||||
throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` });
|
||||
@@ -67,7 +69,7 @@ export async function patchHandler(req: NextApiRequest) {
|
||||
args.data.userId = bodyUserId;
|
||||
}
|
||||
|
||||
const result = await prisma.selectedCalendar.update(args);
|
||||
const result = await SelectedCalendarRepository.updateUserLevel(args);
|
||||
return { selected_calendar: schemaSelectedCalendarPublic.parse(result) };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { FindManyArgs } from "@calcom/lib/server/repository/selectedCalendar";
|
||||
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
|
||||
|
||||
import { schemaSelectedCalendarPublic } from "~/lib/validations/selected-calendar";
|
||||
import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId";
|
||||
@@ -34,7 +34,7 @@ import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/que
|
||||
async function getHandler(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
/* Admin gets all selected calendar by default, otherwise only the user's ones */
|
||||
const args: Prisma.SelectedCalendarFindManyArgs = isSystemWideAdmin ? {} : { where: { userId } };
|
||||
const args: FindManyArgs = isSystemWideAdmin ? {} : { where: { userId } };
|
||||
|
||||
/** Only admins can query other users */
|
||||
if (!isSystemWideAdmin && req.query.userId)
|
||||
@@ -46,7 +46,7 @@ async function getHandler(req: NextApiRequest) {
|
||||
if (Array.isArray(query.userId)) args.orderBy = { userId: "asc" };
|
||||
}
|
||||
|
||||
const data = await prisma.selectedCalendar.findMany(args);
|
||||
const data = await SelectedCalendarRepository.findManyUserLevel(args);
|
||||
return { selected_calendars: data.map((v) => schemaSelectedCalendarPublic.parse(v)) };
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import {
|
||||
@@ -52,7 +53,11 @@ import {
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { userId: bodyUserId, ...body } = schemaSelectedCalendarBodyParams.parse(req.body);
|
||||
const args: Prisma.SelectedCalendarCreateArgs = { data: { ...body, userId } };
|
||||
const args: {
|
||||
data: Prisma.SelectedCalendarUncheckedCreateInput;
|
||||
} = {
|
||||
data: { ...body, userId },
|
||||
};
|
||||
|
||||
if (!isSystemWideAdmin && bodyUserId)
|
||||
throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` });
|
||||
@@ -63,7 +68,7 @@ async function postHandler(req: NextApiRequest) {
|
||||
args.data.userId = bodyUserId;
|
||||
}
|
||||
|
||||
const data = await prisma.selectedCalendar.create(args);
|
||||
const data = await SelectedCalendarRepository.create(args.data);
|
||||
|
||||
return {
|
||||
selected_calendar: schemaSelectedCalendarPublic.parse(data),
|
||||
|
||||
@@ -52,6 +52,8 @@ describe("POST /api/attendees", () => {
|
||||
bookingId: 1,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
prismaMock.attendee.create.mockResolvedValue(attendeeData);
|
||||
req.isSystemWideAdmin = true;
|
||||
req.userId = 123;
|
||||
@@ -84,7 +86,8 @@ describe("POST /api/attendees", () => {
|
||||
timeZone: "UTC",
|
||||
bookingId: 1,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
prismaMock.attendee.create.mockResolvedValue(attendeeData);
|
||||
|
||||
req.userId = 123;
|
||||
|
||||
@@ -11,7 +11,7 @@ import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload";
|
||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
import { buildBooking, buildEventType, buildWebhook } from "@calcom/lib/test/builder";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { Booking } from "@calcom/prisma";
|
||||
import type { Booking } from "@calcom/prisma/client";
|
||||
|
||||
import handler from "../../../pages/api/bookings/_post";
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ describe("DELETE /api/event-types/[id]", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
// Mocking membership.findFirst
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
prismaMock.membership.findFirst.mockImplementation(({ where }) => {
|
||||
const { userId, teamId, accepted, role } = where;
|
||||
const mockData = [
|
||||
|
||||
@@ -72,6 +72,7 @@ describe("POST /api/event-types", () => {
|
||||
},
|
||||
});
|
||||
req.userId = memberUserId;
|
||||
// @ts-expect-error - Return type is wrong
|
||||
vi.mocked(canUserAccessTeamWithRole).mockImplementationOnce(async () => false);
|
||||
|
||||
await handler(req, res);
|
||||
@@ -114,6 +115,7 @@ describe("POST /api/event-types", () => {
|
||||
req.isSystemWideAdmin = true;
|
||||
req.userId = adminUserId;
|
||||
|
||||
// @ts-expect-error - Return type is wrong
|
||||
vi.mocked(canUserAccessTeamWithRole).mockImplementationOnce(async () => false);
|
||||
|
||||
await handler(req, res);
|
||||
@@ -138,6 +140,7 @@ describe("POST /api/event-types", () => {
|
||||
req.isSystemWideAdmin = true;
|
||||
req.userId = adminUserId;
|
||||
|
||||
// @ts-expect-error - Return type is wrong
|
||||
vi.mocked(canUserAccessTeamWithRole).mockImplementationOnce(async () => true);
|
||||
vi.mocked(checkParentEventOwnership).mockImplementationOnce(async () => undefined);
|
||||
vi.mocked(checkUserMembership).mockImplementationOnce(async () => undefined);
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { createMocks } from "node-mocks-http";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { ILicenseKeyService } from "@calcom/ee/common/server/LicenseKeyService";
|
||||
import LicenseKeyService from "@calcom/ee/common/server/LicenseKeyService";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { MembershipRole, UserPermissionRole } from "@calcom/prisma/enums";
|
||||
@@ -21,7 +22,7 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("Verify API key", () => {
|
||||
let service: LicenseKeyService;
|
||||
let service: ILicenseKeyService;
|
||||
|
||||
beforeEach(async () => {
|
||||
service = await LicenseKeyService.create();
|
||||
|
||||
@@ -79,6 +79,15 @@ describe("POST /api/selected-calendars", () => {
|
||||
integration: "google",
|
||||
externalId: "ext123",
|
||||
userId: 444444,
|
||||
id: "xxx-xxx",
|
||||
eventTypeId: null,
|
||||
domainWideDelegationCredentialId: null,
|
||||
googleChannelId: null,
|
||||
googleChannelKind: null,
|
||||
googleChannelResourceId: null,
|
||||
googleChannelResourceUri: null,
|
||||
googleChannelExpiration: null,
|
||||
error: null,
|
||||
});
|
||||
|
||||
await handler(req, res);
|
||||
@@ -104,10 +113,19 @@ describe("POST /api/selected-calendars", () => {
|
||||
req.userId = 333333;
|
||||
|
||||
prismaMock.selectedCalendar.create.mockResolvedValue({
|
||||
id: "xxx-xxx",
|
||||
credentialId: 1,
|
||||
integration: "google",
|
||||
externalId: "ext123",
|
||||
userId: 333333,
|
||||
googleChannelId: null,
|
||||
googleChannelKind: null,
|
||||
googleChannelResourceId: null,
|
||||
googleChannelResourceUri: null,
|
||||
googleChannelExpiration: null,
|
||||
domainWideDelegationCredentialId: null,
|
||||
eventTypeId: null,
|
||||
error: null,
|
||||
});
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.73",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.78",
|
||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
|
||||
@@ -19,7 +19,10 @@ import { DateTime } from "luxon";
|
||||
import { z } from "zod";
|
||||
|
||||
import { APPS_TYPE_ID_MAPPING } from "@calcom/platform-constants";
|
||||
import { getConnectedDestinationCalendars, getBusyCalendarTimes } from "@calcom/platform-libraries";
|
||||
import {
|
||||
getConnectedDestinationCalendarsAndEnsureDefaultsInDb,
|
||||
getBusyCalendarTimes,
|
||||
} from "@calcom/platform-libraries";
|
||||
import { Calendar } from "@calcom/platform-types";
|
||||
import { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
@@ -41,12 +44,18 @@ export class CalendarsService {
|
||||
if (!userWithCalendars) {
|
||||
throw new NotFoundException("User not found");
|
||||
}
|
||||
|
||||
return getConnectedDestinationCalendars(
|
||||
userWithCalendars,
|
||||
false,
|
||||
this.dbWrite.prisma as unknown as PrismaClient
|
||||
);
|
||||
return getConnectedDestinationCalendarsAndEnsureDefaultsInDb({
|
||||
user: {
|
||||
...userWithCalendars,
|
||||
allSelectedCalendars: userWithCalendars.selectedCalendars,
|
||||
userLevelSelectedCalendars: userWithCalendars.selectedCalendars.filter(
|
||||
(calendar) => !calendar.eventTypeId
|
||||
),
|
||||
},
|
||||
onboarding: false,
|
||||
eventTypeId: null,
|
||||
prisma: this.dbWrite.prisma as unknown as PrismaClient,
|
||||
});
|
||||
}
|
||||
|
||||
async getBusyTimes(
|
||||
|
||||
+80
-13
@@ -291,6 +291,7 @@ describe("Event types Endpoints", () => {
|
||||
placeholder: "select language",
|
||||
options: ["javascript", "python", "cobol"],
|
||||
disableOnPrefill: true,
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
scheduleId: firstSchedule.id,
|
||||
@@ -381,10 +382,24 @@ describe("Event types Endpoints", () => {
|
||||
const requestBookingFields = body.bookingFields || [];
|
||||
const expectedBookingFields = [
|
||||
{ isDefault: true, required: true, slug: "name", ...nameBookingField },
|
||||
{ isDefault: true, required: true, slug: "email", type: "email" },
|
||||
{ isDefault: true, required: true, slug: "email", type: "email", disableOnPrefill: false },
|
||||
// note(Lauris): location booking field is added if multiple locations are passed
|
||||
{ isDefault: true, required: false, slug: "location", type: "radioInput" },
|
||||
{ isDefault: true, required: false, slug: "rescheduleReason", type: "textarea" },
|
||||
{
|
||||
isDefault: true,
|
||||
required: false,
|
||||
slug: "location",
|
||||
type: "radioInput",
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
required: false,
|
||||
slug: "rescheduleReason",
|
||||
type: "textarea",
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
...requestBookingFields
|
||||
.filter((field) => field.type !== "name" && field.type !== "email")
|
||||
.map((field) => ({ isDefault: false, ...field })),
|
||||
@@ -787,6 +802,7 @@ describe("Event types Endpoints", () => {
|
||||
placeholder: "select language",
|
||||
options: ["javascript", "python", "cobol"],
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
bookingLimitsCount: {
|
||||
@@ -853,8 +869,15 @@ describe("Event types Endpoints", () => {
|
||||
const requestBookingFields = body.bookingFields || [];
|
||||
const expectedBookingFields = [
|
||||
{ isDefault: true, required: true, slug: "name", ...nameBookingField },
|
||||
{ isDefault: true, required: true, slug: "email", type: "email" },
|
||||
{ isDefault: true, required: false, slug: "rescheduleReason", type: "textarea" },
|
||||
{ isDefault: true, required: true, slug: "email", type: "email", disableOnPrefill: false },
|
||||
{
|
||||
isDefault: true,
|
||||
required: false,
|
||||
slug: "rescheduleReason",
|
||||
type: "textarea",
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
...requestBookingFields
|
||||
.filter((field) => field.type !== "name" && field.type !== "email")
|
||||
.map((field) => ({ isDefault: false, ...field })),
|
||||
@@ -1055,14 +1078,44 @@ describe("Event types Endpoints", () => {
|
||||
let legacyEventTypeId2: number;
|
||||
|
||||
const expectedReturnSystemFields = [
|
||||
{ isDefault: true, required: true, slug: "name", type: "name" },
|
||||
{ isDefault: true, required: true, slug: "email", type: "email" },
|
||||
{ isDefault: true, type: "radioInput", slug: "location", required: false },
|
||||
{ isDefault: true, required: true, slug: "title", type: "text" },
|
||||
{ isDefault: true, required: false, slug: "notes", type: "textarea" },
|
||||
{ isDefault: true, required: false, slug: "guests", type: "multiemail" },
|
||||
{ isDefault: true, required: false, slug: "rescheduleReason", type: "textarea" },
|
||||
{ isDefault: true, type: "phone", slug: "attendeePhoneNumber", required: false },
|
||||
{ isDefault: true, required: true, slug: "name", type: "name", disableOnPrefill: false },
|
||||
{ isDefault: true, required: true, slug: "email", type: "email", disableOnPrefill: false },
|
||||
{
|
||||
isDefault: true,
|
||||
type: "radioInput",
|
||||
slug: "location",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
label: "",
|
||||
placeholder: "",
|
||||
},
|
||||
{ isDefault: true, required: true, slug: "title", type: "text", disableOnPrefill: false, hidden: true },
|
||||
{
|
||||
isDefault: true,
|
||||
required: false,
|
||||
slug: "notes",
|
||||
type: "textarea",
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
required: false,
|
||||
slug: "guests",
|
||||
type: "multiemail",
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
required: false,
|
||||
slug: "rescheduleReason",
|
||||
type: "textarea",
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{ isDefault: true, type: "phone", slug: "attendeePhoneNumber", required: false, hidden: true },
|
||||
];
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -1382,6 +1435,8 @@ describe("Event types Endpoints", () => {
|
||||
label: userDefinedBookingField.label,
|
||||
required: userDefinedBookingField.required,
|
||||
placeholder: userDefinedBookingField.placeholder,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -1461,42 +1516,54 @@ describe("Event types Endpoints", () => {
|
||||
type: "name",
|
||||
slug: "name",
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "email",
|
||||
slug: "email",
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "radioInput",
|
||||
slug: "location",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "text",
|
||||
slug: "title",
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "textarea",
|
||||
slug: "notes",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "multiemail",
|
||||
slug: "guests",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "textarea",
|
||||
slug: "rescheduleReason",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
+18
-1
@@ -34,6 +34,7 @@ import {
|
||||
UpdateEventTypeInput_2024_06_14,
|
||||
GetEventTypesQuery_2024_06_14,
|
||||
CreateEventTypeInput_2024_06_14,
|
||||
EventTypeOutput_2024_06_14,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
@@ -113,10 +114,26 @@ export class EventTypesController_2024_06_14 {
|
||||
@Query() queryParams: GetEventTypesQuery_2024_06_14
|
||||
): Promise<GetEventTypesOutput_2024_06_14> {
|
||||
const eventTypes = await this.eventTypesService.getEventTypes(queryParams);
|
||||
const eventTypesFormatted = this.eventTypeResponseTransformPipe.transform(eventTypes);
|
||||
const eventTypesWithoutHiddenFields = eventTypesFormatted.map((eventType) => {
|
||||
return {
|
||||
...eventType,
|
||||
bookingFields: Array.isArray(eventType?.bookingFields)
|
||||
? eventType?.bookingFields
|
||||
.map((field) => {
|
||||
if ("hidden" in field) {
|
||||
return field.hidden !== true ? field : null;
|
||||
}
|
||||
return field;
|
||||
})
|
||||
.filter((f) => f)
|
||||
: [],
|
||||
};
|
||||
}) as EventTypeOutput_2024_06_14[];
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: this.eventTypeResponseTransformPipe.transform(eventTypes),
|
||||
data: eventTypesWithoutHiddenFields,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@ import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable, BadRequestException } from "@nestjs/common";
|
||||
|
||||
import {
|
||||
transformBookingFieldsApiToInternal,
|
||||
transformBookingFieldsApiRequestToInternal,
|
||||
transformLocationsApiToInternal,
|
||||
transformIntervalLimitsApiToInternal,
|
||||
transformFutureBookingLimitsApiToInternal,
|
||||
@@ -218,7 +218,7 @@ export class InputEventTypesService_2024_06_14 {
|
||||
hasMultipleLocations: boolean
|
||||
) {
|
||||
const customFields: (SystemField | CustomField)[] = inputBookingFields
|
||||
? transformBookingFieldsApiToInternal(inputBookingFields)
|
||||
? transformBookingFieldsApiRequestToInternal(inputBookingFields)
|
||||
: [];
|
||||
const customFieldsWithoutNameEmail = customFields.filter(
|
||||
(field) => field.type !== "name" && field.type !== "email"
|
||||
|
||||
+15
-2
@@ -682,48 +682,61 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
type: "name",
|
||||
slug: "name",
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "email",
|
||||
slug: "email",
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
required: false,
|
||||
slug: "attendeePhoneNumber",
|
||||
type: "phone",
|
||||
slug: "attendeePhoneNumber",
|
||||
required: false,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "radioInput",
|
||||
slug: "location",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "text",
|
||||
slug: "title",
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "textarea",
|
||||
slug: "notes",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "multiemail",
|
||||
slug: "guests",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "textarea",
|
||||
slug: "rescheduleReason",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -2,50 +2,62 @@ import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
// It ensures that we work on userLevel calendars only
|
||||
const ensureUserLevelWhere = {
|
||||
eventTypeId: null,
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class SelectedCalendarsRepository {
|
||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||
|
||||
upsertSelectedCalendar(externalId: string, credentialId: number, userId: number, integration: string) {
|
||||
return this.dbWrite.prisma.selectedCalendar.upsert({
|
||||
create: {
|
||||
userId,
|
||||
externalId,
|
||||
credentialId,
|
||||
integration,
|
||||
},
|
||||
update: {
|
||||
userId,
|
||||
externalId,
|
||||
credentialId,
|
||||
integration,
|
||||
},
|
||||
where: {
|
||||
userId_integration_externalId: {
|
||||
userId,
|
||||
integration,
|
||||
externalId,
|
||||
async upsertSelectedCalendar(
|
||||
externalId: string,
|
||||
credentialId: number,
|
||||
userId: number,
|
||||
integration: string
|
||||
) {
|
||||
// Why we can't use .upsert here, see server/repository/selectedCalendar.ts#upsert
|
||||
const existingUserSelectedCalendar = await this.getUserSelectedCalendar(userId, integration, externalId);
|
||||
const data = {
|
||||
userId,
|
||||
externalId,
|
||||
credentialId,
|
||||
integration,
|
||||
...ensureUserLevelWhere,
|
||||
};
|
||||
|
||||
if (existingUserSelectedCalendar) {
|
||||
return this.dbWrite.prisma.selectedCalendar.update({
|
||||
where: {
|
||||
id: existingUserSelectedCalendar.id,
|
||||
},
|
||||
},
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
return this.dbWrite.prisma.selectedCalendar.create({
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
getUserSelectedCalendars(userId: number) {
|
||||
// It would be unique result but we can't use .findUnique here because of eventTypeId being nullable
|
||||
return this.dbRead.prisma.selectedCalendar.findMany({
|
||||
where: {
|
||||
userId,
|
||||
...ensureUserLevelWhere,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getUserSelectedCalendar(userId: number, integration: string, externalId: string) {
|
||||
return this.dbRead.prisma.selectedCalendar.findUnique({
|
||||
return this.dbRead.prisma.selectedCalendar.findFirst({
|
||||
where: {
|
||||
userId_integration_externalId: {
|
||||
userId,
|
||||
externalId,
|
||||
integration,
|
||||
},
|
||||
userId,
|
||||
externalId,
|
||||
integration,
|
||||
...ensureUserLevelWhere,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -56,33 +68,46 @@ export class SelectedCalendarsRepository {
|
||||
externalId: string,
|
||||
credentialId: number
|
||||
) {
|
||||
return await this.dbWrite.prisma.selectedCalendar.upsert({
|
||||
where: {
|
||||
userId_integration_externalId: {
|
||||
userId,
|
||||
integration,
|
||||
externalId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
const existingUserSelectedCalendar = await this.getUserSelectedCalendar(userId, integration, externalId);
|
||||
|
||||
if (existingUserSelectedCalendar) {
|
||||
return;
|
||||
}
|
||||
|
||||
return await this.dbWrite.prisma.selectedCalendar.create({
|
||||
data: {
|
||||
userId,
|
||||
integration,
|
||||
externalId,
|
||||
credentialId,
|
||||
...ensureUserLevelWhere,
|
||||
},
|
||||
// already exists
|
||||
update: {},
|
||||
});
|
||||
}
|
||||
|
||||
async removeUserSelectedCalendar(userId: number, integration: string, externalId: string) {
|
||||
// Using deleteMany because userId_externalId_integration_eventTypeId is a unique constraint but with eventTypeId being nullable, causing it to be not used as a unique constraint
|
||||
const records = await this.dbWrite.prisma.selectedCalendar.findMany({
|
||||
where: {
|
||||
userId,
|
||||
externalId,
|
||||
integration,
|
||||
...ensureUserLevelWhere,
|
||||
},
|
||||
});
|
||||
|
||||
// Make the behaviour same as .delete which throws error if no record is found
|
||||
if (records.length === 0) {
|
||||
throw new Error("No SelectedCalendar found.");
|
||||
}
|
||||
|
||||
if (records.length > 1) {
|
||||
throw new Error("Multiple SelecteCalendars found. Skipping deletion");
|
||||
}
|
||||
|
||||
return await this.dbWrite.prisma.selectedCalendar.delete({
|
||||
where: {
|
||||
userId_integration_externalId: {
|
||||
userId,
|
||||
externalId,
|
||||
integration,
|
||||
},
|
||||
id: records[0].id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6998,6 +6998,10 @@
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `phone` and the URL contains query parameter `&phone=1234567890`, the phone field will be prefilled with this value and disabled."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -7005,7 +7009,8 @@
|
||||
"slug",
|
||||
"label",
|
||||
"required",
|
||||
"placeholder"
|
||||
"placeholder",
|
||||
"hidden"
|
||||
]
|
||||
},
|
||||
"AddressFieldInput_2024_06_14": {
|
||||
@@ -7035,6 +7040,10 @@
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `address` and the URL contains query parameter `&address=1234 Main St, London`, the address field will be prefilled with this value and disabled."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -7042,7 +7051,8 @@
|
||||
"slug",
|
||||
"label",
|
||||
"required",
|
||||
"placeholder"
|
||||
"placeholder",
|
||||
"hidden"
|
||||
]
|
||||
},
|
||||
"TextFieldInput_2024_06_14": {
|
||||
@@ -7072,6 +7082,10 @@
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `who-referred-you` and the URL contains query parameter `&who-referred-you=bob`, the text field will be prefilled with this value and disabled."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -7079,7 +7093,8 @@
|
||||
"slug",
|
||||
"label",
|
||||
"required",
|
||||
"placeholder"
|
||||
"placeholder",
|
||||
"hidden"
|
||||
]
|
||||
},
|
||||
"NumberFieldInput_2024_06_14": {
|
||||
@@ -7109,6 +7124,10 @@
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `calories-per-day` and the URL contains query parameter `&calories-per-day=3000`, the number field will be prefilled with this value and disabled."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -7116,7 +7135,8 @@
|
||||
"slug",
|
||||
"label",
|
||||
"required",
|
||||
"placeholder"
|
||||
"placeholder",
|
||||
"hidden"
|
||||
]
|
||||
},
|
||||
"TextAreaFieldInput_2024_06_14": {
|
||||
@@ -7146,6 +7166,10 @@
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `dear-diary` and the URL contains query parameter `&dear-diary=Today I shipped a feature`, the text area will be prefilled with this value and disabled."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -7153,7 +7177,8 @@
|
||||
"slug",
|
||||
"label",
|
||||
"required",
|
||||
"placeholder"
|
||||
"placeholder",
|
||||
"hidden"
|
||||
]
|
||||
},
|
||||
"SelectFieldInput_2024_06_14": {
|
||||
@@ -7193,6 +7218,10 @@
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `language` and options of this select field are ['english', 'italian'] and the URL contains query parameter `&language=italian`, the 'italian' will be selected and the select field will be disabled."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -7201,7 +7230,8 @@
|
||||
"label",
|
||||
"required",
|
||||
"placeholder",
|
||||
"options"
|
||||
"options",
|
||||
"hidden"
|
||||
]
|
||||
},
|
||||
"MultiSelectFieldInput_2024_06_14": {
|
||||
@@ -7237,6 +7267,10 @@
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `consultants` and the URL contains query parameter `&consultants=en&language=it`, the 'en' and 'it' will be selected and the select field will be disabled."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -7244,7 +7278,8 @@
|
||||
"slug",
|
||||
"label",
|
||||
"required",
|
||||
"options"
|
||||
"options",
|
||||
"hidden"
|
||||
]
|
||||
},
|
||||
"MultiEmailFieldInput_2024_06_14": {
|
||||
@@ -7274,6 +7309,10 @@
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `consultants` and the URL contains query parameter `&consultants=alice@gmail.com&consultants=bob@gmail.com`, the these emails will be added and none more can be added."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -7281,7 +7320,8 @@
|
||||
"slug",
|
||||
"label",
|
||||
"required",
|
||||
"placeholder"
|
||||
"placeholder",
|
||||
"hidden"
|
||||
]
|
||||
},
|
||||
"CheckboxGroupFieldInput_2024_06_14": {
|
||||
@@ -7317,6 +7357,10 @@
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `notify-me` and the URL contains query parameter `¬ify-me=true`, the checkbox will be selected and the checkbox field will be disabled."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -7324,7 +7368,8 @@
|
||||
"slug",
|
||||
"label",
|
||||
"required",
|
||||
"options"
|
||||
"options",
|
||||
"hidden"
|
||||
]
|
||||
},
|
||||
"RadioGroupFieldInput_2024_06_14": {
|
||||
@@ -7360,6 +7405,10 @@
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `language` and options of this select field are ['english', 'italian'] and the URL contains query parameter `&language=italian`, the 'italian' radio buttom will be selected and the select field will be disabled."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -7367,7 +7416,8 @@
|
||||
"slug",
|
||||
"label",
|
||||
"required",
|
||||
"options"
|
||||
"options",
|
||||
"hidden"
|
||||
]
|
||||
},
|
||||
"BooleanFieldInput_2024_06_14": {
|
||||
@@ -7392,13 +7442,18 @@
|
||||
},
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"slug",
|
||||
"label",
|
||||
"required"
|
||||
"required",
|
||||
"hidden"
|
||||
]
|
||||
},
|
||||
"BusinessDaysWindow_2024_06_14": {
|
||||
@@ -8403,13 +8458,18 @@
|
||||
},
|
||||
"required": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"isDefault",
|
||||
"slug",
|
||||
"type",
|
||||
"required"
|
||||
"required",
|
||||
"hidden"
|
||||
]
|
||||
},
|
||||
"RescheduleReasonDefaultFieldOutput_2024_06_14": {
|
||||
@@ -8431,13 +8491,29 @@
|
||||
},
|
||||
"required": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string"
|
||||
},
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `&rescheduleReason=busy`, the reschedule reason field will be prefilled with this value and disabled."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"isDefault",
|
||||
"slug",
|
||||
"type",
|
||||
"required"
|
||||
"required",
|
||||
"hidden",
|
||||
"disableOnPrefill"
|
||||
]
|
||||
},
|
||||
"TitleDefaultFieldOutput_2024_06_14": {
|
||||
@@ -8459,13 +8535,29 @@
|
||||
},
|
||||
"required": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string"
|
||||
},
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `&title=masterclass`, the title field will be prefilled with this value and disabled."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"isDefault",
|
||||
"slug",
|
||||
"type",
|
||||
"required"
|
||||
"required",
|
||||
"hidden",
|
||||
"disableOnPrefill"
|
||||
]
|
||||
},
|
||||
"NotesDefaultFieldOutput_2024_06_14": {
|
||||
@@ -8487,13 +8579,29 @@
|
||||
},
|
||||
"required": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string"
|
||||
},
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `¬es=hello`, the notes field will be prefilled with this value and disabled."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"isDefault",
|
||||
"slug",
|
||||
"type",
|
||||
"required"
|
||||
"required",
|
||||
"hidden",
|
||||
"disableOnPrefill"
|
||||
]
|
||||
},
|
||||
"GuestsDefaultFieldOutput_2024_06_14": {
|
||||
@@ -8515,13 +8623,29 @@
|
||||
},
|
||||
"required": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string"
|
||||
},
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `&guests=lauris@cal.com`, the guests field will be prefilled with this value and disabled."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"isDefault",
|
||||
"slug",
|
||||
"type",
|
||||
"required"
|
||||
"required",
|
||||
"hidden",
|
||||
"disableOnPrefill"
|
||||
]
|
||||
},
|
||||
"AddressFieldOutput_2024_06_14": {
|
||||
@@ -8567,6 +8691,10 @@
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `address` and the URL contains query parameter `&address=1234 Main St, London`, the address field will be prefilled with this value and disabled."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "object",
|
||||
"default": false,
|
||||
@@ -8579,6 +8707,7 @@
|
||||
"slug",
|
||||
"label",
|
||||
"required",
|
||||
"hidden",
|
||||
"isDefault"
|
||||
]
|
||||
},
|
||||
@@ -8620,6 +8749,10 @@
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "object",
|
||||
"default": false,
|
||||
@@ -8632,6 +8765,7 @@
|
||||
"slug",
|
||||
"label",
|
||||
"required",
|
||||
"hidden",
|
||||
"isDefault"
|
||||
]
|
||||
},
|
||||
@@ -8684,6 +8818,10 @@
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `notify-me` and the URL contains query parameter `¬ify-me=true`, the checkbox will be selected and the checkbox field will be disabled."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "object",
|
||||
"default": false,
|
||||
@@ -8697,6 +8835,7 @@
|
||||
"label",
|
||||
"required",
|
||||
"options",
|
||||
"hidden",
|
||||
"isDefault"
|
||||
]
|
||||
},
|
||||
@@ -8743,6 +8882,10 @@
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `consultants` and the URL contains query parameter `&consultants=alice@gmail.com&consultants=bob@gmail.com`, the these emails will be added and none more can be added."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "object",
|
||||
"default": false,
|
||||
@@ -8755,6 +8898,7 @@
|
||||
"slug",
|
||||
"label",
|
||||
"required",
|
||||
"hidden",
|
||||
"isDefault"
|
||||
]
|
||||
},
|
||||
@@ -8807,6 +8951,10 @@
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `consultants` and the URL contains query parameter `&consultants=en&language=it`, the 'en' and 'it' will be selected and the select field will be disabled."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "object",
|
||||
"default": false,
|
||||
@@ -8820,6 +8968,7 @@
|
||||
"label",
|
||||
"required",
|
||||
"options",
|
||||
"hidden",
|
||||
"isDefault"
|
||||
]
|
||||
},
|
||||
@@ -8866,6 +9015,10 @@
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `calories-per-day` and the URL contains query parameter `&calories-per-day=3000`, the number field will be prefilled with this value and disabled."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "object",
|
||||
"default": false,
|
||||
@@ -8878,6 +9031,7 @@
|
||||
"slug",
|
||||
"label",
|
||||
"required",
|
||||
"hidden",
|
||||
"isDefault"
|
||||
]
|
||||
},
|
||||
@@ -8922,6 +9076,10 @@
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `phone` and the URL contains query parameter `&phone=1234567890`, the phone field will be prefilled with this value and disabled."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "object",
|
||||
"default": false,
|
||||
@@ -8934,6 +9092,7 @@
|
||||
"slug",
|
||||
"label",
|
||||
"required",
|
||||
"hidden",
|
||||
"isDefault"
|
||||
]
|
||||
},
|
||||
@@ -8986,6 +9145,10 @@
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `language` and options of this select field are ['english', 'italian'] and the URL contains query parameter `&language=italian`, the 'italian' radio buttom will be selected and the select field will be disabled."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "object",
|
||||
"default": false,
|
||||
@@ -8999,6 +9162,7 @@
|
||||
"label",
|
||||
"required",
|
||||
"options",
|
||||
"hidden",
|
||||
"isDefault"
|
||||
]
|
||||
},
|
||||
@@ -9055,6 +9219,10 @@
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `language` and options of this select field are ['english', 'italian'] and the URL contains query parameter `&language=italian`, the 'italian' will be selected and the select field will be disabled."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "object",
|
||||
"default": false,
|
||||
@@ -9068,6 +9236,7 @@
|
||||
"label",
|
||||
"required",
|
||||
"options",
|
||||
"hidden",
|
||||
"isDefault"
|
||||
]
|
||||
},
|
||||
@@ -9114,6 +9283,10 @@
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `dear-diary` and the URL contains query parameter `&dear-diary=Today I shipped a feature`, the text area will be prefilled with this value and disabled."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "object",
|
||||
"default": false,
|
||||
@@ -9126,6 +9299,7 @@
|
||||
"slug",
|
||||
"label",
|
||||
"required",
|
||||
"hidden",
|
||||
"isDefault"
|
||||
]
|
||||
},
|
||||
@@ -9172,6 +9346,10 @@
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `who-referred-you` and the URL contains query parameter `&who-referred-you=bob`, the text field will be prefilled with this value and disabled."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "object",
|
||||
"default": false,
|
||||
@@ -9184,6 +9362,7 @@
|
||||
"slug",
|
||||
"label",
|
||||
"required",
|
||||
"hidden",
|
||||
"isDefault"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -1,24 +1,31 @@
|
||||
import type { GetStaticProps, GetStaticPropsContext } from "next";
|
||||
import { unstable_cache } from "next/cache";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
|
||||
export const withAppDirSsg =
|
||||
<T extends Record<string, any>>(getStaticProps: GetStaticProps<T>) =>
|
||||
<T extends Record<string, any>>(getStaticProps: GetStaticProps<T>, routePath: string) =>
|
||||
async (context: GetStaticPropsContext) => {
|
||||
const ssgResponse = await getStaticProps(context);
|
||||
const cacheKey = JSON.stringify({
|
||||
route: routePath,
|
||||
params: context.params || {},
|
||||
});
|
||||
|
||||
if ("redirect" in ssgResponse) {
|
||||
redirect(ssgResponse.redirect.destination);
|
||||
}
|
||||
const getCachedProps = unstable_cache(async () => {
|
||||
const ssgResponse = await getStaticProps(context);
|
||||
|
||||
if ("notFound" in ssgResponse) {
|
||||
notFound();
|
||||
}
|
||||
if ("redirect" in ssgResponse) {
|
||||
redirect(ssgResponse.redirect.destination);
|
||||
}
|
||||
|
||||
const props = await Promise.resolve(ssgResponse.props);
|
||||
if ("notFound" in ssgResponse) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return {
|
||||
...ssgResponse.props,
|
||||
// includes dehydratedState required for future page trpcPropvider
|
||||
...("trpcState" in props && { dehydratedState: props.trpcState }),
|
||||
};
|
||||
return {
|
||||
...ssgResponse.props,
|
||||
...("trpcState" in ssgResponse.props && { dehydratedState: ssgResponse.props.trpcState }),
|
||||
};
|
||||
}, [`ssg-${cacheKey}`]);
|
||||
|
||||
return getCachedProps();
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import type { PageProps } from "~/apps/[slug]/slug-view";
|
||||
import Page from "~/apps/[slug]/slug-view";
|
||||
|
||||
const getData = withAppDirSsg<PageProps>(getStaticProps);
|
||||
const getData = withAppDirSsg<PageProps>(getStaticProps, "future/apps/[slug]");
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
|
||||
const legacyContext = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
|
||||
@@ -28,7 +28,7 @@ export const generateStaticParams = async () => {
|
||||
return paths.map((category) => ({ category }));
|
||||
};
|
||||
|
||||
const getData = withAppDirSsg<PageProps>(getStaticProps);
|
||||
const getData = withAppDirSsg<PageProps>(getStaticProps, "future/apps/categories/[category]");
|
||||
|
||||
export default WithLayout({ getData, Page: CategoryPage, getLayout: null })<"P">;
|
||||
export const dynamic = "force-static";
|
||||
|
||||
@@ -13,7 +13,7 @@ export const generateMetadata = async () => {
|
||||
);
|
||||
};
|
||||
|
||||
const getData = withAppDirSsg(getStaticProps);
|
||||
const getData = withAppDirSsg(getStaticProps, "future/auth/error");
|
||||
|
||||
export default WithLayout({ getData, Page, getLayout: null })<"P">;
|
||||
export const dynamic = "force-static";
|
||||
|
||||
@@ -8,7 +8,7 @@ import Page from "~/bookings/views/bookings-listing-view";
|
||||
import { getStaticProps } from "~/bookings/views/bookings-listing-view.getStaticProps";
|
||||
|
||||
type Y = InferGetStaticPropsType<typeof getStaticProps>;
|
||||
const getData = withAppDirSsg<Y>(getStaticProps);
|
||||
const getData = withAppDirSsg<Y>(getStaticProps, "future/bookings/[status]");
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, Suspense } from "react";
|
||||
|
||||
import { InstallAppButton } from "@calcom/app-store/components";
|
||||
import {
|
||||
@@ -109,12 +109,14 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
||||
{heading && (
|
||||
<>
|
||||
<DestinationCalendarSettingsWebWrapper />
|
||||
<SelectedCalendarsSettingsWebWrapper
|
||||
onChanged={onChanged}
|
||||
fromOnboarding={fromOnboarding}
|
||||
destinationCalendarId={data.destinationCalendar?.externalId}
|
||||
isPending={mutation.isPending}
|
||||
/>
|
||||
<Suspense fallback={<SkeletonLoader />}>
|
||||
<SelectedCalendarsSettingsWebWrapper
|
||||
onChanged={onChanged}
|
||||
fromOnboarding={fromOnboarding}
|
||||
destinationCalendarId={data.destinationCalendar?.externalId}
|
||||
isPending={mutation.isPending}
|
||||
/>
|
||||
</Suspense>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CalendarSwitch } from "@calcom/features/calendars/CalendarSwitch";
|
||||
import { UserCalendarSwitch } from "@calcom/features/calendars/CalendarSwitch";
|
||||
|
||||
interface IConnectedCalendarItem {
|
||||
name: string;
|
||||
@@ -52,7 +52,7 @@ const ConnectedCalendarItem = (prop: IConnectedCalendarItem) => {
|
||||
<div>
|
||||
<ul className="p-4">
|
||||
{calendars?.map((calendar, i) => (
|
||||
<CalendarSwitch
|
||||
<UserCalendarSwitch
|
||||
credentialId={calendar.credentialId}
|
||||
key={calendar.externalId}
|
||||
externalId={calendar.externalId}
|
||||
|
||||
@@ -14,7 +14,10 @@ interface IConnectCalendarsProps {
|
||||
|
||||
const ConnectedCalendars = (props: IConnectCalendarsProps) => {
|
||||
const { nextStep } = props;
|
||||
const queryConnectedCalendars = trpc.viewer.connectedCalendars.useQuery({ onboarding: true });
|
||||
const queryConnectedCalendars = trpc.viewer.connectedCalendars.useQuery({
|
||||
onboarding: true,
|
||||
eventTypeId: null,
|
||||
});
|
||||
const { t } = useLocale();
|
||||
const queryIntegrations = trpc.viewer.integrations.useQuery({
|
||||
variant: "calendar",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "4.8.5",
|
||||
"version": "4.8.7",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
||||
@@ -14,6 +14,7 @@ const selectedCalendarSelectSchema = z.object({
|
||||
integration: z.string(),
|
||||
externalId: z.string(),
|
||||
credentialId: z.coerce.number(),
|
||||
eventTypeId: z.coerce.number().nullish(),
|
||||
});
|
||||
|
||||
/** Shared authentication middleware for GET, DELETE and POST requests */
|
||||
@@ -40,12 +41,13 @@ type CustomNextApiRequest = NextApiRequest & {
|
||||
async function postHandler(req: CustomNextApiRequest) {
|
||||
if (!req.userWithCredentials) throw new HttpError({ statusCode: 401, message: "Not authenticated" });
|
||||
const user = req.userWithCredentials;
|
||||
const { integration, externalId, credentialId } = selectedCalendarSelectSchema.parse(req.body);
|
||||
const { integration, externalId, credentialId, eventTypeId } = selectedCalendarSelectSchema.parse(req.body);
|
||||
await SelectedCalendarRepository.upsert({
|
||||
userId: user.id,
|
||||
integration,
|
||||
externalId,
|
||||
credentialId,
|
||||
eventTypeId: eventTypeId ?? null,
|
||||
});
|
||||
|
||||
return { message: "Calendar Selection Saved" };
|
||||
@@ -54,18 +56,27 @@ async function postHandler(req: CustomNextApiRequest) {
|
||||
async function deleteHandler(req: CustomNextApiRequest) {
|
||||
if (!req.userWithCredentials) throw new HttpError({ statusCode: 401, message: "Not authenticated" });
|
||||
const user = req.userWithCredentials;
|
||||
const { integration, externalId, credentialId } = selectedCalendarSelectSchema.parse(req.query);
|
||||
const { integration, externalId, credentialId, eventTypeId } = selectedCalendarSelectSchema.parse(
|
||||
req.query
|
||||
);
|
||||
const calendarCacheRepository = await CalendarCache.initFromCredentialId(credentialId);
|
||||
await calendarCacheRepository.unwatchCalendar({ calendarId: externalId });
|
||||
await calendarCacheRepository.unwatchCalendar({
|
||||
calendarId: externalId,
|
||||
eventTypeIds: [eventTypeId ?? null],
|
||||
});
|
||||
await SelectedCalendarRepository.delete({
|
||||
userId: user.id,
|
||||
externalId,
|
||||
integration,
|
||||
where: {
|
||||
userId: user.id,
|
||||
externalId,
|
||||
integration,
|
||||
eventTypeId: eventTypeId ?? null,
|
||||
},
|
||||
});
|
||||
|
||||
return { message: "Calendar Selection Saved" };
|
||||
}
|
||||
|
||||
// TODO: It doesn't seem to be used from within the app. It is possible that someone outside Cal.com is using this GET endpoint
|
||||
async function getHandler(req: CustomNextApiRequest) {
|
||||
if (!req.userWithCredentials) throw new HttpError({ statusCode: 401, message: "Not authenticated" });
|
||||
const user = req.userWithCredentials;
|
||||
@@ -76,7 +87,10 @@ async function getHandler(req: CustomNextApiRequest) {
|
||||
// get user's credentials + their connected integrations
|
||||
const calendarCredentials = getCalendarCredentials(user.credentials);
|
||||
// get all the connected integrations' calendars (from third party)
|
||||
const { connectedCalendars } = await getConnectedCalendars(calendarCredentials, user.selectedCalendars);
|
||||
const { connectedCalendars } = await getConnectedCalendars(
|
||||
calendarCredentials,
|
||||
user.userLevelSelectedCalendars
|
||||
);
|
||||
const calendars = connectedCalendars.flatMap((c) => c.calendars).filter(notEmpty);
|
||||
const selectableCalendars = calendars.map((cal) => {
|
||||
return { selected: selectedCalendarIds.findIndex((s) => s.externalId === cal.externalId) > -1, ...cal };
|
||||
|
||||
@@ -2874,6 +2874,8 @@
|
||||
"disable_org_url_description": "إعادة توجيه {{orgSlug}}.cal.com إلى {{destination}}",
|
||||
"single_select": "اختيار فردي",
|
||||
"multi_select": "اختيار متعدد",
|
||||
"calendar_conflict_settings": "إعدادات تعارض التقويم",
|
||||
"select_calendars_conflict_check": "حدد التقويمات التي تريد التحقق من تعارضاتها",
|
||||
"group_meeting": "اجتماع جماعي",
|
||||
"salesforce_ignore_guests": "عدم إنشاء سجلات جديدة للضيوف المضافين إلى الحجز",
|
||||
"google_meet": "Google Meet",
|
||||
|
||||
@@ -2874,6 +2874,8 @@
|
||||
"disable_org_url_description": "{{orgSlug}}.cal.com ünvanını {{destination}} ünvanına yönləndirir",
|
||||
"single_select": "Tək seçim",
|
||||
"multi_select": "Çoxlu seçim",
|
||||
"calendar_conflict_settings": "Təqvim münaqişəsi parametrləri",
|
||||
"select_calendars_conflict_check": "Münaqişələri yoxlamaq üçün təqvimləri seçin",
|
||||
"group_meeting": "Qrup görüşü",
|
||||
"salesforce_ignore_guests": "Rezervasiyaya əlavə edilən qonaqlar üçün yeni qeydlər yaratma",
|
||||
"google_meet": "Google Meet",
|
||||
|
||||
@@ -2874,6 +2874,8 @@
|
||||
"disable_org_url_description": "Пренасочва {{orgSlug}}.cal.com към {{destination}}",
|
||||
"single_select": "Единичен избор",
|
||||
"multi_select": "Множествен избор",
|
||||
"calendar_conflict_settings": "Настройки за конфликти в календара",
|
||||
"select_calendars_conflict_check": "Изберете кои календари да се проверяват за конфликти",
|
||||
"group_meeting": "Групова среща",
|
||||
"salesforce_ignore_guests": "Да не се създават нови записи за гости, добавени към резервацията",
|
||||
"google_meet": "Google Meet",
|
||||
|
||||
@@ -2874,6 +2874,8 @@
|
||||
"disable_org_url_description": "Redirigeix {{orgSlug}}.cal.com a {{destination}}",
|
||||
"single_select": "Selecció única",
|
||||
"multi_select": "Selecció múltiple",
|
||||
"calendar_conflict_settings": "Configuració de conflictes del calendari",
|
||||
"select_calendars_conflict_check": "Selecciona els calendaris per comprovar els conflictes",
|
||||
"group_meeting": "Reunió de grup",
|
||||
"salesforce_ignore_guests": "No crear nous registres per als convidats afegits a la reserva",
|
||||
"google_meet": "Google Meet",
|
||||
|
||||
@@ -2874,6 +2874,8 @@
|
||||
"disable_org_url_description": "Přesměruje {{orgSlug}}.cal.com na {{destination}}",
|
||||
"single_select": "Jeden výběr",
|
||||
"multi_select": "Více výběrů",
|
||||
"calendar_conflict_settings": "Nastavení konfliktů kalendáře",
|
||||
"select_calendars_conflict_check": "Vyberte kalendáře pro kontrolu konfliktů",
|
||||
"group_meeting": "Skupinová schůzka",
|
||||
"salesforce_ignore_guests": "Nevytvářet nové záznamy pro hosty přidané do rezervace",
|
||||
"google_meet": "Google Meet",
|
||||
|
||||
@@ -2874,6 +2874,8 @@
|
||||
"disable_org_url_description": "Omdirigerer {{orgSlug}}.cal.com til {{destination}}",
|
||||
"single_select": "Enkeltvalg",
|
||||
"multi_select": "Flervalg",
|
||||
"calendar_conflict_settings": "Indstillinger for kalenderkonflikt",
|
||||
"select_calendars_conflict_check": "Vælg hvilke kalendere der skal tjekkes for konflikter",
|
||||
"group_meeting": "Gruppemøde",
|
||||
"salesforce_ignore_guests": "Opret ikke nye poster for gæster tilføjet til bookingen",
|
||||
"google_meet": "Google Meet",
|
||||
|
||||
@@ -2874,6 +2874,8 @@
|
||||
"disable_org_url_description": "Leitet {{orgSlug}}.cal.com zu {{destination}} weiter",
|
||||
"single_select": "Einfachauswahl",
|
||||
"multi_select": "Mehrfachauswahl",
|
||||
"calendar_conflict_settings": "Kalender-Konflikt-Einstellungen",
|
||||
"select_calendars_conflict_check": "Wählen Sie die Kalender aus, die auf Konflikte überprüft werden sollen",
|
||||
"group_meeting": "Gruppenbesprechung",
|
||||
"salesforce_ignore_guests": "Keine neuen Datensätze für Gäste erstellen, die zur Buchung hinzugefügt wurden",
|
||||
"google_meet": "Google Meet",
|
||||
|
||||
@@ -2874,6 +2874,8 @@
|
||||
"disable_org_url_description": "Ανακατευθύνει το {{orgSlug}}.cal.com στο {{destination}}",
|
||||
"single_select": "Μονή επιλογή",
|
||||
"multi_select": "Πολλαπλή επιλογή",
|
||||
"calendar_conflict_settings": "Ρυθμίσεις διενέξεων ημερολογίου",
|
||||
"select_calendars_conflict_check": "Επιλέξτε ποια ημερολόγια θα ελέγχονται για διενέξεις",
|
||||
"group_meeting": "Ομαδική συνάντηση",
|
||||
"salesforce_ignore_guests": "Να μην δημιουργούνται νέες εγγραφές για καλεσμένους που προστίθενται στην κράτηση",
|
||||
"google_meet": "Google Meet",
|
||||
|
||||
@@ -2878,6 +2878,8 @@
|
||||
"disable_org_url_description": "Redirects {{orgSlug}}.cal.com to {{destination}}",
|
||||
"single_select": "Single Select",
|
||||
"multi_select": "Multi Select",
|
||||
"calendar_conflict_settings": "Calendar Conflict Settings",
|
||||
"select_calendars_conflict_check": "Select which calendars to check for conflicts",
|
||||
"group_meeting": "Group Meeting",
|
||||
"salesforce_ignore_guests": "Do not create new records for guests added to the booking",
|
||||
"google_meet": "Google Meet",
|
||||
|
||||
@@ -2874,6 +2874,8 @@
|
||||
"disable_org_url_description": "Redirecciona {{orgSlug}}.cal.com a {{destination}}",
|
||||
"single_select": "Selección única",
|
||||
"multi_select": "Selección múltiple",
|
||||
"calendar_conflict_settings": "Configuración de conflictos de calendario",
|
||||
"select_calendars_conflict_check": "Selecciona los calendarios para verificar conflictos",
|
||||
"group_meeting": "Reunión grupal",
|
||||
"salesforce_ignore_guests": "No crear nuevos registros para invitados agregados a la reserva",
|
||||
"google_meet": "Google Meet",
|
||||
|
||||
@@ -2874,6 +2874,8 @@
|
||||
"disable_org_url_description": "Redirecciona {{orgSlug}}.cal.com a {{destination}}",
|
||||
"single_select": "Selección única",
|
||||
"multi_select": "Selección múltiple",
|
||||
"calendar_conflict_settings": "Configuración de conflictos de calendario",
|
||||
"select_calendars_conflict_check": "Selecciona qué calendarios comprobar para detectar conflictos",
|
||||
"group_meeting": "Reunión grupal",
|
||||
"salesforce_ignore_guests": "No crear nuevos registros para invitados añadidos a la reserva",
|
||||
"google_meet": "Google Meet",
|
||||
|
||||
@@ -2874,6 +2874,8 @@
|
||||
"disable_org_url_description": "Suunab {{orgSlug}}.cal.com ümber aadressile {{destination}}",
|
||||
"single_select": "Üksikvalik",
|
||||
"multi_select": "Mitmikvalik",
|
||||
"calendar_conflict_settings": "Kalendri konfliktide seaded",
|
||||
"select_calendars_conflict_check": "Vali kalendrid, mida kontrollida konfliktide suhtes",
|
||||
"group_meeting": "Grupikohtumise",
|
||||
"salesforce_ignore_guests": "Ära loo broneeringule lisatud külaliste jaoks uusi kirjeid",
|
||||
"google_meet": "Google Meet",
|
||||
|
||||
@@ -2874,6 +2874,8 @@
|
||||
"disable_org_url_description": "{{orgSlug}}.cal.com {{destination}}-era birbideratzen du",
|
||||
"single_select": "Hautapen bakarra",
|
||||
"multi_select": "Hautapen anitza",
|
||||
"calendar_conflict_settings": "Egutegien arteko gatazken ezarpenak",
|
||||
"select_calendars_conflict_check": "Hautatu zein egutegi egiaztatu nahi dituzun gatazkarik dagoen ikusteko",
|
||||
"group_meeting": "Talde bilera",
|
||||
"salesforce_ignore_guests": "Ez sortu erregistro berririk erreserbara gehitutako gonbidatuentzat",
|
||||
"google_meet": "Google Meet",
|
||||
|
||||
@@ -2874,6 +2874,8 @@
|
||||
"disable_org_url_description": "Uudelleenohjaa {{orgSlug}}.cal.com osoitteeseen {{destination}}",
|
||||
"single_select": "Yksittäisvalinta",
|
||||
"multi_select": "Monivalinta",
|
||||
"calendar_conflict_settings": "Kalenterin päällekkäisyysasetukset",
|
||||
"select_calendars_conflict_check": "Valitse kalenterit, joista päällekkäisyydet tarkistetaan",
|
||||
"group_meeting": "Ryhmätapaaminen",
|
||||
"salesforce_ignore_guests": "Älä luo uusia tietueita varaukseen lisätyille vieraille",
|
||||
"google_meet": "Google Meet",
|
||||
|
||||
@@ -2874,6 +2874,8 @@
|
||||
"disable_org_url_description": "Redirige {{orgSlug}}.cal.com vers {{destination}}",
|
||||
"single_select": "Sélection unique",
|
||||
"multi_select": "Sélection multiple",
|
||||
"calendar_conflict_settings": "Paramètres de conflit de calendrier",
|
||||
"select_calendars_conflict_check": "Sélectionnez les calendriers à vérifier pour les conflits",
|
||||
"group_meeting": "Réunion de groupe",
|
||||
"salesforce_ignore_guests": "Ne pas créer de nouveaux enregistrements pour les invités ajoutés à la réservation",
|
||||
"google_meet": "Google Meet",
|
||||
|
||||
@@ -2874,6 +2874,8 @@
|
||||
"disable_org_url_description": "מפנה מחדש את {{orgSlug}}.cal.com אל {{destination}}",
|
||||
"single_select": "בחירה בודדת",
|
||||
"multi_select": "בחירה מרובה",
|
||||
"calendar_conflict_settings": "הגדרות התנגשות יומנים",
|
||||
"select_calendars_conflict_check": "בחר אילו יומנים לבדוק להתנגשויות",
|
||||
"group_meeting": "פגישה קבוצתית",
|
||||
"salesforce_ignore_guests": "אל תיצור רשומות חדשות עבור אורחים שנוספו להזמנה",
|
||||
"google_meet": "Google Meet",
|
||||
|
||||
@@ -2874,6 +2874,8 @@
|
||||
"disable_org_url_description": "Átirányítja a {{orgSlug}}.cal.com címet a {{destination}} címre",
|
||||
"single_select": "Egyszeres kiválasztás",
|
||||
"multi_select": "Többszörös kiválasztás",
|
||||
"calendar_conflict_settings": "Naptárütközési beállítások",
|
||||
"select_calendars_conflict_check": "Válaszd ki, mely naptárakat ellenőrizzük ütközések szempontjából",
|
||||
"group_meeting": "Csoportos megbeszélés",
|
||||
"salesforce_ignore_guests": "Ne hozzon létre új rekordokat a foglaláshoz hozzáadott vendégek számára",
|
||||
"google_meet": "Google Meet",
|
||||
|
||||
@@ -2874,6 +2874,8 @@
|
||||
"disable_org_url_description": "Reindirizza {{orgSlug}}.cal.com a {{destination}}",
|
||||
"single_select": "Selezione singola",
|
||||
"multi_select": "Selezione multipla",
|
||||
"calendar_conflict_settings": "Impostazioni conflitti calendario",
|
||||
"select_calendars_conflict_check": "Seleziona quali calendari controllare per i conflitti",
|
||||
"group_meeting": "Riunione di gruppo",
|
||||
"salesforce_ignore_guests": "Non creare nuovi record per gli ospiti aggiunti alla prenotazione",
|
||||
"google_meet": "Google Meet",
|
||||
|
||||
@@ -2874,6 +2874,8 @@
|
||||
"disable_org_url_description": "{{orgSlug}}.cal.comを{{destination}}にリダイレクトします",
|
||||
"single_select": "単一選択",
|
||||
"multi_select": "複数選択",
|
||||
"calendar_conflict_settings": "カレンダーの重複チェック設定",
|
||||
"select_calendars_conflict_check": "重複をチェックするカレンダーを選択",
|
||||
"group_meeting": "グループミーティング",
|
||||
"salesforce_ignore_guests": "予約に追加されたゲストの新規レコードを作成しない",
|
||||
"google_meet": "Google Meet",
|
||||
|
||||
@@ -2874,6 +2874,8 @@
|
||||
"disable_org_url_description": "បញ្ជូនបន្តពី {{orgSlug}}.cal.com ទៅកាន់ {{destination}}",
|
||||
"single_select": "ជ្រើសរើសតែមួយ",
|
||||
"multi_select": "ជ្រើសរើសច្រើន",
|
||||
"calendar_conflict_settings": "ការកំណត់ជម្លោះប្រតិទិន",
|
||||
"select_calendars_conflict_check": "ជ្រើសរើសប្រតិទិនណាមួយដើម្បីពិនិត្យមើលជម្លោះ",
|
||||
"group_meeting": "ការប្រជុំក្រុម",
|
||||
"salesforce_ignore_guests": "កុំបង្កើតកំណត់ត្រាថ្មីសម្រាប់ភ្ញៀវដែលបានបន្ថែមទៅក្នុងការកក់",
|
||||
"google_meet": "Google Meet",
|
||||
|
||||
@@ -2874,6 +2874,8 @@
|
||||
"disable_org_url_description": "{{orgSlug}}.cal.com을 {{destination}}(으)로 리디렉션합니다",
|
||||
"single_select": "단일 선택",
|
||||
"multi_select": "다중 선택",
|
||||
"calendar_conflict_settings": "캘린더 충돌 설정",
|
||||
"select_calendars_conflict_check": "일정 충돌을 확인할 캘린더 선택",
|
||||
"group_meeting": "그룹 미팅",
|
||||
"salesforce_ignore_guests": "예약에 추가된 게스트에 대한 새 레코드를 생성하지 않음",
|
||||
"google_meet": "Google Meet",
|
||||
|
||||
@@ -2874,6 +2874,8 @@
|
||||
"disable_org_url_description": "Verwijst {{orgSlug}}.cal.com door naar {{destination}}",
|
||||
"single_select": "Enkele selectie",
|
||||
"multi_select": "Meervoudige selectie",
|
||||
"calendar_conflict_settings": "Agenda-conflictinstellingen",
|
||||
"select_calendars_conflict_check": "Selecteer welke agenda's gecontroleerd moeten worden op conflicten",
|
||||
"group_meeting": "Groepsbijeenkomst",
|
||||
"salesforce_ignore_guests": "Maak geen nieuwe records aan voor gasten die aan de boeking zijn toegevoegd",
|
||||
"google_meet": "Google Meet",
|
||||
|
||||
@@ -2874,6 +2874,8 @@
|
||||
"disable_org_url_description": "Omdirigerer {{orgSlug}}.cal.com til {{destination}}",
|
||||
"single_select": "Enkeltvalg",
|
||||
"multi_select": "Flervalg",
|
||||
"calendar_conflict_settings": "Innstillinger for kalenderkonflikt",
|
||||
"select_calendars_conflict_check": "Velg hvilke kalendere som skal sjekkes for konflikter",
|
||||
"group_meeting": "Gruppemøte",
|
||||
"salesforce_ignore_guests": "Ikke opprett nye poster for gjester som legges til i bookingen",
|
||||
"google_meet": "Google Meet",
|
||||
|
||||
@@ -2874,6 +2874,8 @@
|
||||
"disable_org_url_description": "Przekierowuje {{orgSlug}}.cal.com na {{destination}}",
|
||||
"single_select": "Pojedynczy wybór",
|
||||
"multi_select": "Wielokrotny wybór",
|
||||
"calendar_conflict_settings": "Ustawienia konfliktów kalendarza",
|
||||
"select_calendars_conflict_check": "Wybierz kalendarze do sprawdzania konfliktów",
|
||||
"group_meeting": "Spotkanie grupowe",
|
||||
"salesforce_ignore_guests": "Nie twórz nowych rekordów dla gości dodanych do rezerwacji",
|
||||
"google_meet": "Google Meet",
|
||||
|
||||
@@ -577,7 +577,7 @@
|
||||
"forgotten_secret_description": "Caso tenha perdido ou esquecido o segredo, você poderá alterá-lo. Mas fique ciente de que todas as integrações com esse segredo precisarão ser atualizadas",
|
||||
"current_incorrect_password": "Senha atual está incorreta",
|
||||
"password_hint_caplow": "Mistura de letras maiúsculas e minúsculas",
|
||||
"password_hint_min": "Mínimo de oito caracteres",
|
||||
"password_hint_min": "Mínimo de sete caracteres",
|
||||
"password_hint_admin_min": "No mínimo 15 caracteres",
|
||||
"password_hint_num": "Contém pelo menos um número",
|
||||
"max_limit_allowed_hint": "O tamanho deve ser de {{limit}} ou menos caracteres",
|
||||
@@ -2874,6 +2874,8 @@
|
||||
"disable_org_url_description": "Redireciona {{orgSlug}}.cal.com para {{destination}}",
|
||||
"single_select": "Seleção única",
|
||||
"multi_select": "Seleção múltipla",
|
||||
"calendar_conflict_settings": "Configurações de conflito de agenda",
|
||||
"select_calendars_conflict_check": "Selecione quais agendas verificar para conflitos",
|
||||
"group_meeting": "Reunião em grupo",
|
||||
"salesforce_ignore_guests": "Não criar novos registros para convidados adicionados ao agendamento",
|
||||
"google_meet": "Google Meet",
|
||||
|
||||
@@ -2874,6 +2874,8 @@
|
||||
"disable_org_url_description": "Redireciona {{orgSlug}}.cal.com para {{destination}}",
|
||||
"single_select": "Seleção única",
|
||||
"multi_select": "Seleção múltipla",
|
||||
"calendar_conflict_settings": "Configurações de conflito de agenda",
|
||||
"select_calendars_conflict_check": "Selecione quais agendas verificar para conflitos",
|
||||
"group_meeting": "Reunião em grupo",
|
||||
"salesforce_ignore_guests": "Não criar novos registros para convidados adicionados ao agendamento",
|
||||
"google_meet": "Google Meet",
|
||||
|
||||
@@ -2874,6 +2874,8 @@
|
||||
"disable_org_url_description": "Redirecționează {{orgSlug}}.cal.com către {{destination}}",
|
||||
"single_select": "Selecție unică",
|
||||
"multi_select": "Selecție multiplă",
|
||||
"calendar_conflict_settings": "Setări pentru conflicte de calendar",
|
||||
"select_calendars_conflict_check": "Selectează calendarele pentru verificarea conflictelor",
|
||||
"group_meeting": "Întâlnire de grup",
|
||||
"salesforce_ignore_guests": "Nu crea înregistrări noi pentru invitații adăugați la programare",
|
||||
"google_meet": "Google Meet",
|
||||
|
||||
@@ -2874,6 +2874,8 @@
|
||||
"disable_org_url_description": "Перенаправляет {{orgSlug}}.cal.com на {{destination}}",
|
||||
"single_select": "Одиночный выбор",
|
||||
"multi_select": "Множественный выбор",
|
||||
"calendar_conflict_settings": "Настройки конфликтов календаря",
|
||||
"select_calendars_conflict_check": "Выберите календари для проверки конфликтов",
|
||||
"group_meeting": "Групповая встреча",
|
||||
"salesforce_ignore_guests": "Не создавать новые записи для гостей, добавленных в бронирование",
|
||||
"google_meet": "Google Meet",
|
||||
|
||||
@@ -2874,6 +2874,8 @@
|
||||
"disable_org_url_description": "Presmeruje {{orgSlug}}.cal.com na {{destination}}",
|
||||
"single_select": "Jednoduchý výber",
|
||||
"multi_select": "Viacnásobný výber",
|
||||
"calendar_conflict_settings": "Nastavenia konfliktov v kalendári",
|
||||
"select_calendars_conflict_check": "Vyberte kalendáre, ktoré sa majú kontrolovať na konflikty",
|
||||
"group_meeting": "Skupinové stretnutie",
|
||||
"salesforce_ignore_guests": "Nevytvárať nové záznamy pre hostí pridaných do rezervácie",
|
||||
"google_meet": "Google Meet",
|
||||
|
||||
@@ -2874,6 +2874,8 @@
|
||||
"disable_org_url_description": "Preusmerava {{orgSlug}}.cal.com na {{destination}}",
|
||||
"single_select": "Jednostruki izbor",
|
||||
"multi_select": "Višestruki izbor",
|
||||
"calendar_conflict_settings": "Podešavanja konflikta kalendara",
|
||||
"select_calendars_conflict_check": "Izaberite kalendare za proveru konflikata",
|
||||
"group_meeting": "Grupni sastanak",
|
||||
"salesforce_ignore_guests": "Ne kreiraj nove zapise za goste dodate u rezervaciju",
|
||||
"google_meet": "Google Meet",
|
||||
|
||||
@@ -2874,6 +2874,8 @@
|
||||
"disable_org_url_description": "Omdirigerar {{orgSlug}}.cal.com till {{destination}}",
|
||||
"single_select": "Enkelval",
|
||||
"multi_select": "Flerval",
|
||||
"calendar_conflict_settings": "Inställningar för kalenderkonflikt",
|
||||
"select_calendars_conflict_check": "Välj vilka kalendrar som ska kontrolleras för konflikter",
|
||||
"group_meeting": "Gruppmöte",
|
||||
"salesforce_ignore_guests": "Skapa inte nya poster för gäster som läggs till i bokningen",
|
||||
"google_meet": "Google Meet",
|
||||
|
||||
@@ -2874,6 +2874,8 @@
|
||||
"disable_org_url_description": "{{orgSlug}}.cal.com adresini {{destination}} adresine yönlendirir",
|
||||
"single_select": "Tekli Seçim",
|
||||
"multi_select": "Çoklu Seçim",
|
||||
"calendar_conflict_settings": "Takvim Çakışma Ayarları",
|
||||
"select_calendars_conflict_check": "Çakışmaları kontrol etmek için takvimleri seçin",
|
||||
"group_meeting": "Grup Toplantısı",
|
||||
"salesforce_ignore_guests": "Rezervasyona eklenen misafirler için yeni kayıt oluşturma",
|
||||
"google_meet": "Google Meet",
|
||||
|
||||
@@ -2874,6 +2874,8 @@
|
||||
"disable_org_url_description": "Перенаправляє {{orgSlug}}.cal.com на {{destination}}",
|
||||
"single_select": "Одиночний вибір",
|
||||
"multi_select": "Множинний вибір",
|
||||
"calendar_conflict_settings": "Налаштування конфліктів календаря",
|
||||
"select_calendars_conflict_check": "Оберіть календарі для перевірки конфліктів",
|
||||
"group_meeting": "Групова зустріч",
|
||||
"salesforce_ignore_guests": "Не створювати нові записи для гостей, доданих до бронювання",
|
||||
"google_meet": "Google Meet",
|
||||
|
||||
@@ -2874,6 +2874,8 @@
|
||||
"disable_org_url_description": "Chuyển hướng {{orgSlug}}.cal.com đến {{destination}}",
|
||||
"single_select": "Chọn một",
|
||||
"multi_select": "Chọn nhiều",
|
||||
"calendar_conflict_settings": "Cài đặt xung đột lịch",
|
||||
"select_calendars_conflict_check": "Chọn lịch cần kiểm tra xung đột",
|
||||
"group_meeting": "Cuộc họp nhóm",
|
||||
"salesforce_ignore_guests": "Không tạo bản ghi mới cho khách được thêm vào lịch hẹn",
|
||||
"google_meet": "Google Meet",
|
||||
|
||||
@@ -2874,6 +2874,8 @@
|
||||
"disable_org_url_description": "将 {{orgSlug}}.cal.com 重定向到 {{destination}}",
|
||||
"single_select": "单选",
|
||||
"multi_select": "多选",
|
||||
"calendar_conflict_settings": "日历冲突设置",
|
||||
"select_calendars_conflict_check": "选择需要检查冲突的日历",
|
||||
"group_meeting": "群组会议",
|
||||
"salesforce_ignore_guests": "不为预约中添加的访客创建新记录",
|
||||
"google_meet": "Google Meet",
|
||||
|
||||
@@ -2874,6 +2874,8 @@
|
||||
"disable_org_url_description": "將 {{orgSlug}}.cal.com 重新導向至 {{destination}}",
|
||||
"single_select": "單選",
|
||||
"multi_select": "多選",
|
||||
"calendar_conflict_settings": "行事曆衝突設定",
|
||||
"select_calendars_conflict_check": "選擇要檢查衝突的行事曆",
|
||||
"group_meeting": "群組會議",
|
||||
"salesforce_ignore_guests": "不為預約中新增的來賓建立新記錄",
|
||||
"google_meet": "Google Meet",
|
||||
|
||||
@@ -31,63 +31,6 @@ vi.mock("@calcom/lib/constants", () => ({
|
||||
|
||||
describe("getSchedule", () => {
|
||||
setupAndTeardown();
|
||||
describe("Calendar event", () => {
|
||||
test("correctly identifies unavailable slots from calendar", async () => {
|
||||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
||||
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
||||
|
||||
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([
|
||||
{
|
||||
start: `${plus2DateString}T04:45:00.000Z`,
|
||||
end: `${plus2DateString}T23:00:00.000Z`,
|
||||
},
|
||||
]);
|
||||
|
||||
const scenarioData = {
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slotInterval: 45,
|
||||
length: 45,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
users: [
|
||||
{
|
||||
...TestData.users.example,
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
},
|
||||
],
|
||||
apps: [TestData.apps["google-calendar"]],
|
||||
};
|
||||
// An event with one accepted booking
|
||||
await createBookingScenario(scenarioData);
|
||||
|
||||
const scheduleForDayWithAGoogleCalendarBooking = await getSchedule({
|
||||
input: {
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
orgSlug: null,
|
||||
},
|
||||
});
|
||||
|
||||
// As per Google Calendar Availability, only 4PM(4-4:45PM) GMT slot would be available
|
||||
expect(scheduleForDayWithAGoogleCalendarBooking).toHaveTimeSlots([`04:00:00.000Z`], {
|
||||
dateString: plus2DateString,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Move these inside describe('Team Event')
|
||||
describe("Round robin lead skip(i.e. use contact owner specified by teamMemberEmail) - CRM", async () => {
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
import {
|
||||
getDate,
|
||||
getGoogleCalendarCredential,
|
||||
createBookingScenario,
|
||||
Timezones,
|
||||
TestData,
|
||||
mockCalendar,
|
||||
} from "../../utils/bookingScenario/bookingScenario";
|
||||
|
||||
import { describe, test } from "vitest";
|
||||
|
||||
import { getAvailableSlots as getSchedule } from "@calcom/trpc/server/routers/viewer/slots/util";
|
||||
|
||||
import { expect, expectedSlotsForSchedule } from "./expects";
|
||||
import { setupAndTeardown } from "./setupAndTeardown";
|
||||
|
||||
describe("getSchedule", () => {
|
||||
setupAndTeardown();
|
||||
describe("Calendar event", () => {
|
||||
test("correctly identifies unavailable slots from selected calendars at user level", async () => {
|
||||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
||||
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
||||
const useEventLevelSelectedCalendars = false;
|
||||
|
||||
mockCalendar("googlecalendar", {
|
||||
create: {
|
||||
uid: "MOCK_ID",
|
||||
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
|
||||
},
|
||||
busySlots: [
|
||||
{
|
||||
start: `${plus2DateString}T04:45:00.000Z`,
|
||||
end: `${plus2DateString}T23:00:00.000Z`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const scenarioData = {
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
useEventLevelSelectedCalendars,
|
||||
slotInterval: 45,
|
||||
length: 45,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
users: [
|
||||
{
|
||||
...TestData.users.example,
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
},
|
||||
],
|
||||
apps: [TestData.apps["google-calendar"]],
|
||||
};
|
||||
// An event with one accepted booking
|
||||
await createBookingScenario(scenarioData);
|
||||
|
||||
const scheduleForDayWithAGoogleCalendarBooking = await getSchedule({
|
||||
input: {
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
orgSlug: null,
|
||||
},
|
||||
});
|
||||
|
||||
// As per Google Calendar Availability, only 4PM(4-4:45PM) GMT slot would be available
|
||||
expect(scheduleForDayWithAGoogleCalendarBooking).toHaveTimeSlots([`04:00:00.000Z`], {
|
||||
dateString: plus2DateString,
|
||||
});
|
||||
});
|
||||
|
||||
describe("useEventLevelSelectedCalendars is true", () => {
|
||||
test("correctly identifies unavailable slots from selected calendars at event level", async () => {
|
||||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
||||
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
||||
const useEventLevelSelectedCalendars = true;
|
||||
const eventTypeId = 1;
|
||||
mockCalendar("googlecalendar", {
|
||||
create: {
|
||||
uid: "MOCK_ID",
|
||||
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
|
||||
},
|
||||
busySlots: [
|
||||
{
|
||||
start: `${plus2DateString}T04:45:00.000Z`,
|
||||
end: `${plus2DateString}T23:00:00.000Z`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const scenarioData = {
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
useEventLevelSelectedCalendars,
|
||||
slotInterval: 45,
|
||||
length: 45,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
users: [
|
||||
{
|
||||
...TestData.users.example,
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [
|
||||
{
|
||||
...TestData.selectedCalendars.google,
|
||||
eventTypeId,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
apps: [TestData.apps["google-calendar"]],
|
||||
};
|
||||
// An event with one accepted booking
|
||||
await createBookingScenario(scenarioData);
|
||||
|
||||
const scheduleForDayWithAGoogleCalendarBooking = await getSchedule({
|
||||
input: {
|
||||
eventTypeId,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
orgSlug: null,
|
||||
},
|
||||
});
|
||||
|
||||
// As per Google Calendar Availability, only 4PM(4-4:45PM) GMT slot would be available
|
||||
expect(scheduleForDayWithAGoogleCalendarBooking).toHaveTimeSlots([`04:00:00.000Z`], {
|
||||
dateString: plus2DateString,
|
||||
});
|
||||
});
|
||||
|
||||
test("doesn't consider user level selected calendars", async () => {
|
||||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
||||
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
||||
const useEventLevelSelectedCalendars = true;
|
||||
const eventTypeId = 1;
|
||||
mockCalendar("googlecalendar", {
|
||||
create: {
|
||||
uid: "MOCK_ID",
|
||||
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
|
||||
},
|
||||
busySlots: [
|
||||
{
|
||||
start: `${plus2DateString}T04:45:00.000Z`,
|
||||
end: `${plus2DateString}T23:00:00.000Z`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const scenarioData = {
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
useEventLevelSelectedCalendars,
|
||||
slotInterval: 60,
|
||||
length: 60,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
users: [
|
||||
{
|
||||
...TestData.users.example,
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [
|
||||
{
|
||||
...TestData.selectedCalendars.google,
|
||||
eventTypeId: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
apps: [TestData.apps["google-calendar"]],
|
||||
};
|
||||
// An event with one accepted booking
|
||||
await createBookingScenario(scenarioData);
|
||||
|
||||
const scheduleForDayWithAGoogleCalendarBooking = await getSchedule({
|
||||
input: {
|
||||
eventTypeId,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
orgSlug: null,
|
||||
},
|
||||
});
|
||||
|
||||
// As per Google Calendar Availability, only 4PM(4-4:45PM) GMT slot would be available
|
||||
expect(scheduleForDayWithAGoogleCalendarBooking).toHaveTimeSlots(
|
||||
expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430,
|
||||
{
|
||||
dateString: plus2DateString,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("doesnt consider another event type's selected calendars", async () => {
|
||||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
||||
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
||||
const useEventLevelSelectedCalendars = true;
|
||||
const eventTypeId = 1;
|
||||
const anotherEventTypeId = 2;
|
||||
mockCalendar("googlecalendar", {
|
||||
create: {
|
||||
uid: "MOCK_ID",
|
||||
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
|
||||
},
|
||||
busySlots: [
|
||||
{
|
||||
start: `${plus2DateString}T04:45:00.000Z`,
|
||||
end: `${plus2DateString}T23:00:00.000Z`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const scenarioData = {
|
||||
eventTypes: [
|
||||
{
|
||||
id: eventTypeId,
|
||||
useEventLevelSelectedCalendars,
|
||||
slotInterval: 60,
|
||||
length: 60,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
users: [
|
||||
{
|
||||
...TestData.users.example,
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [
|
||||
{
|
||||
...TestData.selectedCalendars.google,
|
||||
eventTypeId: anotherEventTypeId,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
apps: [TestData.apps["google-calendar"]],
|
||||
};
|
||||
// An event with one accepted booking
|
||||
await createBookingScenario(scenarioData);
|
||||
|
||||
const scheduleForDayWithAGoogleCalendarBooking = await getSchedule({
|
||||
input: {
|
||||
eventTypeId,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
orgSlug: null,
|
||||
},
|
||||
});
|
||||
|
||||
// As per Google Calendar Availability, only 4PM(4-4:45PM) GMT slot would be available
|
||||
expect(scheduleForDayWithAGoogleCalendarBooking).toHaveTimeSlots(
|
||||
expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430,
|
||||
{
|
||||
dateString: plus2DateString,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -190,6 +190,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
data: {
|
||||
...rest,
|
||||
assignRRMembersUsingSegment: undefined,
|
||||
useEventLevelSelectedCalendars: undefined,
|
||||
rrSegmentQueryValue: undefined,
|
||||
locations: [],
|
||||
scheduleId: null,
|
||||
@@ -350,6 +351,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
data: {
|
||||
...rest,
|
||||
assignRRMembersUsingSegment: undefined,
|
||||
useEventLevelSelectedCalendars: undefined,
|
||||
rrSegmentQueryValue: undefined,
|
||||
locations: [],
|
||||
hashedLink: {
|
||||
@@ -440,6 +442,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
hashedLink: undefined,
|
||||
rrSegmentQueryValue: undefined,
|
||||
assignRRMembersUsingSegment: false,
|
||||
useEventLevelSelectedCalendars: false,
|
||||
},
|
||||
});
|
||||
const { profileId, ...rest } = evType;
|
||||
@@ -449,6 +452,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
...rest,
|
||||
locations: [],
|
||||
assignRRMembersUsingSegment: undefined,
|
||||
useEventLevelSelectedCalendars: undefined,
|
||||
rrSegmentQueryValue: undefined,
|
||||
lockTimeZoneToggleOnBookingPage: false,
|
||||
requiresBookerEmailVerification: false,
|
||||
|
||||
@@ -114,7 +114,9 @@ type InputCredential = typeof TestData.credentials.google & {
|
||||
id?: number;
|
||||
};
|
||||
|
||||
type InputSelectedCalendar = typeof TestData.selectedCalendars.google;
|
||||
type InputSelectedCalendar = (typeof TestData.selectedCalendars)[keyof typeof TestData.selectedCalendars] & {
|
||||
eventTypeId?: number | null;
|
||||
};
|
||||
|
||||
type InputUser = Omit<typeof TestData.users.example, "defaultScheduleId"> & {
|
||||
id: number;
|
||||
@@ -158,6 +160,7 @@ export type InputEventType = {
|
||||
slotInterval?: number;
|
||||
userId?: number;
|
||||
minimumBookingNotice?: number;
|
||||
useEventLevelSelectedCalendars?: boolean;
|
||||
/**
|
||||
* These user ids are `ScenarioData["users"]["id"]`
|
||||
*/
|
||||
@@ -1248,6 +1251,7 @@ export function getOrganizer({
|
||||
metadata,
|
||||
smsLockState,
|
||||
completedOnboarding,
|
||||
username,
|
||||
}: {
|
||||
name: string;
|
||||
email: string;
|
||||
@@ -1263,10 +1267,13 @@ export function getOrganizer({
|
||||
metadata?: userMetadataType;
|
||||
smsLockState?: SMSLockState;
|
||||
completedOnboarding?: boolean;
|
||||
username?: string;
|
||||
}) {
|
||||
username = username ?? TestData.users.example.username;
|
||||
return {
|
||||
...TestData.users.example,
|
||||
name,
|
||||
username,
|
||||
email,
|
||||
id,
|
||||
schedules,
|
||||
@@ -1293,13 +1300,15 @@ export function getScenarioData(
|
||||
eventTypes,
|
||||
usersApartFromOrganizer = [],
|
||||
apps = [],
|
||||
users: _users,
|
||||
webhooks,
|
||||
workflows,
|
||||
bookings,
|
||||
}: {
|
||||
organizer: ReturnType<typeof getOrganizer>;
|
||||
organizer?: ReturnType<typeof getOrganizer>;
|
||||
eventTypes: ScenarioData["eventTypes"];
|
||||
apps?: ScenarioData["apps"];
|
||||
users?: ScenarioData["users"];
|
||||
usersApartFromOrganizer?: ScenarioData["users"];
|
||||
webhooks?: ScenarioData["webhooks"];
|
||||
workflows?: ScenarioData["workflows"];
|
||||
@@ -1307,7 +1316,13 @@ export function getScenarioData(
|
||||
},
|
||||
org?: { id: number | null } | undefined | null
|
||||
) {
|
||||
const users = [organizer, ...usersApartFromOrganizer];
|
||||
if (_users && (usersApartFromOrganizer.length || organizer)) {
|
||||
throw new Error("When users are provided, usersApartFromOrganizer and organizer should not be provided");
|
||||
}
|
||||
const users = _users ? _users : organizer ? [organizer, ...usersApartFromOrganizer] : [];
|
||||
if (!users.length) {
|
||||
throw new Error("No users are specified in any way");
|
||||
}
|
||||
if (org) {
|
||||
const orgId = org.id;
|
||||
if (!orgId) {
|
||||
|
||||
@@ -16,27 +16,46 @@ export function getBasicMockRequestDataForBooking() {
|
||||
hashedLink: null,
|
||||
};
|
||||
}
|
||||
|
||||
type CommonPropsMockRequestData = {
|
||||
rescheduleUid?: string;
|
||||
bookingUid?: string;
|
||||
recurringEventId?: string;
|
||||
recurringCount?: number;
|
||||
rescheduledBy?: string;
|
||||
cancelledBy?: string;
|
||||
schedulingType?: SchedulingType;
|
||||
responses: {
|
||||
email: string;
|
||||
name: string;
|
||||
location?: { optionValue: ""; value: string };
|
||||
attendeePhoneNumber?: string;
|
||||
smsReminderNumber?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export function getMockRequestDataForBooking({
|
||||
data,
|
||||
}: {
|
||||
data: Partial<ReturnType<typeof getBasicMockRequestDataForBooking>> & {
|
||||
eventTypeId: number;
|
||||
user?: string;
|
||||
rescheduleUid?: string;
|
||||
bookingUid?: string;
|
||||
recurringEventId?: string;
|
||||
recurringCount?: number;
|
||||
rescheduledBy?: string;
|
||||
cancelledBy?: string;
|
||||
schedulingType?: SchedulingType;
|
||||
responses: {
|
||||
email: string;
|
||||
name: string;
|
||||
location?: { optionValue: ""; value: string };
|
||||
attendeePhoneNumber?: string;
|
||||
smsReminderNumber?: string;
|
||||
};
|
||||
};
|
||||
} & CommonPropsMockRequestData;
|
||||
}) {
|
||||
return {
|
||||
...getBasicMockRequestDataForBooking(),
|
||||
...data,
|
||||
};
|
||||
}
|
||||
|
||||
export function getMockRequestDataForDynamicGroupBooking({
|
||||
data,
|
||||
}: {
|
||||
data: Partial<ReturnType<typeof getBasicMockRequestDataForBooking>> & {
|
||||
eventTypeId: 0;
|
||||
eventTypeSlug: string;
|
||||
user: string;
|
||||
} & CommonPropsMockRequestData;
|
||||
}) {
|
||||
return {
|
||||
...getBasicMockRequestDataForBooking(),
|
||||
|
||||
@@ -6568,9 +6568,13 @@
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `phone` and the URL contains query parameter `&phone=1234567890`, the phone field will be prefilled with this value and disabled."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
}
|
||||
},
|
||||
"required": ["type", "slug", "label", "required", "placeholder"]
|
||||
"required": ["type", "slug", "label", "required", "placeholder", "hidden"]
|
||||
},
|
||||
"AddressFieldInput_2024_06_14": {
|
||||
"type": "object",
|
||||
@@ -6599,9 +6603,13 @@
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `address` and the URL contains query parameter `&address=1234 Main St, London`, the address field will be prefilled with this value and disabled."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
}
|
||||
},
|
||||
"required": ["type", "slug", "label", "required", "placeholder"]
|
||||
"required": ["type", "slug", "label", "required", "placeholder", "hidden"]
|
||||
},
|
||||
"TextFieldInput_2024_06_14": {
|
||||
"type": "object",
|
||||
@@ -6630,9 +6638,13 @@
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `who-referred-you` and the URL contains query parameter `&who-referred-you=bob`, the text field will be prefilled with this value and disabled."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
}
|
||||
},
|
||||
"required": ["type", "slug", "label", "required", "placeholder"]
|
||||
"required": ["type", "slug", "label", "required", "placeholder", "hidden"]
|
||||
},
|
||||
"NumberFieldInput_2024_06_14": {
|
||||
"type": "object",
|
||||
@@ -6661,9 +6673,13 @@
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `calories-per-day` and the URL contains query parameter `&calories-per-day=3000`, the number field will be prefilled with this value and disabled."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
}
|
||||
},
|
||||
"required": ["type", "slug", "label", "required", "placeholder"]
|
||||
"required": ["type", "slug", "label", "required", "placeholder", "hidden"]
|
||||
},
|
||||
"TextAreaFieldInput_2024_06_14": {
|
||||
"type": "object",
|
||||
@@ -6692,9 +6708,13 @@
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `dear-diary` and the URL contains query parameter `&dear-diary=Today I shipped a feature`, the text area will be prefilled with this value and disabled."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
}
|
||||
},
|
||||
"required": ["type", "slug", "label", "required", "placeholder"]
|
||||
"required": ["type", "slug", "label", "required", "placeholder", "hidden"]
|
||||
},
|
||||
"SelectFieldInput_2024_06_14": {
|
||||
"type": "object",
|
||||
@@ -6730,9 +6750,13 @@
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `language` and options of this select field are ['english', 'italian'] and the URL contains query parameter `&language=italian`, the 'italian' will be selected and the select field will be disabled."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
}
|
||||
},
|
||||
"required": ["type", "slug", "label", "required", "placeholder", "options"]
|
||||
"required": ["type", "slug", "label", "required", "placeholder", "options", "hidden"]
|
||||
},
|
||||
"MultiSelectFieldInput_2024_06_14": {
|
||||
"type": "object",
|
||||
@@ -6764,9 +6788,13 @@
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `consultants` and the URL contains query parameter `&consultants=en&language=it`, the 'en' and 'it' will be selected and the select field will be disabled."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
}
|
||||
},
|
||||
"required": ["type", "slug", "label", "required", "options"]
|
||||
"required": ["type", "slug", "label", "required", "options", "hidden"]
|
||||
},
|
||||
"MultiEmailFieldInput_2024_06_14": {
|
||||
"type": "object",
|
||||
@@ -6795,9 +6823,13 @@
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `consultants` and the URL contains query parameter `&consultants=alice@gmail.com&consultants=bob@gmail.com`, the these emails will be added and none more can be added."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
}
|
||||
},
|
||||
"required": ["type", "slug", "label", "required", "placeholder"]
|
||||
"required": ["type", "slug", "label", "required", "placeholder", "hidden"]
|
||||
},
|
||||
"CheckboxGroupFieldInput_2024_06_14": {
|
||||
"type": "object",
|
||||
@@ -6829,9 +6861,13 @@
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `notify-me` and the URL contains query parameter `¬ify-me=true`, the checkbox will be selected and the checkbox field will be disabled."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
}
|
||||
},
|
||||
"required": ["type", "slug", "label", "required", "options"]
|
||||
"required": ["type", "slug", "label", "required", "options", "hidden"]
|
||||
},
|
||||
"RadioGroupFieldInput_2024_06_14": {
|
||||
"type": "object",
|
||||
@@ -6863,9 +6899,13 @@
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `language` and options of this select field are ['english', 'italian'] and the URL contains query parameter `&language=italian`, the 'italian' radio buttom will be selected and the select field will be disabled."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
}
|
||||
},
|
||||
"required": ["type", "slug", "label", "required", "options"]
|
||||
"required": ["type", "slug", "label", "required", "options", "hidden"]
|
||||
},
|
||||
"BooleanFieldInput_2024_06_14": {
|
||||
"type": "object",
|
||||
@@ -6889,9 +6929,13 @@
|
||||
},
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
}
|
||||
},
|
||||
"required": ["type", "slug", "label", "required"]
|
||||
"required": ["type", "slug", "label", "required", "hidden"]
|
||||
},
|
||||
"BusinessDaysWindow_2024_06_14": {
|
||||
"type": "object",
|
||||
@@ -7782,9 +7826,13 @@
|
||||
},
|
||||
"required": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
}
|
||||
},
|
||||
"required": ["isDefault", "slug", "type", "required"]
|
||||
"required": ["isDefault", "slug", "type", "required", "hidden"]
|
||||
},
|
||||
"RescheduleReasonDefaultFieldOutput_2024_06_14": {
|
||||
"type": "object",
|
||||
@@ -7805,9 +7853,23 @@
|
||||
},
|
||||
"required": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string"
|
||||
},
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `&rescheduleReason=busy`, the reschedule reason field will be prefilled with this value and disabled."
|
||||
}
|
||||
},
|
||||
"required": ["isDefault", "slug", "type", "required"]
|
||||
"required": ["isDefault", "slug", "type", "required", "hidden", "disableOnPrefill"]
|
||||
},
|
||||
"TitleDefaultFieldOutput_2024_06_14": {
|
||||
"type": "object",
|
||||
@@ -7828,9 +7890,23 @@
|
||||
},
|
||||
"required": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string"
|
||||
},
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `&title=masterclass`, the title field will be prefilled with this value and disabled."
|
||||
}
|
||||
},
|
||||
"required": ["isDefault", "slug", "type", "required"]
|
||||
"required": ["isDefault", "slug", "type", "required", "hidden", "disableOnPrefill"]
|
||||
},
|
||||
"NotesDefaultFieldOutput_2024_06_14": {
|
||||
"type": "object",
|
||||
@@ -7851,9 +7927,23 @@
|
||||
},
|
||||
"required": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string"
|
||||
},
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `¬es=hello`, the notes field will be prefilled with this value and disabled."
|
||||
}
|
||||
},
|
||||
"required": ["isDefault", "slug", "type", "required"]
|
||||
"required": ["isDefault", "slug", "type", "required", "hidden", "disableOnPrefill"]
|
||||
},
|
||||
"GuestsDefaultFieldOutput_2024_06_14": {
|
||||
"type": "object",
|
||||
@@ -7874,9 +7964,23 @@
|
||||
},
|
||||
"required": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string"
|
||||
},
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `&guests=lauris@cal.com`, the guests field will be prefilled with this value and disabled."
|
||||
}
|
||||
},
|
||||
"required": ["isDefault", "slug", "type", "required"]
|
||||
"required": ["isDefault", "slug", "type", "required", "hidden", "disableOnPrefill"]
|
||||
},
|
||||
"AddressFieldOutput_2024_06_14": {
|
||||
"type": "object",
|
||||
@@ -7921,6 +8025,10 @@
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `address` and the URL contains query parameter `&address=1234 Main St, London`, the address field will be prefilled with this value and disabled."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "object",
|
||||
"default": false,
|
||||
@@ -7928,7 +8036,7 @@
|
||||
"example": false
|
||||
}
|
||||
},
|
||||
"required": ["type", "slug", "label", "required", "isDefault"]
|
||||
"required": ["type", "slug", "label", "required", "hidden", "isDefault"]
|
||||
},
|
||||
"BooleanFieldOutput_2024_06_14": {
|
||||
"type": "object",
|
||||
@@ -7968,6 +8076,10 @@
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "object",
|
||||
"default": false,
|
||||
@@ -7975,7 +8087,7 @@
|
||||
"example": false
|
||||
}
|
||||
},
|
||||
"required": ["type", "slug", "label", "required", "isDefault"]
|
||||
"required": ["type", "slug", "label", "required", "hidden", "isDefault"]
|
||||
},
|
||||
"CheckboxGroupFieldOutput_2024_06_14": {
|
||||
"type": "object",
|
||||
@@ -8023,6 +8135,10 @@
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `notify-me` and the URL contains query parameter `¬ify-me=true`, the checkbox will be selected and the checkbox field will be disabled."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "object",
|
||||
"default": false,
|
||||
@@ -8030,7 +8146,7 @@
|
||||
"example": false
|
||||
}
|
||||
},
|
||||
"required": ["type", "slug", "label", "required", "options", "isDefault"]
|
||||
"required": ["type", "slug", "label", "required", "options", "hidden", "isDefault"]
|
||||
},
|
||||
"MultiEmailFieldOutput_2024_06_14": {
|
||||
"type": "object",
|
||||
@@ -8075,6 +8191,10 @@
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `consultants` and the URL contains query parameter `&consultants=alice@gmail.com&consultants=bob@gmail.com`, the these emails will be added and none more can be added."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "object",
|
||||
"default": false,
|
||||
@@ -8082,7 +8202,7 @@
|
||||
"example": false
|
||||
}
|
||||
},
|
||||
"required": ["type", "slug", "label", "required", "isDefault"]
|
||||
"required": ["type", "slug", "label", "required", "hidden", "isDefault"]
|
||||
},
|
||||
"MultiSelectFieldOutput_2024_06_14": {
|
||||
"type": "object",
|
||||
@@ -8130,6 +8250,10 @@
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `consultants` and the URL contains query parameter `&consultants=en&language=it`, the 'en' and 'it' will be selected and the select field will be disabled."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "object",
|
||||
"default": false,
|
||||
@@ -8137,7 +8261,7 @@
|
||||
"example": false
|
||||
}
|
||||
},
|
||||
"required": ["type", "slug", "label", "required", "options", "isDefault"]
|
||||
"required": ["type", "slug", "label", "required", "options", "hidden", "isDefault"]
|
||||
},
|
||||
"NumberFieldOutput_2024_06_14": {
|
||||
"type": "object",
|
||||
@@ -8182,6 +8306,10 @@
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `calories-per-day` and the URL contains query parameter `&calories-per-day=3000`, the number field will be prefilled with this value and disabled."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "object",
|
||||
"default": false,
|
||||
@@ -8189,7 +8317,7 @@
|
||||
"example": false
|
||||
}
|
||||
},
|
||||
"required": ["type", "slug", "label", "required", "isDefault"]
|
||||
"required": ["type", "slug", "label", "required", "hidden", "isDefault"]
|
||||
},
|
||||
"PhoneFieldOutput_2024_06_14": {
|
||||
"type": "object",
|
||||
@@ -8232,6 +8360,10 @@
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `phone` and the URL contains query parameter `&phone=1234567890`, the phone field will be prefilled with this value and disabled."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "object",
|
||||
"default": false,
|
||||
@@ -8239,7 +8371,7 @@
|
||||
"example": false
|
||||
}
|
||||
},
|
||||
"required": ["type", "slug", "label", "required", "isDefault"]
|
||||
"required": ["type", "slug", "label", "required", "hidden", "isDefault"]
|
||||
},
|
||||
"RadioGroupFieldOutput_2024_06_14": {
|
||||
"type": "object",
|
||||
@@ -8287,6 +8419,10 @@
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `language` and options of this select field are ['english', 'italian'] and the URL contains query parameter `&language=italian`, the 'italian' radio buttom will be selected and the select field will be disabled."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "object",
|
||||
"default": false,
|
||||
@@ -8294,7 +8430,7 @@
|
||||
"example": false
|
||||
}
|
||||
},
|
||||
"required": ["type", "slug", "label", "required", "options", "isDefault"]
|
||||
"required": ["type", "slug", "label", "required", "options", "hidden", "isDefault"]
|
||||
},
|
||||
"SelectFieldOutput_2024_06_14": {
|
||||
"type": "object",
|
||||
@@ -8346,6 +8482,10 @@
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `language` and options of this select field are ['english', 'italian'] and the URL contains query parameter `&language=italian`, the 'italian' will be selected and the select field will be disabled."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "object",
|
||||
"default": false,
|
||||
@@ -8353,7 +8493,7 @@
|
||||
"example": false
|
||||
}
|
||||
},
|
||||
"required": ["type", "slug", "label", "required", "options", "isDefault"]
|
||||
"required": ["type", "slug", "label", "required", "options", "hidden", "isDefault"]
|
||||
},
|
||||
"TextAreaFieldOutput_2024_06_14": {
|
||||
"type": "object",
|
||||
@@ -8398,6 +8538,10 @@
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `dear-diary` and the URL contains query parameter `&dear-diary=Today I shipped a feature`, the text area will be prefilled with this value and disabled."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "object",
|
||||
"default": false,
|
||||
@@ -8405,7 +8549,7 @@
|
||||
"example": false
|
||||
}
|
||||
},
|
||||
"required": ["type", "slug", "label", "required", "isDefault"]
|
||||
"required": ["type", "slug", "label", "required", "hidden", "isDefault"]
|
||||
},
|
||||
"TextFieldOutput_2024_06_14": {
|
||||
"type": "object",
|
||||
@@ -8450,6 +8594,10 @@
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if the slug is `who-referred-you` and the URL contains query parameter `&who-referred-you=bob`, the text field will be prefilled with this value and disabled."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "object",
|
||||
"default": false,
|
||||
@@ -8457,7 +8605,7 @@
|
||||
"example": false
|
||||
}
|
||||
},
|
||||
"required": ["type", "slug", "label", "required", "isDefault"]
|
||||
"required": ["type", "slug", "label", "required", "hidden", "isDefault"]
|
||||
},
|
||||
"EventTypeOutput_2024_06_14": {
|
||||
"type": "object",
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
Once you create a Cal.com account, you are taken to the onboarding flow, where you'll need to go through the process of setting up some basics to get you up and running.
|
||||
|
||||
## Set up your name and Timezone
|
||||
|
||||
Here all you need to do is enter your full name to be displayed on your booking page, and confirm the Timezone that you're in.
|
||||
|
||||
[](https://downloads.intercomcdn.com/i/o/1195453145/c9b80553eef388dc17159eb2/i1600x900-akHcmOHfAXkX_g7isuj.png?expires=1734714000\&signature=bf60f076c8be0cdb75adb87a3dd5752858dcbe3ff180ab6ebf4d031d65ed5258\&req=dSEuE817noBbXPMW1HO4zTBxsMaGV1gm1DL2Z9%2F%2BQFifN2WjjinbVwafM6T5%0ARaL9zXFNHzrcG3wxWR4%3D%0A)
|
||||
|
||||
|
||||
## Connect a calendar
|
||||
|
||||
You can connect your Cal.com account with any of your calendars to automatically check for busy times and create new events as they are scheduled. If you wish to connect a calendar at a later stage, you can always click on I'll connect my calendar later and it'll skip to the next step.
|
||||
|
||||
[](https://downloads.intercomcdn.com/i/o/1195453149/f925bef4ffa0c3dfba474b3b/i1600x900-UcoZykKljPsG_pftvub.png?expires=1734714000\&signature=000519db799774393a89877fb30e880d9da94f32a75d36d3e5a80ac049ca0123\&req=dSEuE817noBbUPMW1HO4zeZHSZdXerJcD8lCqNo4dvyWVoOezDI3AXKbsZhS%0ANkOqYBaHc7hK4T2AsIM%3D%0A)
|
||||
|
||||
|
||||
## Set up your availability
|
||||
|
||||
You can set your availability schedule in this step. You can always create or modify your availability at a later stage in the application as well. If you wish you can learn more about [Setting up your availability](https://cal.com/docs/how-to-guides/setting-up-your-availability)or check out the deep dive into the [Availability](https://cal.com/docs/core-features/availability) feature.
|
||||
|
||||
[](https://downloads.intercomcdn.com/i/o/1195453147/132a2404780a8562fb1cde2d/i1600x900--rsrw7G9JKrW_d8fm8m.png?expires=1734714000\&signature=7a2205317ef97414e3d21152f26f6e29f2d4602b418483d52ae7a153aa3139df\&req=dSEuE817noBbXvMW1HO4zd1E9SbAWKPFuzqf51wG7XJc4LH2AVGqq5hbJfnc%0AsjDrR5aYBQaRWORa7%2F0%3D%0A)
|
||||
|
||||
|
||||
## Final profile details
|
||||
|
||||
Finally, you can personalize your Cal.com booking link by providing a brief description about yourself and uploading a photograph so the people know who they're booking with.
|
||||
|
||||
[](https://downloads.intercomcdn.com/i/o/1195453146/58c4a5fbdd2fcf6cc17f4543/i1600x900-YhAs1gRVQJwU_wf80pn.png?expires=1734714000\&signature=662e46f9d63ef15c9907134266c0dfeebee9c7dd9865deedffd40d589125e2f0\&req=dSEuE817noBbX%2FMW1HO4zWnuexBfXiF8GXk3aYtP0jJBtLE1W50mTVeyKwo4%0ACOBYZPlkK985qOcQOpY%3D%0A)
|
||||
|
||||
That's it. You're fully onboarded and all set to start your scheduling journey!
|
||||
@@ -0,0 +1,19 @@
|
||||
Cal.com offers customizable appearance baked into the product. You can select from a few options and dictate how your booking pages will look like:
|
||||
|
||||
[](https://downloads.intercomcdn.com/i/o/1198206315/2164d7a47f3c65ba0bf706cb/i1600x899-HIol7m7sT4zB_lqidld.png?expires=1734714000\&signature=02aa2a62098d5eeb0e1e37cb18d6cb4ac807d038f02b08e93b32488e66c9db93\&req=dSEuHst%2Bm4JeXPMW1HO4zdxW8e1lYdsGkf3DS870RkQuEAWG7v0FpWRkqsEc%0AtPwY9zj60L8tT8uVkPA%3D%0A)
|
||||
|
||||
|
||||
## Theme
|
||||
|
||||
You can select what the theme of your booking pages would be. You can either force the booking page to be of Light mode or Dark mode where the visitors to your booking page would only ever see the selected mode. Alternatively, you could select the System default (selected by default by Cal.com) where the visitors to your website would see the version as per their system default settings (Light or Dark)
|
||||
|
||||
[](https://downloads.intercomcdn.com/i/o/1198206314/f04bd67aa9cae8b54fb6d9b4/i805x289-9ZjOQDA70FMu_zjot71.png?expires=1734714000\&signature=f9d629c3cf779a2a89483890ece99cbd9d6f9a448b0d3f9a84d050e8911c46ac\&req=dSEuHst%2Bm4JeXfMW1HO4ze2Wk3jG3ORofxgPA8NLppLNtG8nsjtRtR1pPujo%0APjy7tUX%2BQivWNM%2Br2rs%3D%0A)
|
||||
|
||||
|
||||
## Brand Colors
|
||||
|
||||
You can select your Brand Color for both light theme and dark theme which over-rides the default brand color of Cal.com in your public booking pages. This adds a custom feel to the booking experience.
|
||||
|
||||
[](https://downloads.intercomcdn.com/i/o/1198206313/f30357693da318f7934b7c88/i817x190--uQ-QNjQv7N-_dhazio.png?expires=1734714000\&signature=e14a0ad67e98e7ab8c19d36d1c27d463b9f42d6b7634aec5956584e468678801\&req=dSEuHst%2Bm4JeWvMW1HO4zSGYuaZeLpetOwWwqy61UKiW0pTsmTcvhb3LU8W2%0As5OsVpLHL2I9NeTC5nk%3D%0A)
|
||||
|
||||
You can easily use these in combination with [embed](https://cal.com/docs/core-features/embed) to make the cal.com embed feel like a part of your website by simply updating these settings to match your website.
|
||||
@@ -0,0 +1,24 @@
|
||||
[](https://downloads.intercomcdn.com/i/o/1198516524/505260fe7d81071196387055/i1600x600-Z8AAymlAKVce_i4nxdb.png?expires=1734714000\&signature=587aa816f0dbbd04a33cf01abf7a2eed40db8a79e5c8d27143f288177102124e\&req=dSEuHsx%2Fm4RdXfMW1HO4zWgud8yLptvKvIvPZJajfyS0aZMFn%2B5i3QycHg63%0A4mbN9rXv1DmkJnbmdw4%3D%0A)
|
||||
|
||||
|
||||
## Sign up for a free Cal.com account (recommended)
|
||||
|
||||
The easiest way to get started with Cal.com is with our hosted plan. We offer a generous free tier where you get unlimited scheduling with no limits and all of our features built for individuals, with the option to upgrade to a teams plan for collaboration features.
|
||||
|
||||
[Sign up for a Cal.com account](https://cal.com/docs/introduction/quick-start/sign-up-for-a-cal.com-account)
|
||||
|
||||
[](https://downloads.intercomcdn.com/i/o/1198516520/2b04acb45b27895293b1342f/i1600x600-Bt00AJE4uNcL_hiqcp9.png?expires=1734714000\&signature=7801860013d1cf148d0a2ee818b73d005f8a4b991142811f61521da3415f9d1c\&req=dSEuHsx%2Fm4RdWfMW1HO4zZ7b2lOBVgWtmZAzR1fVuzBWzPOH7NAHPqpuJ05b%0AY5L%2FoLnclN2QbXQdru4%3D%0A)
|
||||
|
||||
|
||||
## Self-host Cal.com on your infrastructure
|
||||
|
||||
For full data ownership and control over your scheduling, you can self-host Cal.com on your own infrastructure.
|
||||
|
||||
[Self-hosting](https://cal.com/docs/introduction/quick-start/self-hosting)
|
||||
|
||||
|
||||
## Quick start for contributors
|
||||
|
||||
If you wish to contribute to Cal.com, please take a look at our guide to fast-track the onboarding process for new contributors.
|
||||
|
||||
[Quick start for contributors](https://cal.com/docs/knowledgebase/contributors-guide/quick-start)
|
||||
@@ -0,0 +1,17 @@
|
||||
Signing up for a Cal.com account is easy, and can be done in 30 seconds.
|
||||
|
||||
|
||||
[](https://downloads.intercomcdn.com/i/o/1195450358/19847d88dcfad843e67625c3/i958x1232-mgsIib6PZJLt_zobva8.png?expires=1734714000\&signature=32dc841eb7f14bc2405dcdcc5099bb9c6f69201b41d18dfc01f875ec8258cc96\&req=dSEuE817nYJaUfMW1HO4zbaARDNHwLdsX03LyFtsrjDQzWADhg88oNWCQ3gG%0AbA%2FZgo9B8pw%2Fp7a6MOw%3D%0A)
|
||||
|
||||
|
||||
To get started, head to [cal.com/signup](https://cal.com/signup), where you'll be able to enter your desired username (the part that comes after cal.com, like cal.com/_bailey_), your email and a password. Alternatively, you can sign up using your Google account, or using SAML SSO by clicking on the respective buttons.
|
||||
|
||||
|
||||
Please note that if you choose a premium username, which is a username that‘s either short or a common name, it costs a monthly fee. This prevents username squatting and ensures that people and brands are often able to get their username without it being taken by someone. You can always start with a non-premium username and then upgrade later on from within the application.
|
||||
|
||||
|
||||
## Next steps
|
||||
|
||||
Once you've signed up, you'll go through the new user onboarding flow. Click below to skip to that section of the documentation:
|
||||
|
||||
[Complete your onboarding](https://intercom.help/calcom-inc-e6dea00610aa/en/articles/9929590-complete-your-onboarding)
|
||||
@@ -1,27 +0,0 @@
|
||||
---
|
||||
title: "Quick Start"
|
||||
---
|
||||
|
||||
<img src="/images/i1600x600-Z8AAymlAKVce_i4nxdb.png" />
|
||||
|
||||
## Sign up for a free Cal.com account (recommended)
|
||||
|
||||
The easiest way to get started with Cal.com is with our hosted plan. We offer a generous free tier where you get unlimited scheduling with no limits and all of our features built for individuals, with the option to upgrade to a teams plan for collaboration features.
|
||||
|
||||
<a href="/docs/introduction/quick-start/sign-up-for-a-cal.com-account">Sign up for a Cal.com account</a>
|
||||
|
||||
<img src="/images/i1600x600-Bt00AJE4uNcL_hiqcp9.png" />
|
||||
|
||||
|
||||
## Self-host Cal.com on your infrastructure
|
||||
|
||||
For full data ownership and control over your scheduling, you can self-host Cal.com on your own infrastructure.
|
||||
|
||||
<a href="/docs/introduction/quick-start/self-hosting">Self-hosting</a>
|
||||
|
||||
|
||||
## Quick start for contributors
|
||||
|
||||
If you wish to contribute to Cal.com, please take a look at our guide to fast-track the onboarding process for new contributors.
|
||||
|
||||
<a href="/docs/knowledgebase/contributors-guide/quick-start">Quick start for contributors</a>
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
title: "Quick start"
|
||||
description: "Description of your new file."
|
||||
---
|
||||
|
||||
## Sign up for a free Cal.com account (recommended)
|
||||
|
||||
The easiest way to get started with Cal.com is with our hosted plan. We offer a generous free tier where you get unlimited scheduling with no limits and all of our features built for individuals, with the option to upgrade to a teams plan for collaboration features.
|
||||
@@ -2876,6 +2876,8 @@ checksums:
|
||||
disable_org_url_description: 875a1606ef83f120547305072b1b8723
|
||||
single_select: 40d573e93a3059895cf3e9ee1e7012c6
|
||||
multi_select: 1b93efbd975c6266858392adf33e6558
|
||||
calendar_conflict_settings: 9d5721f39701b71ed421797e44e0a701
|
||||
select_calendars_conflict_check: f4147c353f07390fcb44caa27c34ac57
|
||||
group_meeting: d30d80ebb9c0b0256130a3c516cb4b4f
|
||||
salesforce_ignore_guests: f56d10682d9a637a4bec42abcd212a35
|
||||
google_meet: e30a500b4e93f691c4b2ab046b96e753
|
||||
|
||||
Binary file not shown.
@@ -13,6 +13,7 @@ import bolna_config_json from "./bolna/config.json";
|
||||
import { metadata as caldavcalendar__metadata_ts } from "./caldavcalendar/_metadata";
|
||||
import campfire_config_json from "./campfire/config.json";
|
||||
import campsite_config_json from "./campsite/config.json";
|
||||
import chatbase_config_json from "./chatbase/config.json";
|
||||
import clic_config_json from "./clic/config.json";
|
||||
import closecom_config_json from "./closecom/config.json";
|
||||
import cron_config_json from "./cron/config.json";
|
||||
@@ -112,6 +113,7 @@ export const appStoreMetadata = {
|
||||
caldavcalendar: caldavcalendar__metadata_ts,
|
||||
campfire: campfire_config_json,
|
||||
campsite: campsite_config_json,
|
||||
chatbase: chatbase_config_json,
|
||||
clic: clic_config_json,
|
||||
closecom: closecom_config_json,
|
||||
cron: cron_config_json,
|
||||
|
||||
@@ -14,6 +14,7 @@ export const apiHandlers = {
|
||||
caldavcalendar: import("./caldavcalendar/api"),
|
||||
campfire: import("./campfire/api"),
|
||||
campsite: import("./campsite/api"),
|
||||
chatbase: import("./chatbase/api"),
|
||||
clic: import("./clic/api"),
|
||||
closecom: import("./closecom/api"),
|
||||
cron: import("./cron/api"),
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
items:
|
||||
- 1.jpg
|
||||
- 2.jpg
|
||||
- 3.jpg
|
||||
---
|
||||
|
||||
# Custom ChatGPT for your business
|
||||
|
||||
Build a custom GPT, embed it on your website and let it handle customer support, lead generation, engage with your users, and more.
|
||||
|
||||
{DESCRIPTION}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { createDefaultInstallation } from "@calcom/app-store/_utils/installation";
|
||||
import type { AppDeclarativeHandler } from "@calcom/types/AppHandler";
|
||||
|
||||
import appConfig from "../config.json";
|
||||
|
||||
const handler: AppDeclarativeHandler = {
|
||||
appType: appConfig.type,
|
||||
variant: appConfig.variant,
|
||||
slug: appConfig.slug,
|
||||
supportsMultipleInstalls: false,
|
||||
handlerType: "add",
|
||||
redirect: {
|
||||
newTab: true,
|
||||
url: "https://www.chatbase.co/guide/cal",
|
||||
},
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as add } from "./add";
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"/*": "Don't modify slug - If required, do it using cli edit command",
|
||||
"name": "Chatbase",
|
||||
"slug": "chatbase",
|
||||
"type": "chatbase_other",
|
||||
"logo": "icon-dark.svg",
|
||||
"url": "https://www.chatbase.co/guide/cal",
|
||||
"variant": "other",
|
||||
"categories": ["other"],
|
||||
"publisher": "Chatbase",
|
||||
"email": "sandra@chatbase.co",
|
||||
"description": "Add Cal.com to your Chatbase AI Chatbot and accept bookings right inside chat.",
|
||||
"isTemplate": false,
|
||||
"__createdUsingCli": true,
|
||||
"__template": "link-as-an-app"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * as api from "./api";
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"name": "@calcom/chatbase",
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
},
|
||||
"description": "Add Cal.com to your Chatbase AI Chatbot and accept bookings right inside chat."
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 9.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 95 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
@@ -0,0 +1,10 @@
|
||||
<svg width="6" height="6" viewBox="0 0 6 6" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.85714 0.404297H1.33333C0.596954 0.404297 0 1.00125 0 1.73763V4.26144C0 4.99782 0.596954 5.59477 1.33333 5.59477H3.85714C4.59352 5.59477 5.19048 4.99782 5.19048 4.26144V1.73763C5.19048 1.00125 4.59352 0.404297 3.85714 0.404297Z" fill="#09090B"/>
|
||||
<path d="M4.02316 2.61876H3.18506C3.1803 2.55299 3.16419 2.48855 3.13744 2.42828C3.113 2.3724 3.07704 2.3223 3.03191 2.28127C2.98678 2.24024 2.93349 2.2092 2.87554 2.19019C2.80692 2.16493 2.73437 2.15203 2.66125 2.15209C2.5184 2.15209 2.40411 2.18542 2.30887 2.25209C2.21363 2.31876 2.13744 2.41876 2.08983 2.54257C2.04221 2.67114 2.0184 2.82352 2.0184 2.99971C2.0184 3.19019 2.04221 3.34257 2.08983 3.47114C2.14221 3.59495 2.21363 3.69019 2.30887 3.75685C2.40411 3.81876 2.5184 3.85209 2.65649 3.85209C2.73268 3.85209 2.79935 3.8378 2.86125 3.81876C2.9184 3.79971 2.97078 3.77114 3.0184 3.73304C3.06602 3.69971 3.09935 3.65209 3.12792 3.59971C3.15649 3.55209 3.17554 3.49019 3.18506 3.42352L4.02316 3.42828C4.01078 3.56555 3.97368 3.69945 3.91363 3.82352C3.77325 4.11755 3.52646 4.34732 3.22316 4.46638C3.03653 4.53885 2.83762 4.57443 2.63744 4.57114C2.35649 4.57114 2.10411 4.50923 1.88506 4.39019C1.66235 4.26708 1.48033 4.08175 1.36125 3.85685C1.22792 3.62352 1.16602 3.3378 1.16602 2.99971C1.16602 2.65685 1.23268 2.37114 1.36125 2.14257C1.49459 1.90447 1.67078 1.72828 1.88983 1.60923C2.28608 1.41139 2.74378 1.37552 3.16602 1.50923C3.32792 1.55685 3.47078 1.6378 3.59459 1.7378C3.7179 1.8396 3.81848 1.96613 3.88983 2.10923C3.96125 2.25685 4.00887 2.42828 4.02316 2.61876Z" fill="white"/>
|
||||
<mask id="mask0_2819_55" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="1" y="1" width="3" height="4">
|
||||
<path d="M1.97619 1.5L2.42857 2.19048L2.97619 3.78571L3.09524 4.54762L1.59524 4.64286L1 3.78571L1.16667 2.11905L1.97619 1.5Z" fill="#A1A1AA"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_2819_55)">
|
||||
<path d="M4.02316 2.61876H3.18506C3.1803 2.55299 3.16419 2.48855 3.13744 2.42828C3.113 2.3724 3.07704 2.3223 3.03191 2.28127C2.98678 2.24024 2.93349 2.2092 2.87554 2.19019C2.80692 2.16493 2.73437 2.15203 2.66125 2.15209C2.5184 2.15209 2.40411 2.18542 2.30887 2.25209C2.21363 2.31876 2.13744 2.41876 2.08983 2.54257C2.04221 2.67114 2.0184 2.82352 2.0184 2.99971C2.0184 3.19019 2.04221 3.34257 2.08983 3.47114C2.14221 3.59495 2.21363 3.69019 2.30887 3.75685C2.40411 3.81876 2.5184 3.85209 2.65649 3.85209C2.73268 3.85209 2.79935 3.8378 2.86125 3.81876C2.9184 3.79971 2.97078 3.77114 3.0184 3.73304C3.06602 3.69971 3.09935 3.65209 3.12792 3.59971C3.15649 3.55209 3.17554 3.49019 3.18506 3.42352L4.02316 3.42828C4.01078 3.56555 3.97368 3.69945 3.91363 3.82352C3.77325 4.11755 3.52646 4.34732 3.22316 4.46638C3.03653 4.53885 2.83762 4.57443 2.63744 4.57114C2.35649 4.57114 2.10411 4.50923 1.88506 4.39019C1.66235 4.26708 1.48033 4.08175 1.36125 3.85685C1.22792 3.62352 1.16602 3.3378 1.16602 2.99971C1.16602 2.65685 1.23268 2.37114 1.36125 2.14257C1.49459 1.90447 1.67078 1.72828 1.88983 1.60923C2.28608 1.41139 2.74378 1.37552 3.16602 1.50923C3.32792 1.55685 3.47078 1.6378 3.59459 1.7378C3.7179 1.8396 3.81848 1.96613 3.88983 2.10923C3.96125 2.25685 4.00887 2.42828 4.02316 2.61876Z" fill="#B2AEB9"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
@@ -113,6 +113,8 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
// also this improves performance for most of the happy-paths.
|
||||
try {
|
||||
await gCalService.upsertSelectedCalendar({
|
||||
// First install should add a user-level selectedCalendar only.
|
||||
eventTypeId: null,
|
||||
externalId: selectedCalendarWhereUnique.externalId,
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -14,7 +14,11 @@ async function postHandler(req: NextApiRequest) {
|
||||
throw new HttpError({ statusCode: 403, message: "Missing Channel ID" });
|
||||
}
|
||||
|
||||
const selectedCalendar = await SelectedCalendarRepository.findByGoogleChannelId(
|
||||
// There could be multiple selected calendars for the same googleChannelId for different eventTypes and same user
|
||||
// Every such record has their googleChannel related fields set which are same
|
||||
// So, it is enough to get the first selected calendar for this googleChannelId
|
||||
// Further code gets all the selected calendars for this calendar's credential
|
||||
const selectedCalendar = await SelectedCalendarRepository.findFirstByGoogleChannelId(
|
||||
req.headers["x-goog-channel-id"]
|
||||
);
|
||||
|
||||
@@ -32,6 +36,9 @@ async function postHandler(req: NextApiRequest) {
|
||||
});
|
||||
const { selectedCalendars } = credential;
|
||||
const calendar = await getCalendar(credential);
|
||||
|
||||
// Make sure to pass unique SelectedCalendars to avoid unnecessary third party api calls
|
||||
// Necessary to do here so that it is ensure for all calendar apps
|
||||
await calendar?.fetchAvailabilityAndSetCache?.(selectedCalendars);
|
||||
return { message: "ok" };
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user