Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1619f2daf2 | ||
|
|
57bd0b3df3 | ||
|
|
e359d13d9a | ||
|
|
3b7cb542ed | ||
|
|
fd7f6c32ae | ||
|
|
06d8b98b15 | ||
|
|
0c69c5dfea | ||
|
|
00f7d8be0d | ||
|
|
2e9beb8513 | ||
|
|
b6c6fa7365 | ||
|
|
c623785ec2 | ||
|
|
6a381cdc79 | ||
|
|
9d4496133e | ||
|
|
1452672495 | ||
|
|
9159c591ff | ||
|
|
408aee5851 | ||
|
|
29bbada77a | ||
|
|
6178092878 | ||
|
|
6f531d0be5 | ||
|
|
d7b723c914 | ||
|
|
aad3988d60 | ||
|
|
c8172ab790 | ||
|
|
657f9dfd41 | ||
|
|
c48b209c04 | ||
|
|
b9a460d8b4 | ||
|
|
a7727b5ca6 | ||
|
|
9b661b75f1 | ||
|
|
487cc0751d | ||
|
|
f7f36978e1 | ||
|
|
c1dbb7cffd | ||
|
|
e722e75a6f | ||
|
|
455572b5f4 | ||
|
|
2fe4462f62 | ||
|
|
f9dc788007 | ||
|
|
6e293d2985 | ||
|
|
00334c5610 | ||
|
|
2f9c8cb441 | ||
|
|
39173900b1 | ||
|
|
bd61e01a5d | ||
|
|
aa0b3a2778 | ||
|
|
170feeae5e | ||
|
|
a67b57b216 | ||
|
|
821a16648f | ||
|
|
727f7df2c2 | ||
|
|
be5f7f83b5 | ||
|
|
02a49e2b4b | ||
|
|
22f92f1ee3 | ||
|
|
c7a797c0e0 | ||
|
|
447436dfff | ||
|
|
c415b2d7f5 | ||
|
|
c10c87c98e | ||
|
|
85f96f492d | ||
|
|
b1350d0764 | ||
|
|
bac2d1f6f0 | ||
|
|
fec504496a | ||
|
|
890fc92148 | ||
|
|
557bd8bf25 | ||
|
|
d8027fe248 | ||
|
|
116c765c36 | ||
|
|
c6c3e228a3 | ||
|
|
7584645b8c | ||
|
|
32ff7ffed1 | ||
|
|
55113f20fe | ||
|
|
0a293308e7 | ||
|
|
e558daaa0b | ||
|
|
3f0a6718bc | ||
|
|
8ffebdba44 | ||
|
|
cc1101ac43 | ||
|
|
7a80a5f929 | ||
|
|
a27e1498b8 | ||
|
|
a697857fa0 | ||
|
|
e2f45bdf5a | ||
|
|
d2930bc243 | ||
|
|
b0b93f612e | ||
|
|
558c8be740 | ||
|
|
1aee1742d5 | ||
|
|
ab942ed4cd | ||
|
|
b5bec79354 | ||
|
|
b48d0e1f48 | ||
|
|
4a38b4dc89 | ||
|
|
ff12e387b3 | ||
|
|
6b5e144042 | ||
|
|
cd7b93a51e | ||
|
|
a76366cbcb | ||
|
|
860c04bbda | ||
|
+6 |
225313e391 | ||
|
|
3994929a51 | ||
|
|
ae38027d76 | ||
|
|
9036080fcf | ||
|
|
e6ef4e1988 | ||
|
|
3c2619a7a3 | ||
|
|
cfb09a1b21 | ||
|
|
5fe4e09975 | ||
|
|
542799e2cf | ||
|
|
3eda7858c0 | ||
|
|
b59bf7d1ee | ||
|
|
0dadeac352 | ||
|
|
5eb7a4d56e | ||
|
|
43fa944e2f | ||
|
|
1866b2845c | ||
|
|
2cfc63e6e3 | ||
|
|
4f66f3d2c6 | ||
|
|
ca3e7bd97d | ||
|
|
cafa3f9cf3 | ||
|
|
e04faffcbb | ||
|
|
6f1556a2fd | ||
|
|
d72a954896 | ||
|
|
abeb20373c | ||
|
|
422ef2f5b8 | ||
|
|
ec920eb2a8 | ||
|
|
795eecf300 | ||
|
|
8ae3d6d55b | ||
|
|
85070574da | ||
|
|
d1c2872f6e | ||
|
|
42d78f8099 | ||
|
|
f0d7a757d4 | ||
|
|
0b2f09ab7d | ||
|
|
355aaa7010 | ||
|
|
c27b73aa9a | ||
|
|
a622c7b624 | ||
|
|
19bd3e5849 | ||
|
|
7827028e0a | ||
|
|
ebc3ced687 | ||
|
|
b56824e626 | ||
|
|
460e511459 | ||
|
|
b4fd6f3442 | ||
|
|
0a7a0e3687 | ||
|
|
595fffbea0 |
+8
-4
@@ -112,6 +112,9 @@ NEXT_PUBLIC_POSTHOG_HOST=
|
||||
PLAIN_API_KEY=
|
||||
PLAIN_API_URL=https://api.plain.com/v1
|
||||
PLAIN_HMAC_SECRET_KEY=
|
||||
PLAIN_CHAT_ID=
|
||||
PLAIN_CHAT_HMAC_SECRET_KEY=
|
||||
NEXT_PUBLIC_PLAIN_CHAT_EXCLUDED_PATHS=
|
||||
|
||||
# Zendesk Config
|
||||
NEXT_PUBLIC_ZENDESK_KEY=
|
||||
@@ -361,11 +364,7 @@ APP_ROUTER_APPS_SLUG_SETUP_ENABLED=0
|
||||
APP_ROUTER_APPS_CATEGORIES_ENABLED=0
|
||||
# whether we redirect to the future/apps/categories/[category] from /apps/categories/[category] or not
|
||||
APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED=0
|
||||
APP_ROUTER_BOOKING_ENABLED=0
|
||||
APP_ROUTER_BOOKINGS_STATUS_ENABLED=0
|
||||
APP_ROUTER_APPS_ENABLED=0
|
||||
APP_ROUTER_TEAM_ENABLED=0
|
||||
APP_ROUTER_TEAMS_ENABLED=0
|
||||
APP_ROUTER_AUTH_FORGOT_PASSWORD_ENABLED=0
|
||||
APP_ROUTER_AUTH_LOGIN_ENABLED=0
|
||||
APP_ROUTER_AUTH_LOGOUT_ENABLED=0
|
||||
@@ -416,3 +415,8 @@ REPLEXICA_API_KEY=
|
||||
|
||||
# Comma-separated list of DSyncData.directoryId to log SCIM API requests for. It can be enabled temporarily for debugging the requests being sent to SCIM server.
|
||||
DIRECTORY_IDS_TO_LOG=
|
||||
|
||||
|
||||
# Set this when Cal.com is used to serve only one organization's booking pages
|
||||
# Read more about it in the README.md
|
||||
NEXT_PUBLIC_SINGLE_ORG_SLUG=
|
||||
|
||||
Vendored
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
undecided:
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
Vendored
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
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
undecided:
|
||||
- "@calcom/prisma"
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/prisma"
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
undecided:
|
||||
- "@calcom/prisma"
|
||||
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-monorepo
|
||||
Vendored
@@ -485,6 +485,15 @@ Don't code but still want to contribute? Join our [Discussions](https://github.c
|
||||
|
||||
- Set CSP_POLICY="non-strict" env variable, which enables [Strict CSP](https://web.dev/strict-csp/) except for unsafe-inline in style-src . If you have some custom changes in your instance, you might have to make some code change to make your instance CSP compatible. Right now it enables strict CSP only on login page and on other SSR pages it is enabled in Report only mode to detect possible issues. On, SSG pages it is still not supported.
|
||||
|
||||
## Single Org Mode
|
||||
If you want to have booker.yourcompany.com to be the domain used for both dashboard(e.g. https://booker.yourcompany.com/event-types) and booking pages(e.g. https://booker.yourcompany.com/john.joe/15min).
|
||||
- Set the `NEXT_PUBLIC_SINGLE_ORG_SLUG` environment variable to the slug of the organization you want to use. `NEXT_PUBLIC_SINGLE_ORG_SLUG=booker`
|
||||
- Set the `NEXT_PUBLIC_WEBAPP_URL` environment variable to the URL of the Cal.com self-hosted instance e.g. `NEXT_PUBLIC_WEBAPP_URL=https://booker.yourcompany.com`.
|
||||
- Set the `NEXT_PUBLIC_WEBSITE_URL` environment variable to the URL of the Cal.com self-hosted instance e.g. `NEXT_PUBLIC_WEBSITE_URL=https://booker.yourcompany.com`.
|
||||
- Set the `NEXTAUTH_URL` environment variable to the URL of the Cal.com self-hosted instance e.g. `NEXTAUTH_URL=https://booker.yourcompany.com`.
|
||||
|
||||
Note: It causes root to serve the dashboard and not the organization profile page which shows all bookable users in the organization.
|
||||
|
||||
## Integrations
|
||||
|
||||
### Obtaining the Google API Credentials
|
||||
|
||||
@@ -11,15 +11,15 @@ test.describe("Org", () => {
|
||||
await expectPageToBeRenderedWithEmbedSsr(page);
|
||||
});
|
||||
|
||||
test("Org User(Peer) Page should be embeddable", async ({ page }) => {
|
||||
const response = await page.goto("https://i.cal.com/peer/embed");
|
||||
test("Org User(Rick) Page should be embeddable", async ({ page }) => {
|
||||
const response = await page.goto("https://i.cal.com/team-rick/embed");
|
||||
expect(response?.status()).toBe(200);
|
||||
await expect(page.locator("text=Peer Richelsen")).toBeVisible();
|
||||
await expect(page.locator("text=Rick in i.cal.com")).toBeVisible();
|
||||
await expectPageToBeRenderedWithEmbedSsr(page);
|
||||
});
|
||||
|
||||
test("Org User Event(peer/meet) Page should be embeddable", async ({ page }) => {
|
||||
const response = await page.goto("https://i.cal.com/peer/meet/embed");
|
||||
test("Org User Event(/team-rick/test-event) Page should be embeddable", async ({ page }) => {
|
||||
const response = await page.goto("https://i.cal.com/team-rick/test-event/embed");
|
||||
expect(response?.status()).toBe(200);
|
||||
await expect(page.locator('[data-testid="decrementMonth"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="incrementMonth"]')).toBeVisible();
|
||||
|
||||
@@ -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.79",
|
||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
CAL_API_VERSION_HEADER,
|
||||
X_CAL_CLIENT_ID,
|
||||
X_CAL_SECRET_KEY,
|
||||
X_CAL_PLATFORM_EMBED,
|
||||
} from "@calcom/platform-constants";
|
||||
|
||||
import { TRPCExceptionFilter } from "./filters/trpc-exception.filter";
|
||||
@@ -45,6 +46,7 @@ export const bootstrap = (app: NestExpressApplication): NestExpressApplication =
|
||||
allowedHeaders: [
|
||||
X_CAL_CLIENT_ID,
|
||||
X_CAL_SECRET_KEY,
|
||||
X_CAL_PLATFORM_EMBED,
|
||||
CAL_API_VERSION_HEADER,
|
||||
"Accept",
|
||||
"Authorization",
|
||||
|
||||
@@ -37,7 +37,7 @@ import { Request } from "express";
|
||||
import { NextApiRequest } from "next/types";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { X_CAL_CLIENT_ID } from "@calcom/platform-constants";
|
||||
import { X_CAL_CLIENT_ID, X_CAL_PLATFORM_EMBED } from "@calcom/platform-constants";
|
||||
import { BOOKING_READ, SUCCESS_STATUS, BOOKING_WRITE } from "@calcom/platform-constants";
|
||||
import {
|
||||
handleNewRecurringBooking,
|
||||
@@ -161,14 +161,15 @@ export class BookingsController_2024_04_15 {
|
||||
async createBooking(
|
||||
@Req() req: BookingRequest,
|
||||
@Body() body: CreateBookingInput_2024_04_15,
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string,
|
||||
@Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string
|
||||
): Promise<ApiResponse<Partial<BookingResponse>>> {
|
||||
const oAuthClientId = clientId?.toString();
|
||||
const { orgSlug, locationUrl } = body;
|
||||
req.headers["x-cal-force-slug"] = orgSlug;
|
||||
try {
|
||||
const booking = await handleNewBooking(
|
||||
await this.createNextApiBookingRequest(req, oAuthClientId, locationUrl)
|
||||
await this.createNextApiBookingRequest(req, oAuthClientId, locationUrl, isEmbed)
|
||||
);
|
||||
if (booking.userId && booking.uid && booking.startTime) {
|
||||
void (await this.billingService.increaseUsageByUserId(booking.userId, {
|
||||
@@ -192,13 +193,16 @@ export class BookingsController_2024_04_15 {
|
||||
@Req() req: BookingRequest,
|
||||
@Param("bookingId") bookingId: string,
|
||||
@Body() _: CancelBookingInput_2024_04_15,
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string,
|
||||
@Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string
|
||||
): Promise<ApiResponse<{ bookingId: number; bookingUid: string; onlyRemovedAttendee: boolean }>> {
|
||||
const oAuthClientId = clientId?.toString();
|
||||
if (bookingId) {
|
||||
try {
|
||||
req.body.id = parseInt(bookingId);
|
||||
const res = await handleCancelBooking(await this.createNextApiBookingRequest(req, oAuthClientId));
|
||||
const res = await handleCancelBooking(
|
||||
await this.createNextApiBookingRequest(req, oAuthClientId, undefined, isEmbed)
|
||||
);
|
||||
if (!res.onlyRemovedAttendee) {
|
||||
void (await this.billingService.cancelUsageByBookingUid(res.bookingUid));
|
||||
}
|
||||
@@ -246,7 +250,8 @@ export class BookingsController_2024_04_15 {
|
||||
async createRecurringBooking(
|
||||
@Req() req: BookingRequest,
|
||||
@Body() _: CreateRecurringBookingInput_2024_04_15[],
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string,
|
||||
@Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string
|
||||
): Promise<ApiResponse<BookingResponse[]>> {
|
||||
const oAuthClientId = clientId?.toString();
|
||||
try {
|
||||
@@ -258,7 +263,7 @@ export class BookingsController_2024_04_15 {
|
||||
}
|
||||
|
||||
const createdBookings: BookingResponse[] = await handleNewRecurringBooking(
|
||||
await this.createNextApiRecurringBookingRequest(req, oAuthClientId)
|
||||
await this.createNextApiRecurringBookingRequest(req, oAuthClientId, undefined, isEmbed)
|
||||
);
|
||||
|
||||
createdBookings.forEach(async (booking) => {
|
||||
@@ -284,13 +289,14 @@ export class BookingsController_2024_04_15 {
|
||||
async createInstantBooking(
|
||||
@Req() req: BookingRequest,
|
||||
@Body() _: CreateBookingInput_2024_04_15,
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string,
|
||||
@Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string
|
||||
): Promise<ApiResponse<Awaited<ReturnType<typeof handleInstantMeeting>>>> {
|
||||
const oAuthClientId = clientId?.toString();
|
||||
req.userId = (await this.getOwnerId(req)) ?? -1;
|
||||
try {
|
||||
const instantMeeting = await handleInstantMeeting(
|
||||
await this.createNextApiBookingRequest(req, oAuthClientId)
|
||||
await this.createNextApiBookingRequest(req, oAuthClientId, undefined, isEmbed)
|
||||
);
|
||||
|
||||
if (instantMeeting.userId && instantMeeting.bookingUid) {
|
||||
@@ -332,8 +338,14 @@ export class BookingsController_2024_04_15 {
|
||||
}
|
||||
}
|
||||
|
||||
private async getOAuthClientsParams(clientId: string): Promise<OAuthRequestParams> {
|
||||
private async getOAuthClientsParams(clientId: string, isEmbed = false): Promise<OAuthRequestParams> {
|
||||
const res = { ...DEFAULT_PLATFORM_PARAMS };
|
||||
|
||||
if (isEmbed) {
|
||||
// embed should ignore oauth client settings and enable emails by default
|
||||
return { ...res, arePlatformEmailsEnabled: true };
|
||||
}
|
||||
|
||||
try {
|
||||
const client = await this.oAuthClientRepository.getOAuthClient(clientId);
|
||||
// fetch oAuthClient from db and use data stored in db to set these values
|
||||
@@ -354,13 +366,14 @@ export class BookingsController_2024_04_15 {
|
||||
private async createNextApiBookingRequest(
|
||||
req: BookingRequest,
|
||||
oAuthClientId?: string,
|
||||
platformBookingLocation?: string
|
||||
platformBookingLocation?: string,
|
||||
isEmbed?: string
|
||||
): Promise<NextApiRequest & { userId?: number } & OAuthRequestParams> {
|
||||
const requestId = req.get("X-Request-Id");
|
||||
const clone = { ...req };
|
||||
const userId = (await this.getOwnerId(req)) ?? -1;
|
||||
const oAuthParams = oAuthClientId
|
||||
? await this.getOAuthClientsParams(oAuthClientId)
|
||||
? await this.getOAuthClientsParams(oAuthClientId, this.transformToBoolean(isEmbed))
|
||||
: DEFAULT_PLATFORM_PARAMS;
|
||||
this.logger.log(`createNextApiBookingRequest_2024_04_15`, {
|
||||
requestId,
|
||||
@@ -377,12 +390,13 @@ export class BookingsController_2024_04_15 {
|
||||
private async createNextApiRecurringBookingRequest(
|
||||
req: BookingRequest,
|
||||
oAuthClientId?: string,
|
||||
platformBookingLocation?: string
|
||||
platformBookingLocation?: string,
|
||||
isEmbed?: string
|
||||
): Promise<NextApiRequest & { userId?: number } & OAuthRequestParams> {
|
||||
const clone = { ...req };
|
||||
const userId = (await this.getOwnerId(req)) ?? -1;
|
||||
const oAuthParams = oAuthClientId
|
||||
? await this.getOAuthClientsParams(oAuthClientId)
|
||||
? await this.getOAuthClientsParams(oAuthClientId, this.transformToBoolean(isEmbed))
|
||||
: DEFAULT_PLATFORM_PARAMS;
|
||||
const requestId = req.get("X-Request-Id");
|
||||
this.logger.log(`createNextApiRecurringBookingRequest_2024_04_15`, {
|
||||
@@ -424,4 +438,8 @@ export class BookingsController_2024_04_15 {
|
||||
|
||||
throw new InternalServerErrorException(errMsg);
|
||||
}
|
||||
|
||||
private transformToBoolean(v?: string): boolean {
|
||||
return v && typeof v === "string" ? v.toLowerCase() === "true" : false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { MarkAbsentBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/out
|
||||
import { ReassignBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reassign-booking.output";
|
||||
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
|
||||
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
||||
import { VERSION_2024_08_13_VALUE } from "@/lib/api-versions";
|
||||
import { VERSION_2024_08_13_VALUE, VERSION_2024_08_13 } from "@/lib/api-versions";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
@@ -65,6 +65,7 @@ import {
|
||||
@ApiHeader({
|
||||
name: "cal-api-version",
|
||||
description: `Must be set to \`2024-08-13\``,
|
||||
example: VERSION_2024_08_13,
|
||||
required: true,
|
||||
})
|
||||
export class BookingsController_2024_08_13 {
|
||||
|
||||
@@ -952,7 +952,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
slug: recurringSeatedTventTypeSlug,
|
||||
});
|
||||
expect(firstBooking.attendees.length).toEqual(2);
|
||||
expect(firstBooking.attendees[0]).toEqual({
|
||||
const firstAttendee = firstBooking.attendees.find(
|
||||
(attendee) => attendee.name === nameAttendeeOne
|
||||
);
|
||||
expect(firstAttendee).toEqual({
|
||||
name: nameAttendeeOne,
|
||||
email: emailAttendeeOne,
|
||||
timeZone: body.attendee.timeZone,
|
||||
@@ -965,7 +968,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
},
|
||||
metadata: createdRecurringSeatedBooking[0].attendees[0].metadata,
|
||||
});
|
||||
expect(firstBooking.attendees[1]).toEqual({
|
||||
const secondAttendee = firstBooking.attendees.find(
|
||||
(attendee) => attendee.name === nameAttendeeTwo
|
||||
);
|
||||
expect(secondAttendee).toEqual({
|
||||
name: nameAttendeeTwo,
|
||||
email: emailAttendeeTwo,
|
||||
timeZone: body.attendee.timeZone,
|
||||
|
||||
@@ -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(
|
||||
|
||||
+213
-66
@@ -33,11 +33,18 @@ import {
|
||||
ApiSuccessResponse,
|
||||
CreateEventTypeInput_2024_06_14,
|
||||
EventTypeOutput_2024_06_14,
|
||||
NameFieldInput_2024_06_14,
|
||||
NameDefaultFieldInput_2024_06_14,
|
||||
NotesDefaultFieldInput_2024_06_14,
|
||||
UpdateEventTypeInput_2024_06_14,
|
||||
} from "@calcom/platform-types";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
|
||||
const orderBySlug = (a: { slug: string }, b: { slug: string }) => {
|
||||
if (a.slug < b.slug) return -1;
|
||||
if (a.slug > b.slug) return 1;
|
||||
return 0;
|
||||
};
|
||||
|
||||
describe("Event types Endpoints", () => {
|
||||
describe("Not authenticated", () => {
|
||||
let app: INestApplication;
|
||||
@@ -93,6 +100,68 @@ describe("Event types Endpoints", () => {
|
||||
let orgUserEventType1: EventType;
|
||||
let orgUserEventType2: EventType;
|
||||
let orgUserEventType3: EventType;
|
||||
|
||||
const defaultResponseBookingFieldName = {
|
||||
isDefault: true,
|
||||
type: "name",
|
||||
slug: "name",
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
};
|
||||
|
||||
const defaultResponseBookingFieldEmail = {
|
||||
isDefault: true,
|
||||
type: "email",
|
||||
slug: "email",
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
};
|
||||
|
||||
const defaultResponseBookingFieldLocation = {
|
||||
isDefault: true,
|
||||
type: "radioInput",
|
||||
slug: "location",
|
||||
required: false,
|
||||
hidden: false,
|
||||
disableOnPrefill: false,
|
||||
};
|
||||
|
||||
const defaultResponseBookingFieldTitle = {
|
||||
isDefault: true,
|
||||
type: "text",
|
||||
slug: "title",
|
||||
required: true,
|
||||
hidden: true,
|
||||
disableOnPrefill: false,
|
||||
};
|
||||
|
||||
const defaultResponseBookingFieldNotes = {
|
||||
isDefault: true,
|
||||
type: "textarea",
|
||||
slug: "notes",
|
||||
required: false,
|
||||
hidden: false,
|
||||
disableOnPrefill: false,
|
||||
};
|
||||
|
||||
const defaultResponseBookingFieldGuests = {
|
||||
isDefault: true,
|
||||
type: "multiemail",
|
||||
slug: "guests",
|
||||
required: false,
|
||||
hidden: false,
|
||||
disableOnPrefill: false,
|
||||
};
|
||||
|
||||
const defaultResponseBookingFieldRescheduleReason = {
|
||||
isDefault: true,
|
||||
type: "textarea",
|
||||
slug: "rescheduleReason",
|
||||
required: false,
|
||||
hidden: false,
|
||||
disableOnPrefill: false,
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
@@ -253,7 +322,7 @@ describe("Event types Endpoints", () => {
|
||||
});
|
||||
|
||||
it("should create an event type", async () => {
|
||||
const nameBookingField: NameFieldInput_2024_06_14 = {
|
||||
const nameBookingField: NameDefaultFieldInput_2024_06_14 = {
|
||||
type: "name",
|
||||
label: "Your name sir / madam",
|
||||
placeholder: "john doe",
|
||||
@@ -291,6 +360,7 @@ describe("Event types Endpoints", () => {
|
||||
placeholder: "select language",
|
||||
options: ["javascript", "python", "cobol"],
|
||||
disableOnPrefill: true,
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
scheduleId: firstSchedule.id,
|
||||
@@ -378,16 +448,25 @@ describe("Event types Endpoints", () => {
|
||||
);
|
||||
expect(createdEventType.color).toEqual(body.color);
|
||||
|
||||
const requestBookingFields = body.bookingFields || [];
|
||||
const expectedBookingFields = [
|
||||
{ isDefault: true, required: true, slug: "name", ...nameBookingField },
|
||||
{ isDefault: true, required: true, slug: "email", type: "email" },
|
||||
// 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" },
|
||||
...requestBookingFields
|
||||
.filter((field) => field.type !== "name" && field.type !== "email")
|
||||
.map((field) => ({ isDefault: false, ...field })),
|
||||
{ ...defaultResponseBookingFieldName, ...nameBookingField },
|
||||
{ ...defaultResponseBookingFieldEmail },
|
||||
{ ...defaultResponseBookingFieldLocation },
|
||||
{ ...defaultResponseBookingFieldTitle },
|
||||
{ ...defaultResponseBookingFieldGuests },
|
||||
{ ...defaultResponseBookingFieldRescheduleReason },
|
||||
{
|
||||
type: "select",
|
||||
label: "select which language you want to learn",
|
||||
slug: "select-language",
|
||||
required: true,
|
||||
placeholder: "select language",
|
||||
options: ["javascript", "python", "cobol"],
|
||||
disableOnPrefill: true,
|
||||
hidden: false,
|
||||
isDefault: false,
|
||||
},
|
||||
{ ...defaultResponseBookingFieldNotes },
|
||||
];
|
||||
|
||||
expect(createdEventType.bookingFields).toEqual(expectedBookingFields);
|
||||
@@ -417,7 +496,9 @@ describe("Event types Endpoints", () => {
|
||||
expect(fetchedEventType?.lengthInMinutes).toEqual(eventType.lengthInMinutes);
|
||||
expect(fetchedEventType?.lengthInMinutesOptions).toEqual(eventType.lengthInMinutesOptions);
|
||||
expect(fetchedEventType?.locations).toEqual(eventType.locations);
|
||||
expect(fetchedEventType?.bookingFields).toEqual(eventType.bookingFields);
|
||||
expect(fetchedEventType?.bookingFields.sort(orderBySlug)).toEqual(
|
||||
eventType.bookingFields.sort(orderBySlug).filter((f) => ("hidden" in f ? !f?.hidden : true))
|
||||
);
|
||||
expect(fetchedEventType?.ownerId).toEqual(user.id);
|
||||
expect(fetchedEventType.bookingLimitsCount).toEqual(eventType.bookingLimitsCount);
|
||||
expect(fetchedEventType.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot);
|
||||
@@ -440,7 +521,6 @@ describe("Event types Endpoints", () => {
|
||||
});
|
||||
|
||||
it(`/GET/event-types by username and orgSlug`, async () => {
|
||||
console.log(organization);
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types?username=${orgUser.username}&orgSlug=${organization.slug}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
@@ -621,7 +701,6 @@ describe("Event types Endpoints", () => {
|
||||
.expect(201);
|
||||
|
||||
const createdEventType = createResponse.body.data;
|
||||
console.log("createdEventType: ", createdEventType);
|
||||
|
||||
expect(createdEventType).toMatchObject({
|
||||
id: expect.any(Number),
|
||||
@@ -764,13 +843,20 @@ describe("Event types Endpoints", () => {
|
||||
});
|
||||
|
||||
it("should update event type", async () => {
|
||||
const nameBookingField: NameFieldInput_2024_06_14 = {
|
||||
const nameBookingField: NameDefaultFieldInput_2024_06_14 = {
|
||||
type: "name",
|
||||
label: "Your name sir / madam",
|
||||
placeholder: "john doe",
|
||||
disableOnPrefill: true,
|
||||
};
|
||||
|
||||
const notesBookingField: NotesDefaultFieldInput_2024_06_14 = {
|
||||
slug: "notes",
|
||||
label: "lemme take some notes",
|
||||
placeholder: "write your notes here",
|
||||
required: true,
|
||||
};
|
||||
|
||||
const newTitle = "Coding class in Italian!";
|
||||
|
||||
const body: UpdateEventTypeInput_2024_06_14 = {
|
||||
@@ -787,7 +873,9 @@ describe("Event types Endpoints", () => {
|
||||
placeholder: "select language",
|
||||
options: ["javascript", "python", "cobol"],
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
notesBookingField,
|
||||
],
|
||||
bookingLimitsCount: {
|
||||
day: 4,
|
||||
@@ -850,14 +938,25 @@ describe("Event types Endpoints", () => {
|
||||
expect(updatedEventType.lengthInMinutes).toEqual(eventType.lengthInMinutes);
|
||||
expect(updatedEventType.locations).toEqual(eventType.locations);
|
||||
|
||||
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" },
|
||||
...requestBookingFields
|
||||
.filter((field) => field.type !== "name" && field.type !== "email")
|
||||
.map((field) => ({ isDefault: false, ...field })),
|
||||
{ ...defaultResponseBookingFieldName, ...nameBookingField },
|
||||
{ ...defaultResponseBookingFieldEmail },
|
||||
{ ...defaultResponseBookingFieldLocation },
|
||||
{ ...defaultResponseBookingFieldTitle },
|
||||
{ ...defaultResponseBookingFieldGuests },
|
||||
{ ...defaultResponseBookingFieldRescheduleReason },
|
||||
{
|
||||
type: "select",
|
||||
label: "select which language you want to learn",
|
||||
slug: "select-language",
|
||||
required: true,
|
||||
placeholder: "select language",
|
||||
options: ["javascript", "python", "cobol"],
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
isDefault: false,
|
||||
},
|
||||
{ ...defaultResponseBookingFieldNotes, ...notesBookingField },
|
||||
];
|
||||
|
||||
expect(updatedEventType.bookingFields).toEqual(expectedBookingFields);
|
||||
@@ -974,7 +1073,9 @@ describe("Event types Endpoints", () => {
|
||||
expect(fetchedEventType?.lengthInMinutes).toEqual(eventType.lengthInMinutes);
|
||||
expect(fetchedEventType?.lengthInMinutesOptions).toEqual(eventType.lengthInMinutesOptions);
|
||||
expect(fetchedEventType?.locations).toEqual(eventType.locations);
|
||||
expect(fetchedEventType?.bookingFields).toEqual(eventType.bookingFields);
|
||||
expect(fetchedEventType?.bookingFields.sort(orderBySlug)).toEqual(
|
||||
eventType.bookingFields.sort(orderBySlug).filter((f) => ("hidden" in f ? !f?.hidden : true))
|
||||
);
|
||||
expect(fetchedEventType?.ownerId).toEqual(user.id);
|
||||
expect(fetchedEventType.bookingLimitsCount).toEqual(eventType.bookingLimitsCount);
|
||||
expect(fetchedEventType.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot);
|
||||
@@ -1055,14 +1156,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: "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 },
|
||||
{
|
||||
isDefault: true,
|
||||
required: false,
|
||||
slug: "notes",
|
||||
type: "textarea",
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
];
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -1186,15 +1317,6 @@ describe("Event types Endpoints", () => {
|
||||
defaultLabel: "what_is_this_meeting_about",
|
||||
defaultPlaceholder: "",
|
||||
},
|
||||
{
|
||||
name: "notes",
|
||||
type: "textarea",
|
||||
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||
editable: "system-but-optional",
|
||||
required: false,
|
||||
defaultLabel: "additional_notes",
|
||||
defaultPlaceholder: "share_additional_notes",
|
||||
},
|
||||
{
|
||||
name: "guests",
|
||||
type: "multiemail",
|
||||
@@ -1230,6 +1352,15 @@ describe("Event types Endpoints", () => {
|
||||
required: false,
|
||||
defaultLabel: "phone_number",
|
||||
},
|
||||
{
|
||||
name: "notes",
|
||||
type: "textarea",
|
||||
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||
editable: "system-but-optional",
|
||||
required: false,
|
||||
defaultLabel: "additional_notes",
|
||||
defaultPlaceholder: "share_additional_notes",
|
||||
},
|
||||
],
|
||||
};
|
||||
const legacyEventType = await eventTypesRepositoryFixture.create(legacyEventTypeInput, user.id);
|
||||
@@ -1316,15 +1447,6 @@ describe("Event types Endpoints", () => {
|
||||
defaultLabel: "what_is_this_meeting_about",
|
||||
defaultPlaceholder: "",
|
||||
},
|
||||
{
|
||||
name: "notes",
|
||||
type: "textarea",
|
||||
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||
editable: "system-but-optional",
|
||||
required: false,
|
||||
defaultLabel: "additional_notes",
|
||||
defaultPlaceholder: "share_additional_notes",
|
||||
},
|
||||
{
|
||||
name: "guests",
|
||||
type: "multiemail",
|
||||
@@ -1360,6 +1482,15 @@ describe("Event types Endpoints", () => {
|
||||
required: false,
|
||||
defaultLabel: "phone_number",
|
||||
},
|
||||
{
|
||||
name: "notes",
|
||||
type: "textarea",
|
||||
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||
editable: "system-but-optional",
|
||||
required: false,
|
||||
defaultLabel: "additional_notes",
|
||||
defaultPlaceholder: "share_additional_notes",
|
||||
},
|
||||
],
|
||||
};
|
||||
const legacyEventType = await eventTypesRepositoryFixture.create(legacyEventTypeInput, user.id);
|
||||
@@ -1373,17 +1504,21 @@ describe("Event types Endpoints", () => {
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||
const fetchedEventType = responseBody.data;
|
||||
|
||||
expect(fetchedEventType.bookingFields).toEqual([
|
||||
...expectedReturnSystemFields,
|
||||
{
|
||||
isDefault: false,
|
||||
type: userDefinedBookingField.type,
|
||||
slug: userDefinedBookingField.name,
|
||||
label: userDefinedBookingField.label,
|
||||
required: userDefinedBookingField.required,
|
||||
placeholder: userDefinedBookingField.placeholder,
|
||||
},
|
||||
]);
|
||||
expect(fetchedEventType.bookingFields.sort(orderBySlug)).toEqual(
|
||||
[
|
||||
{
|
||||
isDefault: false,
|
||||
type: userDefinedBookingField.type,
|
||||
slug: userDefinedBookingField.name,
|
||||
label: userDefinedBookingField.label,
|
||||
required: userDefinedBookingField.required,
|
||||
placeholder: userDefinedBookingField.placeholder,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
...expectedReturnSystemFields,
|
||||
].sort(orderBySlug)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1461,42 +1596,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,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "textarea",
|
||||
slug: "notes",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "multiemail",
|
||||
slug: "guests",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "textarea",
|
||||
slug: "rescheduleReason",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "textarea",
|
||||
slug: "notes",
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+36
-14
@@ -12,9 +12,12 @@ import {
|
||||
transformRecurrenceApiToInternal,
|
||||
systemBeforeFieldName,
|
||||
systemBeforeFieldEmail,
|
||||
systemBeforeFieldLocation,
|
||||
systemAfterFieldTitle,
|
||||
systemAfterFieldNotes,
|
||||
systemAfterFieldGuests,
|
||||
systemAfterFieldRescheduleReason,
|
||||
EventTypeMetaDataSchema,
|
||||
systemBeforeFieldLocation,
|
||||
transformBookerLayoutsApiToInternal,
|
||||
transformConfirmationPolicyApiToInternal,
|
||||
transformEventColorsApiToInternal,
|
||||
@@ -217,27 +220,46 @@ export class InputEventTypesService_2024_06_14 {
|
||||
inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"],
|
||||
hasMultipleLocations: boolean
|
||||
) {
|
||||
const customFields: (SystemField | CustomField)[] = inputBookingFields
|
||||
const internalFields: (SystemField | CustomField)[] = inputBookingFields
|
||||
? transformBookingFieldsApiToInternal(inputBookingFields)
|
||||
: [];
|
||||
const customFieldsWithoutNameEmail = customFields.filter(
|
||||
(field) => field.type !== "name" && field.type !== "email"
|
||||
const systemCustomFields = internalFields.filter((field) => !this.isUserCustomField(field));
|
||||
const userCustomFields = internalFields.filter((field) => this.isUserCustomField(field));
|
||||
|
||||
const systemCustomNameField = systemCustomFields?.find((field) => field.type === "name");
|
||||
const systemCustomEmailField = systemCustomFields?.find((field) => field.type === "email");
|
||||
const systemCustomTitleField = systemCustomFields?.find((field) => field.name === "title");
|
||||
const systemCustomNotesField = systemCustomFields?.find((field) => field.name === "notes");
|
||||
const systemCustomGuestsField = systemCustomFields?.find((field) => field.name === "guests");
|
||||
const systemCustomRescheduleReasonField = systemCustomFields?.find(
|
||||
(field) => field.name === "rescheduleReason"
|
||||
);
|
||||
const customNameField = customFields?.find((field) => field.type === "name");
|
||||
const customEmailField = customFields?.find((field) => field.type === "email");
|
||||
|
||||
const defaultFieldsBefore: (SystemField | CustomField)[] = [
|
||||
customNameField || systemBeforeFieldName,
|
||||
customEmailField || systemBeforeFieldEmail,
|
||||
systemCustomNameField || systemBeforeFieldName,
|
||||
systemCustomEmailField || systemBeforeFieldEmail,
|
||||
systemBeforeFieldLocation,
|
||||
];
|
||||
// note(Lauris): if event type has multiple locations then a radio button booking field has to be displayed to allow booker to pick location
|
||||
if (hasMultipleLocations) {
|
||||
defaultFieldsBefore.push(systemBeforeFieldLocation);
|
||||
}
|
||||
|
||||
const defaultFieldsAfter = [systemAfterFieldRescheduleReason];
|
||||
const defaultFieldsAfter = [
|
||||
systemCustomTitleField || systemAfterFieldTitle,
|
||||
systemCustomNotesField || systemAfterFieldNotes,
|
||||
systemCustomGuestsField || systemAfterFieldGuests,
|
||||
systemCustomRescheduleReasonField || systemAfterFieldRescheduleReason,
|
||||
];
|
||||
|
||||
return [...defaultFieldsBefore, ...customFieldsWithoutNameEmail, ...defaultFieldsAfter];
|
||||
return [...defaultFieldsBefore, ...userCustomFields, ...defaultFieldsAfter];
|
||||
}
|
||||
|
||||
isUserCustomField(field: SystemField | CustomField): field is CustomField {
|
||||
return (
|
||||
field.type !== "name" &&
|
||||
field.type !== "email" &&
|
||||
field.name !== "title" &&
|
||||
field.name !== "notes" &&
|
||||
field.name !== "guests" &&
|
||||
field.name !== "rescheduleReason"
|
||||
);
|
||||
}
|
||||
|
||||
transformInputIntervalLimits(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingLimitsCount"]) {
|
||||
|
||||
+13
-1
@@ -242,7 +242,19 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
}
|
||||
}
|
||||
|
||||
return [...transformBookingFieldsInternalToApi(knownBookingFields), ...unknownBookingFields];
|
||||
const fields = [...transformBookingFieldsInternalToApi(knownBookingFields), ...unknownBookingFields];
|
||||
|
||||
// ensure additional notes are always at the end
|
||||
return fields.sort((a, b) => {
|
||||
if (!a?.slug || !b?.slug) return 0;
|
||||
if (a.slug === "notes" && b.slug !== "notes") {
|
||||
return 1;
|
||||
}
|
||||
if (b.slug === "notes" && a.slug !== "notes") {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
getDefaultBookingFields(isOrgTeamEvent: boolean) {
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ export class OutputSchedulesService_2024_06_11 {
|
||||
|
||||
async getResponseSchedule(databaseSchedule: Schedule & { availability: Availability[] }) {
|
||||
if (!databaseSchedule.timeZone) {
|
||||
throw new Error("Failed to create schedule because its timezone is not set.");
|
||||
databaseSchedule.timeZone = "Europe/London";
|
||||
}
|
||||
|
||||
const ownerDefaultScheduleId = await this.usersRepository.getUserScheduleDefaultId(
|
||||
|
||||
@@ -41,6 +41,18 @@ describe("isOriginAllowed", () => {
|
||||
const origin = "https://sub.mydomain.com/path/to/resource";
|
||||
expect(isOriginAllowed(origin, allowedOrigins)).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle patterns with wildcard for routes correctly", () => {
|
||||
const allowedOrigins = ["https://domain.com/*"];
|
||||
const origin = "https://domain.com/dashboard";
|
||||
expect(isOriginAllowed(origin, allowedOrigins)).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle patterns with wildcard for root route correctly", () => {
|
||||
const allowedOrigins = ["https://domain.com*"];
|
||||
const origin = "https://domain.com";
|
||||
expect(isOriginAllowed(origin, allowedOrigins)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("is not allowed", () => {
|
||||
|
||||
@@ -179,7 +179,7 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
|
||||
if (origin && !isOriginAllowed(origin, client.redirectUris)) {
|
||||
throw new UnauthorizedException(
|
||||
`Invalid request origin - please open https://app.cal.com/settings/platform and add the origin '${origin}' to the 'Redirect uris' of your OAuth client.`
|
||||
`Invalid request origin - please open https://app.cal.com/settings/platform and add the origin '${origin}' to the 'Redirect uris' of your OAuth client with ID '${client.id}'`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+24
-29
@@ -677,53 +677,48 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
const fetchedEventType = responseBody.data;
|
||||
|
||||
expect(fetchedEventType.bookingFields).toEqual([
|
||||
{
|
||||
isDefault: true,
|
||||
type: "name",
|
||||
slug: "name",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "email",
|
||||
slug: "email",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
required: false,
|
||||
slug: "attendeePhoneNumber",
|
||||
type: "phone",
|
||||
},
|
||||
{ isDefault: true, required: true, slug: "name", type: "name", disableOnPrefill: false },
|
||||
{ isDefault: true, required: true, slug: "email", type: "email", disableOnPrefill: false },
|
||||
{ isDefault: true, 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,
|
||||
slug: "title",
|
||||
type: "text",
|
||||
disableOnPrefill: false,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "textarea",
|
||||
slug: "notes",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "multiemail",
|
||||
slug: "guests",
|
||||
required: false,
|
||||
type: "multiemail",
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "textarea",
|
||||
slug: "rescheduleReason",
|
||||
required: false,
|
||||
slug: "rescheduleReason",
|
||||
type: "textarea",
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
required: false,
|
||||
slug: "notes",
|
||||
type: "textarea",
|
||||
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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,490 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.module";
|
||||
import { SchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/schedules.service";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { SlotsModule } from "@/modules/slots/slots.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { SelectedSlotsRepositoryFixture } from "test/fixtures/repository/selected-slots.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { randomNumber } from "test/utils/randomNumber";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
const expectedSlotsUTC = {
|
||||
slots: {
|
||||
"2050-09-05": [
|
||||
{ time: "2050-09-05T07:00:00.000Z" },
|
||||
{ time: "2050-09-05T08:00:00.000Z" },
|
||||
{ time: "2050-09-05T09:00:00.000Z" },
|
||||
{ time: "2050-09-05T10:00:00.000Z" },
|
||||
{ time: "2050-09-05T11:00:00.000Z" },
|
||||
{ time: "2050-09-05T12:00:00.000Z" },
|
||||
{ time: "2050-09-05T13:00:00.000Z" },
|
||||
{ time: "2050-09-05T14:00:00.000Z" },
|
||||
],
|
||||
"2050-09-06": [
|
||||
{ time: "2050-09-06T07:00:00.000Z" },
|
||||
{ time: "2050-09-06T08:00:00.000Z" },
|
||||
{ time: "2050-09-06T09:00:00.000Z" },
|
||||
{ time: "2050-09-06T10:00:00.000Z" },
|
||||
{ time: "2050-09-06T11:00:00.000Z" },
|
||||
{ time: "2050-09-06T12:00:00.000Z" },
|
||||
{ time: "2050-09-06T13:00:00.000Z" },
|
||||
{ time: "2050-09-06T14:00:00.000Z" },
|
||||
],
|
||||
"2050-09-07": [
|
||||
{ time: "2050-09-07T07:00:00.000Z" },
|
||||
{ time: "2050-09-07T08:00:00.000Z" },
|
||||
{ time: "2050-09-07T09:00:00.000Z" },
|
||||
{ time: "2050-09-07T10:00:00.000Z" },
|
||||
{ time: "2050-09-07T11:00:00.000Z" },
|
||||
{ time: "2050-09-07T12:00:00.000Z" },
|
||||
{ time: "2050-09-07T13:00:00.000Z" },
|
||||
{ time: "2050-09-07T14:00:00.000Z" },
|
||||
],
|
||||
"2050-09-08": [
|
||||
{ time: "2050-09-08T07:00:00.000Z" },
|
||||
{ time: "2050-09-08T08:00:00.000Z" },
|
||||
{ time: "2050-09-08T09:00:00.000Z" },
|
||||
{ time: "2050-09-08T10:00:00.000Z" },
|
||||
{ time: "2050-09-08T11:00:00.000Z" },
|
||||
{ time: "2050-09-08T12:00:00.000Z" },
|
||||
{ time: "2050-09-08T13:00:00.000Z" },
|
||||
{ time: "2050-09-08T14:00:00.000Z" },
|
||||
],
|
||||
"2050-09-09": [
|
||||
{ time: "2050-09-09T07:00:00.000Z" },
|
||||
{ time: "2050-09-09T08:00:00.000Z" },
|
||||
{ time: "2050-09-09T09:00:00.000Z" },
|
||||
{ time: "2050-09-09T10:00:00.000Z" },
|
||||
{ time: "2050-09-09T11:00:00.000Z" },
|
||||
{ time: "2050-09-09T12:00:00.000Z" },
|
||||
{ time: "2050-09-09T13:00:00.000Z" },
|
||||
{ time: "2050-09-09T14:00:00.000Z" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const expectedSlotsRome = {
|
||||
slots: {
|
||||
"2050-09-05": [
|
||||
{ time: "2050-09-05T09:00:00.000+02:00" },
|
||||
{ time: "2050-09-05T10:00:00.000+02:00" },
|
||||
{ time: "2050-09-05T11:00:00.000+02:00" },
|
||||
{ time: "2050-09-05T12:00:00.000+02:00" },
|
||||
{ time: "2050-09-05T13:00:00.000+02:00" },
|
||||
{ time: "2050-09-05T14:00:00.000+02:00" },
|
||||
{ time: "2050-09-05T15:00:00.000+02:00" },
|
||||
{ time: "2050-09-05T16:00:00.000+02:00" },
|
||||
],
|
||||
"2050-09-06": [
|
||||
{ time: "2050-09-06T09:00:00.000+02:00" },
|
||||
{ time: "2050-09-06T10:00:00.000+02:00" },
|
||||
{ time: "2050-09-06T11:00:00.000+02:00" },
|
||||
{ time: "2050-09-06T12:00:00.000+02:00" },
|
||||
{ time: "2050-09-06T13:00:00.000+02:00" },
|
||||
{ time: "2050-09-06T14:00:00.000+02:00" },
|
||||
{ time: "2050-09-06T15:00:00.000+02:00" },
|
||||
{ time: "2050-09-06T16:00:00.000+02:00" },
|
||||
],
|
||||
"2050-09-07": [
|
||||
{ time: "2050-09-07T09:00:00.000+02:00" },
|
||||
{ time: "2050-09-07T10:00:00.000+02:00" },
|
||||
{ time: "2050-09-07T11:00:00.000+02:00" },
|
||||
{ time: "2050-09-07T12:00:00.000+02:00" },
|
||||
{ time: "2050-09-07T13:00:00.000+02:00" },
|
||||
{ time: "2050-09-07T14:00:00.000+02:00" },
|
||||
{ time: "2050-09-07T15:00:00.000+02:00" },
|
||||
{ time: "2050-09-07T16:00:00.000+02:00" },
|
||||
],
|
||||
"2050-09-08": [
|
||||
{ time: "2050-09-08T09:00:00.000+02:00" },
|
||||
{ time: "2050-09-08T10:00:00.000+02:00" },
|
||||
{ time: "2050-09-08T11:00:00.000+02:00" },
|
||||
{ time: "2050-09-08T12:00:00.000+02:00" },
|
||||
{ time: "2050-09-08T13:00:00.000+02:00" },
|
||||
{ time: "2050-09-08T14:00:00.000+02:00" },
|
||||
{ time: "2050-09-08T15:00:00.000+02:00" },
|
||||
{ time: "2050-09-08T16:00:00.000+02:00" },
|
||||
],
|
||||
"2050-09-09": [
|
||||
{ time: "2050-09-09T09:00:00.000+02:00" },
|
||||
{ time: "2050-09-09T10:00:00.000+02:00" },
|
||||
{ time: "2050-09-09T11:00:00.000+02:00" },
|
||||
{ time: "2050-09-09T12:00:00.000+02:00" },
|
||||
{ time: "2050-09-09T13:00:00.000+02:00" },
|
||||
{ time: "2050-09-09T14:00:00.000+02:00" },
|
||||
{ time: "2050-09-09T15:00:00.000+02:00" },
|
||||
{ time: "2050-09-09T16:00:00.000+02:00" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const expectedSlotsUTCRange = {
|
||||
slots: {
|
||||
"2050-09-05": [
|
||||
{ startTime: "2050-09-05T07:00:00.000Z", endTime: "2050-09-05T08:00:00.000Z" },
|
||||
{ startTime: "2050-09-05T08:00:00.000Z", endTime: "2050-09-05T09:00:00.000Z" },
|
||||
{ startTime: "2050-09-05T09:00:00.000Z", endTime: "2050-09-05T10:00:00.000Z" },
|
||||
{ startTime: "2050-09-05T10:00:00.000Z", endTime: "2050-09-05T11:00:00.000Z" },
|
||||
{ startTime: "2050-09-05T11:00:00.000Z", endTime: "2050-09-05T12:00:00.000Z" },
|
||||
{ startTime: "2050-09-05T12:00:00.000Z", endTime: "2050-09-05T13:00:00.000Z" },
|
||||
{ startTime: "2050-09-05T13:00:00.000Z", endTime: "2050-09-05T14:00:00.000Z" },
|
||||
{ startTime: "2050-09-05T14:00:00.000Z", endTime: "2050-09-05T15:00:00.000Z" },
|
||||
],
|
||||
"2050-09-06": [
|
||||
{ startTime: "2050-09-06T07:00:00.000Z", endTime: "2050-09-06T08:00:00.000Z" },
|
||||
{ startTime: "2050-09-06T08:00:00.000Z", endTime: "2050-09-06T09:00:00.000Z" },
|
||||
{ startTime: "2050-09-06T09:00:00.000Z", endTime: "2050-09-06T10:00:00.000Z" },
|
||||
{ startTime: "2050-09-06T10:00:00.000Z", endTime: "2050-09-06T11:00:00.000Z" },
|
||||
{ startTime: "2050-09-06T11:00:00.000Z", endTime: "2050-09-06T12:00:00.000Z" },
|
||||
{ startTime: "2050-09-06T12:00:00.000Z", endTime: "2050-09-06T13:00:00.000Z" },
|
||||
{ startTime: "2050-09-06T13:00:00.000Z", endTime: "2050-09-06T14:00:00.000Z" },
|
||||
{ startTime: "2050-09-06T14:00:00.000Z", endTime: "2050-09-06T15:00:00.000Z" },
|
||||
],
|
||||
"2050-09-07": [
|
||||
{ startTime: "2050-09-07T07:00:00.000Z", endTime: "2050-09-07T08:00:00.000Z" },
|
||||
{ startTime: "2050-09-07T08:00:00.000Z", endTime: "2050-09-07T09:00:00.000Z" },
|
||||
{ startTime: "2050-09-07T09:00:00.000Z", endTime: "2050-09-07T10:00:00.000Z" },
|
||||
{ startTime: "2050-09-07T10:00:00.000Z", endTime: "2050-09-07T11:00:00.000Z" },
|
||||
{ startTime: "2050-09-07T11:00:00.000Z", endTime: "2050-09-07T12:00:00.000Z" },
|
||||
{ startTime: "2050-09-07T12:00:00.000Z", endTime: "2050-09-07T13:00:00.000Z" },
|
||||
{ startTime: "2050-09-07T13:00:00.000Z", endTime: "2050-09-07T14:00:00.000Z" },
|
||||
{ startTime: "2050-09-07T14:00:00.000Z", endTime: "2050-09-07T15:00:00.000Z" },
|
||||
],
|
||||
"2050-09-08": [
|
||||
{ startTime: "2050-09-08T07:00:00.000Z", endTime: "2050-09-08T08:00:00.000Z" },
|
||||
{ startTime: "2050-09-08T08:00:00.000Z", endTime: "2050-09-08T09:00:00.000Z" },
|
||||
{ startTime: "2050-09-08T09:00:00.000Z", endTime: "2050-09-08T10:00:00.000Z" },
|
||||
{ startTime: "2050-09-08T10:00:00.000Z", endTime: "2050-09-08T11:00:00.000Z" },
|
||||
{ startTime: "2050-09-08T11:00:00.000Z", endTime: "2050-09-08T12:00:00.000Z" },
|
||||
{ startTime: "2050-09-08T12:00:00.000Z", endTime: "2050-09-08T13:00:00.000Z" },
|
||||
{ startTime: "2050-09-08T13:00:00.000Z", endTime: "2050-09-08T14:00:00.000Z" },
|
||||
{ startTime: "2050-09-08T14:00:00.000Z", endTime: "2050-09-08T15:00:00.000Z" },
|
||||
],
|
||||
"2050-09-09": [
|
||||
{ startTime: "2050-09-09T07:00:00.000Z", endTime: "2050-09-09T08:00:00.000Z" },
|
||||
{ startTime: "2050-09-09T08:00:00.000Z", endTime: "2050-09-09T09:00:00.000Z" },
|
||||
{ startTime: "2050-09-09T09:00:00.000Z", endTime: "2050-09-09T10:00:00.000Z" },
|
||||
{ startTime: "2050-09-09T10:00:00.000Z", endTime: "2050-09-09T11:00:00.000Z" },
|
||||
{ startTime: "2050-09-09T11:00:00.000Z", endTime: "2050-09-09T12:00:00.000Z" },
|
||||
{ startTime: "2050-09-09T12:00:00.000Z", endTime: "2050-09-09T13:00:00.000Z" },
|
||||
{ startTime: "2050-09-09T13:00:00.000Z", endTime: "2050-09-09T14:00:00.000Z" },
|
||||
{ startTime: "2050-09-09T14:00:00.000Z", endTime: "2050-09-09T15:00:00.000Z" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const expectedSlotsRomeRange = {
|
||||
slots: {
|
||||
"2050-09-05": [
|
||||
{ startTime: "2050-09-05T09:00:00.000+02:00", endTime: "2050-09-05T10:00:00.000+02:00" },
|
||||
{ startTime: "2050-09-05T10:00:00.000+02:00", endTime: "2050-09-05T11:00:00.000+02:00" },
|
||||
{ startTime: "2050-09-05T11:00:00.000+02:00", endTime: "2050-09-05T12:00:00.000+02:00" },
|
||||
{ startTime: "2050-09-05T12:00:00.000+02:00", endTime: "2050-09-05T13:00:00.000+02:00" },
|
||||
{ startTime: "2050-09-05T13:00:00.000+02:00", endTime: "2050-09-05T14:00:00.000+02:00" },
|
||||
{ startTime: "2050-09-05T14:00:00.000+02:00", endTime: "2050-09-05T15:00:00.000+02:00" },
|
||||
{ startTime: "2050-09-05T15:00:00.000+02:00", endTime: "2050-09-05T16:00:00.000+02:00" },
|
||||
{ startTime: "2050-09-05T16:00:00.000+02:00", endTime: "2050-09-05T17:00:00.000+02:00" },
|
||||
],
|
||||
"2050-09-06": [
|
||||
{ startTime: "2050-09-06T09:00:00.000+02:00", endTime: "2050-09-06T10:00:00.000+02:00" },
|
||||
{ startTime: "2050-09-06T10:00:00.000+02:00", endTime: "2050-09-06T11:00:00.000+02:00" },
|
||||
{ startTime: "2050-09-06T11:00:00.000+02:00", endTime: "2050-09-06T12:00:00.000+02:00" },
|
||||
{ startTime: "2050-09-06T12:00:00.000+02:00", endTime: "2050-09-06T13:00:00.000+02:00" },
|
||||
{ startTime: "2050-09-06T13:00:00.000+02:00", endTime: "2050-09-06T14:00:00.000+02:00" },
|
||||
{ startTime: "2050-09-06T14:00:00.000+02:00", endTime: "2050-09-06T15:00:00.000+02:00" },
|
||||
{ startTime: "2050-09-06T15:00:00.000+02:00", endTime: "2050-09-06T16:00:00.000+02:00" },
|
||||
{ startTime: "2050-09-06T16:00:00.000+02:00", endTime: "2050-09-06T17:00:00.000+02:00" },
|
||||
],
|
||||
"2050-09-07": [
|
||||
{ startTime: "2050-09-07T09:00:00.000+02:00", endTime: "2050-09-07T10:00:00.000+02:00" },
|
||||
{ startTime: "2050-09-07T10:00:00.000+02:00", endTime: "2050-09-07T11:00:00.000+02:00" },
|
||||
{ startTime: "2050-09-07T11:00:00.000+02:00", endTime: "2050-09-07T12:00:00.000+02:00" },
|
||||
{ startTime: "2050-09-07T12:00:00.000+02:00", endTime: "2050-09-07T13:00:00.000+02:00" },
|
||||
{ startTime: "2050-09-07T13:00:00.000+02:00", endTime: "2050-09-07T14:00:00.000+02:00" },
|
||||
{ startTime: "2050-09-07T14:00:00.000+02:00", endTime: "2050-09-07T15:00:00.000+02:00" },
|
||||
{ startTime: "2050-09-07T15:00:00.000+02:00", endTime: "2050-09-07T16:00:00.000+02:00" },
|
||||
{ startTime: "2050-09-07T16:00:00.000+02:00", endTime: "2050-09-07T17:00:00.000+02:00" },
|
||||
],
|
||||
"2050-09-08": [
|
||||
{ startTime: "2050-09-08T09:00:00.000+02:00", endTime: "2050-09-08T10:00:00.000+02:00" },
|
||||
{ startTime: "2050-09-08T10:00:00.000+02:00", endTime: "2050-09-08T11:00:00.000+02:00" },
|
||||
{ startTime: "2050-09-08T11:00:00.000+02:00", endTime: "2050-09-08T12:00:00.000+02:00" },
|
||||
{ startTime: "2050-09-08T12:00:00.000+02:00", endTime: "2050-09-08T13:00:00.000+02:00" },
|
||||
{ startTime: "2050-09-08T13:00:00.000+02:00", endTime: "2050-09-08T14:00:00.000+02:00" },
|
||||
{ startTime: "2050-09-08T14:00:00.000+02:00", endTime: "2050-09-08T15:00:00.000+02:00" },
|
||||
{ startTime: "2050-09-08T15:00:00.000+02:00", endTime: "2050-09-08T16:00:00.000+02:00" },
|
||||
{ startTime: "2050-09-08T16:00:00.000+02:00", endTime: "2050-09-08T17:00:00.000+02:00" },
|
||||
],
|
||||
"2050-09-09": [
|
||||
{ startTime: "2050-09-09T09:00:00.000+02:00", endTime: "2050-09-09T10:00:00.000+02:00" },
|
||||
{ startTime: "2050-09-09T10:00:00.000+02:00", endTime: "2050-09-09T11:00:00.000+02:00" },
|
||||
{ startTime: "2050-09-09T11:00:00.000+02:00", endTime: "2050-09-09T12:00:00.000+02:00" },
|
||||
{ startTime: "2050-09-09T12:00:00.000+02:00", endTime: "2050-09-09T13:00:00.000+02:00" },
|
||||
{ startTime: "2050-09-09T13:00:00.000+02:00", endTime: "2050-09-09T14:00:00.000+02:00" },
|
||||
{ startTime: "2050-09-09T14:00:00.000+02:00", endTime: "2050-09-09T15:00:00.000+02:00" },
|
||||
{ startTime: "2050-09-09T15:00:00.000+02:00", endTime: "2050-09-09T16:00:00.000+02:00" },
|
||||
{ startTime: "2050-09-09T16:00:00.000+02:00", endTime: "2050-09-09T17:00:00.000+02:00" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
describe("Slots Endpoints", () => {
|
||||
describe("Individual user slots", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let schedulesService: SchedulesService_2024_06_11;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let selectedSlotsRepositoryFixture: SelectedSlotsRepositoryFixture;
|
||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||
|
||||
const userEmail = `slots-${randomNumber()}-controller-e2e@api.com`;
|
||||
const userName = "bob";
|
||||
let user: User;
|
||||
let eventTypeId: number;
|
||||
let eventTypeSlug: string;
|
||||
let reservedSlotUid: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [
|
||||
AppModule,
|
||||
PrismaModule,
|
||||
UsersModule,
|
||||
TokensModule,
|
||||
SchedulesModule_2024_06_11,
|
||||
SlotsModule,
|
||||
],
|
||||
})
|
||||
)
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
schedulesService = moduleRef.get<SchedulesService_2024_06_11>(SchedulesService_2024_06_11);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
selectedSlotsRepositoryFixture = new SelectedSlotsRepositoryFixture(moduleRef);
|
||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
name: "bob slot",
|
||||
username: userName,
|
||||
});
|
||||
|
||||
// nxte(Lauris): this creates default schedule monday to friday from 9AM to 5PM in Europe/Rome timezone
|
||||
await schedulesService.createUserDefaultSchedule(user.id, "Europe/Rome");
|
||||
|
||||
const event = await eventTypesRepositoryFixture.create(
|
||||
{ title: "frisbee match", slug: "frisbee-match", length: 60 },
|
||||
user.id
|
||||
);
|
||||
eventTypeId = event.id;
|
||||
eventTypeSlug = event.slug;
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it("should get slots in UTC by event type id", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/api/v2/slots/available?eventTypeId=${eventTypeId}&startTime=2050-09-05&endTime=2050-09-10`)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
expect(res.body).toEqual({
|
||||
status: SUCCESS_STATUS,
|
||||
data: expectedSlotsUTC,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should get slots in specified time zone by event type id", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(
|
||||
`/api/v2/slots/available?eventTypeId=${eventTypeId}&startTime=2050-09-05&endTime=2050-09-10&timeZone=Europe/Rome`
|
||||
)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
expect(res.body).toEqual({
|
||||
status: SUCCESS_STATUS,
|
||||
data: expectedSlotsRome,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should get slots in UTC by event type id in range format", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(
|
||||
`/api/v2/slots/available?eventTypeId=${eventTypeId}&startTime=2050-09-05&endTime=2050-09-10&slotFormat=range`
|
||||
)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
expect(res.body).toEqual({
|
||||
status: SUCCESS_STATUS,
|
||||
data: expectedSlotsUTCRange,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should get slots in specified time zone by event type id in range format", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(
|
||||
`/api/v2/slots/available?eventTypeId=${eventTypeId}&startTime=2050-09-05&endTime=2050-09-10&timeZone=Europe/Rome&slotFormat=range`
|
||||
)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
expect(res.body).toEqual({
|
||||
status: SUCCESS_STATUS,
|
||||
data: expectedSlotsRomeRange,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should get slots in UTC by event type slug", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(
|
||||
`/api/v2/slots/available?eventTypeSlug=${eventTypeSlug}&usernameList[]=${userName}&startTime=2050-09-05&endTime=2050-09-10`
|
||||
)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
expect(res.body).toEqual({
|
||||
status: SUCCESS_STATUS,
|
||||
data: expectedSlotsUTC,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should get slots in specified time zone by event type slug", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(
|
||||
`/api/v2/slots/available?eventTypeSlug=${eventTypeSlug}&usernameList[]=${userName}&startTime=2050-09-05&endTime=2050-09-10&timeZone=Europe/Rome`
|
||||
)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
expect(res.body).toEqual({
|
||||
status: SUCCESS_STATUS,
|
||||
data: expectedSlotsRome,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should reserve a slot and it should not appear in available slots", async () => {
|
||||
const slotStartTime = "2050-09-05T10:00:00.000Z";
|
||||
const reserveResponse = await request(app.getHttpServer())
|
||||
.post(`/api/v2/slots/reserve`)
|
||||
.send({
|
||||
eventTypeId,
|
||||
slotUtcStartDate: slotStartTime,
|
||||
slotUtcEndDate: "2050-09-05T11:00:00.000Z",
|
||||
})
|
||||
.expect(201);
|
||||
|
||||
const reserveResponseBody = reserveResponse.body;
|
||||
expect(reserveResponseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const uid: string = reserveResponseBody.data;
|
||||
expect(uid).toBeDefined();
|
||||
if (!uid) {
|
||||
throw new Error("Reserved slot uid is undefined");
|
||||
}
|
||||
reservedSlotUid = uid;
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/slots/available?eventTypeId=${eventTypeId}&startTime=2050-09-05&endTime=2050-09-10`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const slots = responseBody.data;
|
||||
|
||||
expect(slots).toBeDefined();
|
||||
const days = Object.keys(slots.slots);
|
||||
expect(days.length).toEqual(5);
|
||||
|
||||
const expectedSlotsUTC2050_09_05 = expectedSlotsUTC.slots["2050-09-05"].filter(
|
||||
(slot) => slot.time !== slotStartTime
|
||||
);
|
||||
expect(slots).toEqual({
|
||||
slots: { ...expectedSlotsUTC.slots, "2050-09-05": expectedSlotsUTC2050_09_05 },
|
||||
});
|
||||
});
|
||||
|
||||
it("should delete reserved slot", async () => {
|
||||
await request(app.getHttpServer())
|
||||
.delete(`/api/v2/slots/selected-slot?uid=${reservedSlotUid}`)
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it("should do a booking and slot should not be available at that time", async () => {
|
||||
const startTime = "2050-09-05T11:00:00.000Z";
|
||||
await bookingsRepositoryFixture.create({
|
||||
uid: `booking-uid-${eventTypeId}`,
|
||||
title: "booking title",
|
||||
startTime,
|
||||
endTime: "2050-09-05T12:00:00.000Z",
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeId,
|
||||
},
|
||||
},
|
||||
metadata: {},
|
||||
responses: {
|
||||
name: "tester",
|
||||
email: "tester@example.com",
|
||||
guests: [],
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/slots/available?eventTypeId=${eventTypeId}&startTime=2050-09-05&endTime=2050-09-10`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const slots = responseBody.data;
|
||||
|
||||
expect(slots).toBeDefined();
|
||||
const days = Object.keys(slots.slots);
|
||||
expect(days.length).toEqual(5);
|
||||
|
||||
const expectedSlotsUTC2050_09_05 = expectedSlotsUTC.slots["2050-09-05"].filter(
|
||||
(slot) => slot.time !== startTime
|
||||
);
|
||||
expect(slots).toEqual({
|
||||
slots: { ...expectedSlotsUTC.slots, "2050-09-05": expectedSlotsUTC2050_09_05 },
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await selectedSlotsRepositoryFixture.deleteByUId(reservedSlotUid);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
|
||||
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,11 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { SlotsOutputService } from "@/modules/slots/services/slots-output.service";
|
||||
import { SlotsService } from "@/modules/slots/services/slots.service";
|
||||
import { Query, Body, Controller, Get, Delete, Post, Req, Res } from "@nestjs/common";
|
||||
import { ApiTags as DocsTags, ApiCreatedResponse, ApiOkResponse, ApiOperation } from "@nestjs/swagger";
|
||||
import { Response as ExpressResponse, Request as ExpressRequest } from "express";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { SlotFormat } from "@calcom/platform-enums";
|
||||
import { getAvailableSlots } from "@calcom/platform-libraries";
|
||||
import type { AvailableSlotsType } from "@calcom/platform-libraries";
|
||||
import { RemoveSelectedSlotInput, ReserveSlotInput } from "@calcom/platform-types";
|
||||
@@ -17,7 +17,10 @@ import { ApiResponse, GetAvailableSlotsInput } from "@calcom/platform-types";
|
||||
})
|
||||
@DocsTags("Slots")
|
||||
export class SlotsController {
|
||||
constructor(private readonly slotsService: SlotsService) {}
|
||||
constructor(
|
||||
private readonly slotsService: SlotsService,
|
||||
private readonly slotsOutputService: SlotsOutputService
|
||||
) {}
|
||||
|
||||
@Post("/reserve")
|
||||
@ApiCreatedResponse({
|
||||
@@ -159,19 +162,17 @@ export class SlotsController {
|
||||
},
|
||||
});
|
||||
|
||||
const transformedSlots =
|
||||
query.slotFormat === SlotFormat.Range
|
||||
? await this.slotsService.formatSlots(
|
||||
availableSlots,
|
||||
query.duration,
|
||||
query.eventTypeId,
|
||||
query.slotFormat
|
||||
)
|
||||
: availableSlots.slots;
|
||||
const { slots } = await this.slotsOutputService.getOutputSlots(
|
||||
availableSlots,
|
||||
query.duration,
|
||||
query.eventTypeId,
|
||||
query.slotFormat,
|
||||
query.timeZone
|
||||
);
|
||||
|
||||
return {
|
||||
data: {
|
||||
slots: transformedSlots,
|
||||
slots,
|
||||
},
|
||||
status: SUCCESS_STATUS,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { EventTypesRepository_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.repository";
|
||||
import { Injectable, BadRequestException } from "@nestjs/common";
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
import { SlotFormat } from "@calcom/platform-enums";
|
||||
|
||||
type TimeSlots = { slots: Record<string, { time: string }[]> };
|
||||
type RangeSlots = { slots: Record<string, { startTime: string; endTime: string }[]> };
|
||||
|
||||
@Injectable()
|
||||
export class SlotsOutputService {
|
||||
constructor(private readonly eventTypesRepository: EventTypesRepository_2024_04_15) {}
|
||||
|
||||
async getOutputSlots(
|
||||
availableSlots: TimeSlots,
|
||||
duration?: number,
|
||||
eventTypeId?: number,
|
||||
slotFormat?: SlotFormat,
|
||||
timeZone?: string
|
||||
): Promise<TimeSlots | RangeSlots> {
|
||||
if (!slotFormat) {
|
||||
return timeZone ? this.setTimeZone(availableSlots, timeZone) : availableSlots;
|
||||
}
|
||||
|
||||
const formattedSlots = await this.formatSlots(availableSlots, duration, eventTypeId, slotFormat);
|
||||
return timeZone ? this.setTimeZoneRange(formattedSlots, timeZone) : formattedSlots;
|
||||
}
|
||||
|
||||
private setTimeZone(slots: TimeSlots, timeZone: string): TimeSlots {
|
||||
const formattedSlots = Object.entries(slots.slots).reduce((acc, [date, daySlots]) => {
|
||||
acc[date] = daySlots.map((slot) => ({
|
||||
time: DateTime.fromISO(slot.time).setZone(timeZone).toISO() || "unknown-time",
|
||||
}));
|
||||
return acc;
|
||||
}, {} as Record<string, { time: string }[]>);
|
||||
|
||||
return { slots: formattedSlots };
|
||||
}
|
||||
|
||||
private setTimeZoneRange(slots: RangeSlots, timeZone: string): RangeSlots {
|
||||
const formattedSlots = Object.entries(slots.slots).reduce((acc, [date, daySlots]) => {
|
||||
acc[date] = daySlots.map((slot) => ({
|
||||
startTime: DateTime.fromISO(slot.startTime).setZone(timeZone).toISO() || "unknown-start-time",
|
||||
endTime: DateTime.fromISO(slot.endTime).setZone(timeZone).toISO() || "unknown-end-time",
|
||||
}));
|
||||
return acc;
|
||||
}, {} as Record<string, { startTime: string; endTime: string }[]>);
|
||||
|
||||
return { slots: formattedSlots };
|
||||
}
|
||||
|
||||
private async formatSlots(
|
||||
availableSlots: TimeSlots,
|
||||
duration?: number,
|
||||
eventTypeId?: number,
|
||||
slotFormat?: SlotFormat
|
||||
): Promise<RangeSlots> {
|
||||
if (slotFormat && !Object.values(SlotFormat).includes(slotFormat)) {
|
||||
throw new BadRequestException("Invalid slot format. Must be either 'range' or 'time'");
|
||||
}
|
||||
|
||||
const slotDuration = await this.getDuration(duration, eventTypeId);
|
||||
|
||||
const slots = Object.entries(availableSlots.slots).reduce<
|
||||
Record<string, { startTime: string; endTime: string }[]>
|
||||
>((acc, [date, slots]) => {
|
||||
acc[date] = (slots as { time: string }[]).map((slot) => {
|
||||
const startTime = new Date(slot.time);
|
||||
const endTime = new Date(startTime.getTime() + slotDuration * 60000);
|
||||
return {
|
||||
startTime: startTime.toISOString(),
|
||||
endTime: endTime.toISOString(),
|
||||
};
|
||||
});
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return { slots };
|
||||
}
|
||||
|
||||
private async getDuration(duration?: number, eventTypeId?: number): Promise<number> {
|
||||
if (duration) {
|
||||
return duration;
|
||||
}
|
||||
|
||||
if (eventTypeId) {
|
||||
const eventType = await this.eventTypesRepository.getEventTypeWithDuration(eventTypeId);
|
||||
if (!eventType) {
|
||||
throw new Error("Event type not found");
|
||||
}
|
||||
return eventType.length;
|
||||
}
|
||||
|
||||
throw new Error("duration or eventTypeId is required");
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { EventTypesRepository_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.repository";
|
||||
import { SlotsRepository } from "@/modules/slots/slots.repository";
|
||||
import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common";
|
||||
import { DateTime } from "luxon";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
import { SlotFormat } from "@calcom/platform-enums";
|
||||
@@ -55,51 +56,4 @@ export class SlotsService {
|
||||
const event = await this.eventTypeRepo.getEventTypeById(eventTypeId);
|
||||
return !!event?.teamId;
|
||||
}
|
||||
|
||||
async getEventTypeWithDuration(eventTypeId: number) {
|
||||
return await this.eventTypeRepo.getEventTypeWithDuration(eventTypeId);
|
||||
}
|
||||
|
||||
async getDuration(duration?: number, eventTypeId?: number): Promise<number> {
|
||||
if (duration) {
|
||||
return duration;
|
||||
}
|
||||
|
||||
if (eventTypeId) {
|
||||
const eventType = await this.eventTypeRepo.getEventTypeWithDuration(eventTypeId);
|
||||
if (!eventType) {
|
||||
throw new Error("Event type not found");
|
||||
}
|
||||
return eventType.length;
|
||||
}
|
||||
|
||||
throw new Error("duration or eventTypeId is required");
|
||||
}
|
||||
|
||||
async formatSlots(
|
||||
availableSlots: { slots: Record<string, { time: string }[]> },
|
||||
duration?: number,
|
||||
eventTypeId?: number,
|
||||
slotFormat?: SlotFormat
|
||||
): Promise<Record<string, { startTime: string; endTime: string }[]>> {
|
||||
if (slotFormat && !Object.values(SlotFormat).includes(slotFormat)) {
|
||||
throw new BadRequestException("Invalid slot format. Must be either 'range' or 'time'");
|
||||
}
|
||||
|
||||
const slotDuration = await this.getDuration(duration, eventTypeId);
|
||||
|
||||
return Object.entries(availableSlots.slots).reduce<
|
||||
Record<string, { startTime: string; endTime: string }[]>
|
||||
>((acc, [date, slots]) => {
|
||||
acc[date] = (slots as { time: string }[]).map((slot) => {
|
||||
const startTime = new Date(slot.time);
|
||||
const endTime = new Date(startTime.getTime() + slotDuration * 60000);
|
||||
return {
|
||||
startTime: startTime.toISOString(),
|
||||
endTime: endTime.toISOString(),
|
||||
};
|
||||
});
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { SlotsController } from "@/modules/slots/controllers/slots.controller";
|
||||
import { SlotsOutputService } from "@/modules/slots/services/slots-output.service";
|
||||
import { SlotsService } from "@/modules/slots/services/slots.service";
|
||||
import { SlotsRepository } from "@/modules/slots/slots.repository";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, EventTypesModule_2024_04_15],
|
||||
providers: [SlotsRepository, SlotsService],
|
||||
providers: [SlotsRepository, SlotsService, SlotsOutputService],
|
||||
controllers: [SlotsController],
|
||||
exports: [SlotsService],
|
||||
})
|
||||
|
||||
@@ -5139,7 +5139,7 @@
|
||||
"name": "eventTypeSlug",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Slug of the event type for which slots are being fetched.",
|
||||
"description": "Slug of the event type for which slots are being fetched. If event slug is provided then username must be provided too as query parameter `usernameList[]=username`",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -5148,7 +5148,8 @@
|
||||
"name": "usernameList",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Only for dynamic events - list of usernames for which slots are being fetched.",
|
||||
"description": "Only if eventTypeSlug is provided or for dynamic events - list of usernames for which slots are being fetched.",
|
||||
"example": "usernameList[]=bob",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -6998,6 +6999,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 +7010,8 @@
|
||||
"slug",
|
||||
"label",
|
||||
"required",
|
||||
"placeholder"
|
||||
"placeholder",
|
||||
"hidden"
|
||||
]
|
||||
},
|
||||
"AddressFieldInput_2024_06_14": {
|
||||
@@ -7035,6 +7041,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 +7052,8 @@
|
||||
"slug",
|
||||
"label",
|
||||
"required",
|
||||
"placeholder"
|
||||
"placeholder",
|
||||
"hidden"
|
||||
]
|
||||
},
|
||||
"TextFieldInput_2024_06_14": {
|
||||
@@ -7072,6 +7083,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 +7094,8 @@
|
||||
"slug",
|
||||
"label",
|
||||
"required",
|
||||
"placeholder"
|
||||
"placeholder",
|
||||
"hidden"
|
||||
]
|
||||
},
|
||||
"NumberFieldInput_2024_06_14": {
|
||||
@@ -7109,6 +7125,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 +7136,8 @@
|
||||
"slug",
|
||||
"label",
|
||||
"required",
|
||||
"placeholder"
|
||||
"placeholder",
|
||||
"hidden"
|
||||
]
|
||||
},
|
||||
"TextAreaFieldInput_2024_06_14": {
|
||||
@@ -7146,6 +7167,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 +7178,8 @@
|
||||
"slug",
|
||||
"label",
|
||||
"required",
|
||||
"placeholder"
|
||||
"placeholder",
|
||||
"hidden"
|
||||
]
|
||||
},
|
||||
"SelectFieldInput_2024_06_14": {
|
||||
@@ -7193,6 +7219,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 +7231,8 @@
|
||||
"label",
|
||||
"required",
|
||||
"placeholder",
|
||||
"options"
|
||||
"options",
|
||||
"hidden"
|
||||
]
|
||||
},
|
||||
"MultiSelectFieldInput_2024_06_14": {
|
||||
@@ -7237,6 +7268,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 +7279,8 @@
|
||||
"slug",
|
||||
"label",
|
||||
"required",
|
||||
"options"
|
||||
"options",
|
||||
"hidden"
|
||||
]
|
||||
},
|
||||
"MultiEmailFieldInput_2024_06_14": {
|
||||
@@ -7274,6 +7310,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 +7321,8 @@
|
||||
"slug",
|
||||
"label",
|
||||
"required",
|
||||
"placeholder"
|
||||
"placeholder",
|
||||
"hidden"
|
||||
]
|
||||
},
|
||||
"CheckboxGroupFieldInput_2024_06_14": {
|
||||
@@ -7317,6 +7358,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 +7369,8 @@
|
||||
"slug",
|
||||
"label",
|
||||
"required",
|
||||
"options"
|
||||
"options",
|
||||
"hidden"
|
||||
]
|
||||
},
|
||||
"RadioGroupFieldInput_2024_06_14": {
|
||||
@@ -7360,6 +7406,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 +7417,8 @@
|
||||
"slug",
|
||||
"label",
|
||||
"required",
|
||||
"options"
|
||||
"options",
|
||||
"hidden"
|
||||
]
|
||||
},
|
||||
"BooleanFieldInput_2024_06_14": {
|
||||
@@ -7392,13 +7443,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": {
|
||||
@@ -7686,7 +7742,7 @@
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"NameFieldInput_2024_06_14": {
|
||||
"NameDefaultFieldInput_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
@@ -7711,7 +7767,7 @@
|
||||
"placeholder"
|
||||
]
|
||||
},
|
||||
"EmailFieldInput_2024_06_14": {
|
||||
"EmailDefaultFieldInput_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
@@ -7736,6 +7792,126 @@
|
||||
"placeholder"
|
||||
]
|
||||
},
|
||||
"TitleDefaultFieldInput_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"example": "title",
|
||||
"description": "only allowed value for type is `title`"
|
||||
},
|
||||
"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=journey`, the title field will be prefilled with this value and disabled."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"slug"
|
||||
]
|
||||
},
|
||||
"NotesDefaultFieldInput_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"example": "notes",
|
||||
"description": "only allowed value for type is `notes`"
|
||||
},
|
||||
"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=journey`, the notes field will be prefilled with this value and disabled."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"slug"
|
||||
]
|
||||
},
|
||||
"GuestsDefaultFieldInput_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"example": "guests",
|
||||
"description": "only allowed value for type is `guests`"
|
||||
},
|
||||
"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=bob@cal.com`, the guests field will be prefilled with this value and disabled."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"slug"
|
||||
]
|
||||
},
|
||||
"RescheduleReasonDefaultFieldInput_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"example": "rescheduleReason",
|
||||
"description": "only allowed value for type is `rescheduleReason`"
|
||||
},
|
||||
"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=travel`, the rescheduleReason field will be prefilled with this value and disabled."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"slug"
|
||||
]
|
||||
},
|
||||
"BookerLayouts_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -7866,10 +8042,22 @@
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/NameFieldInput_2024_06_14"
|
||||
"$ref": "#/components/schemas/NameDefaultFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EmailFieldInput_2024_06_14"
|
||||
"$ref": "#/components/schemas/EmailDefaultFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/TitleDefaultFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/NotesDefaultFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/GuestsDefaultFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/RescheduleReasonDefaultFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/PhoneFieldInput_2024_06_14"
|
||||
@@ -8403,125 +8591,218 @@
|
||||
},
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"name",
|
||||
"email",
|
||||
"title",
|
||||
"notes",
|
||||
"guests"
|
||||
],
|
||||
"example": "rescheduleReason",
|
||||
"description": "only allowed value for type is `rescheduleReason`",
|
||||
"default": "rescheduleReason"
|
||||
},
|
||||
"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."
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "object",
|
||||
"default": true,
|
||||
"description": "This property is always true because it's a default field",
|
||||
"example": true
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"default": "rescheduleReason"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"default": "textarea"
|
||||
},
|
||||
"required": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"isDefault",
|
||||
"slug",
|
||||
"type",
|
||||
"required"
|
||||
"isDefault",
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"TitleDefaultFieldOutput_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"name",
|
||||
"email",
|
||||
"title",
|
||||
"notes",
|
||||
"guests"
|
||||
],
|
||||
"example": "title",
|
||||
"description": "only allowed value for type is `title`",
|
||||
"default": "title"
|
||||
},
|
||||
"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."
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "object",
|
||||
"default": true,
|
||||
"description": "This property is always true because it's a default field",
|
||||
"example": true
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"default": "title"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"default": "text"
|
||||
},
|
||||
"required": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"isDefault",
|
||||
"slug",
|
||||
"type",
|
||||
"required"
|
||||
"isDefault",
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"NotesDefaultFieldOutput_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"name",
|
||||
"email",
|
||||
"title",
|
||||
"notes",
|
||||
"guests"
|
||||
],
|
||||
"example": "notes",
|
||||
"description": "only allowed value for type is `notes`",
|
||||
"default": "notes"
|
||||
},
|
||||
"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."
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "object",
|
||||
"default": true,
|
||||
"description": "This property is always true because it's a default field",
|
||||
"example": true
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"default": "notes"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"default": "textarea"
|
||||
},
|
||||
"required": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"isDefault",
|
||||
"slug",
|
||||
"type",
|
||||
"required"
|
||||
"isDefault",
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"GuestsDefaultFieldOutput_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"name",
|
||||
"email",
|
||||
"title",
|
||||
"notes",
|
||||
"guests"
|
||||
],
|
||||
"example": "guests",
|
||||
"description": "only allowed value for type is `guests`",
|
||||
"default": "guests"
|
||||
},
|
||||
"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."
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "object",
|
||||
"default": true,
|
||||
"description": "This property is always true because it's a default field",
|
||||
"example": true
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"default": "guests"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"default": "multiemail"
|
||||
},
|
||||
"required": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"isDefault",
|
||||
"slug",
|
||||
"type",
|
||||
"required"
|
||||
"isDefault",
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"AddressFieldOutput_2024_06_14": {
|
||||
@@ -8567,6 +8848,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 +8864,7 @@
|
||||
"slug",
|
||||
"label",
|
||||
"required",
|
||||
"hidden",
|
||||
"isDefault"
|
||||
]
|
||||
},
|
||||
@@ -8620,6 +8906,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 +8922,7 @@
|
||||
"slug",
|
||||
"label",
|
||||
"required",
|
||||
"hidden",
|
||||
"isDefault"
|
||||
]
|
||||
},
|
||||
@@ -8684,6 +8975,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 +8992,7 @@
|
||||
"label",
|
||||
"required",
|
||||
"options",
|
||||
"hidden",
|
||||
"isDefault"
|
||||
]
|
||||
},
|
||||
@@ -8743,6 +9039,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 +9055,7 @@
|
||||
"slug",
|
||||
"label",
|
||||
"required",
|
||||
"hidden",
|
||||
"isDefault"
|
||||
]
|
||||
},
|
||||
@@ -8807,6 +9108,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 +9125,7 @@
|
||||
"label",
|
||||
"required",
|
||||
"options",
|
||||
"hidden",
|
||||
"isDefault"
|
||||
]
|
||||
},
|
||||
@@ -8866,6 +9172,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 +9188,7 @@
|
||||
"slug",
|
||||
"label",
|
||||
"required",
|
||||
"hidden",
|
||||
"isDefault"
|
||||
]
|
||||
},
|
||||
@@ -8922,6 +9233,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 +9249,7 @@
|
||||
"slug",
|
||||
"label",
|
||||
"required",
|
||||
"hidden",
|
||||
"isDefault"
|
||||
]
|
||||
},
|
||||
@@ -8986,6 +9302,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 +9319,7 @@
|
||||
"label",
|
||||
"required",
|
||||
"options",
|
||||
"hidden",
|
||||
"isDefault"
|
||||
]
|
||||
},
|
||||
@@ -9055,6 +9376,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 +9393,7 @@
|
||||
"label",
|
||||
"required",
|
||||
"options",
|
||||
"hidden",
|
||||
"isDefault"
|
||||
]
|
||||
},
|
||||
@@ -9114,6 +9440,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 +9456,7 @@
|
||||
"slug",
|
||||
"label",
|
||||
"required",
|
||||
"hidden",
|
||||
"isDefault"
|
||||
]
|
||||
},
|
||||
@@ -9172,6 +9503,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 +9519,7 @@
|
||||
"slug",
|
||||
"label",
|
||||
"required",
|
||||
"hidden",
|
||||
"isDefault"
|
||||
]
|
||||
},
|
||||
@@ -9597,10 +9933,22 @@
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/NameFieldInput_2024_06_14"
|
||||
"$ref": "#/components/schemas/NameDefaultFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EmailFieldInput_2024_06_14"
|
||||
"$ref": "#/components/schemas/EmailDefaultFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/TitleDefaultFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/NotesDefaultFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/GuestsDefaultFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/RescheduleReasonDefaultFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/PhoneFieldInput_2024_06_14"
|
||||
@@ -11174,10 +11522,22 @@
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/NameFieldInput_2024_06_14"
|
||||
"$ref": "#/components/schemas/NameDefaultFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EmailFieldInput_2024_06_14"
|
||||
"$ref": "#/components/schemas/EmailDefaultFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/TitleDefaultFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/NotesDefaultFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/GuestsDefaultFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/RescheduleReasonDefaultFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/PhoneFieldInput_2024_06_14"
|
||||
@@ -11999,10 +12359,22 @@
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/NameFieldInput_2024_06_14"
|
||||
"$ref": "#/components/schemas/NameDefaultFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EmailFieldInput_2024_06_14"
|
||||
"$ref": "#/components/schemas/EmailDefaultFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/TitleDefaultFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/NotesDefaultFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/GuestsDefaultFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/RescheduleReasonDefaultFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/PhoneFieldInput_2024_06_14"
|
||||
|
||||
@@ -38,6 +38,11 @@ export class EventTypesRepositoryFixture {
|
||||
return this.prismaWriteClient.eventType.create({
|
||||
data: {
|
||||
...data,
|
||||
users: {
|
||||
connect: {
|
||||
id: userId,
|
||||
},
|
||||
},
|
||||
owner: {
|
||||
connect: {
|
||||
id: userId,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { TestingModule } from "@nestjs/testing";
|
||||
import { SelectedSlots } from "@prisma/client";
|
||||
|
||||
export class SelectedSlotsRepositoryFixture {
|
||||
private prismaReadClient: PrismaReadService["prisma"];
|
||||
private prismaWriteClient: PrismaWriteService["prisma"];
|
||||
|
||||
constructor(private readonly module: TestingModule) {
|
||||
this.prismaReadClient = module.get(PrismaReadService).prisma;
|
||||
this.prismaWriteClient = module.get(PrismaWriteService).prisma;
|
||||
}
|
||||
|
||||
async deleteByUId(uid: SelectedSlots["uid"]) {
|
||||
return this.prismaWriteClient.selectedSlots.deleteMany({ where: { uid } });
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,14 @@
|
||||
# @calcom/web
|
||||
|
||||
## 4.8.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @calcom/embed-core@1.5.2
|
||||
- @calcom/embed-react@1.5.2
|
||||
- @calcom/embed-snippet@1.3.2
|
||||
|
||||
## 4.5.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { getBucket } from "abTest/utils";
|
||||
import type { NextMiddleware, NextRequest } from "next/server";
|
||||
import { NextResponse, URLPattern } from "next/server";
|
||||
import z from "zod";
|
||||
|
||||
import { FUTURE_ROUTES_ENABLED_COOKIE_NAME, FUTURE_ROUTES_OVERRIDE_COOKIE_NAME } from "@calcom/lib/constants";
|
||||
|
||||
@@ -20,10 +19,6 @@ const ROUTES: [URLPattern, boolean][] = [
|
||||
["/auth/error", process.env.APP_ROUTER_AUTH_ERROR_ENABLED === "1"] as const,
|
||||
["/auth/platform/:path*", process.env.APP_ROUTER_AUTH_PLATFORM_ENABLED === "1"] as const,
|
||||
["/auth/oauth2/:path*", process.env.APP_ROUTER_AUTH_OAUTH2_ENABLED === "1"] as const,
|
||||
["/bookings/:status", process.env.APP_ROUTER_BOOKINGS_STATUS_ENABLED === "1"] as const,
|
||||
["/booking/:path*", process.env.APP_ROUTER_BOOKING_ENABLED === "1"] as const,
|
||||
["/team", process.env.APP_ROUTER_TEAM_ENABLED === "1"] as const,
|
||||
["/teams", process.env.APP_ROUTER_TEAMS_ENABLED === "1"] as const,
|
||||
].map(([pathname, enabled]) => [
|
||||
new URLPattern({
|
||||
pathname,
|
||||
@@ -31,8 +26,6 @@ const ROUTES: [URLPattern, boolean][] = [
|
||||
enabled,
|
||||
]);
|
||||
|
||||
const bucketSchema = z.union([z.literal("legacy"), z.literal("future")]);
|
||||
|
||||
export const abTestMiddlewareFactory =
|
||||
(next: (req: NextRequest) => Promise<NextResponse<unknown>>): NextMiddleware =>
|
||||
async (req: NextRequest) => {
|
||||
@@ -49,11 +42,9 @@ export const abTestMiddlewareFactory =
|
||||
return response;
|
||||
}
|
||||
|
||||
const safeParsedBucket = override
|
||||
? { success: true as const, data: "future" as const }
|
||||
: bucketSchema.safeParse(req.cookies.get(FUTURE_ROUTES_ENABLED_COOKIE_NAME)?.value);
|
||||
const bucketValue = override ? "future" : req.cookies.get(FUTURE_ROUTES_ENABLED_COOKIE_NAME)?.value;
|
||||
|
||||
if (!safeParsedBucket.success) {
|
||||
if (!bucketValue || !["future", "legacy"].includes(bucketValue)) {
|
||||
// cookie does not exist or it has incorrect value
|
||||
const bucket = getBucket();
|
||||
|
||||
@@ -72,7 +63,7 @@ export const abTestMiddlewareFactory =
|
||||
return NextResponse.rewrite(url, response);
|
||||
}
|
||||
|
||||
if (safeParsedBucket.data === "legacy") {
|
||||
if (bucketValue === "legacy") {
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import Head from "next/head";
|
||||
import { _generateMetadata, getTranslate } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button } from "@calcom/ui";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
export const generateMetadata = () =>
|
||||
_generateMetadata(
|
||||
(t) => `${t("access_denied")} | ${APP_NAME}`,
|
||||
() => ""
|
||||
);
|
||||
|
||||
export default function AccessDenied() {
|
||||
const { t } = useLocale();
|
||||
async function Error403() {
|
||||
const t = await getTranslate();
|
||||
|
||||
return (
|
||||
<div className="bg-subtle flex h-screen">
|
||||
<Head>
|
||||
<title>{`${t("access_denied")} | ${APP_NAME}`}</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<div className="rtl: bg-default m-auto rounded-md p-10 text-right ltr:text-left">
|
||||
<h1 className="font-cal text-emphasis text-6xl">403</h1>
|
||||
<h2 className="text-emphasis mt-6 text-2xl font-medium">{t("dont_have_access_this_page")}</h2>
|
||||
@@ -27,4 +27,4 @@ export default function AccessDenied() {
|
||||
);
|
||||
}
|
||||
|
||||
AccessDenied.PageWrapper = PageWrapper;
|
||||
export default WithLayout({ ServerPage: Error403 });
|
||||
@@ -0,0 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, showToast } from "@calcom/ui";
|
||||
|
||||
export default function CopyButton({ error }: { error: string }) {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<Button
|
||||
color="secondary"
|
||||
className="mt-2 border-0 font-sans font-normal hover:bg-gray-300"
|
||||
StartIcon="copy"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(error);
|
||||
showToast("Link copied!", "success");
|
||||
}}>
|
||||
{t("copy")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { _generateMetadata, getTranslate } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { Button } from "@calcom/ui";
|
||||
|
||||
import CopyButton from "./copy-button";
|
||||
|
||||
export const generateMetadata = () =>
|
||||
_generateMetadata(
|
||||
(t) => `${t("something_unexpected_occurred")} | ${APP_NAME}`,
|
||||
() => ""
|
||||
);
|
||||
|
||||
async function Error500({ searchParams }: { searchParams: { error?: string } }) {
|
||||
const t = await getTranslate();
|
||||
|
||||
return (
|
||||
<div className="bg-subtle flex h-screen">
|
||||
<div className="rtl: bg-default m-auto rounded-md p-10 text-right ltr:text-left">
|
||||
<h1 className="font-cal text-emphasis text-6xl">500</h1>
|
||||
<h2 className="text-emphasis mt-6 text-2xl font-medium">{t("500_error_message")}</h2>
|
||||
<p className="text-default mb-6 mt-4 max-w-2xl text-sm">{t("something_went_wrong_on_our_end")}</p>
|
||||
{searchParams?.error && (
|
||||
<div className="mb-8 flex flex-col">
|
||||
<p className="text-default mb-4 max-w-2xl text-sm">
|
||||
{t("please_provide_following_text_to_suppport")}:
|
||||
</p>
|
||||
<pre className="bg-emphasis text-emphasis w-full max-w-2xl whitespace-normal break-words rounded-md p-4">
|
||||
{searchParams.error}
|
||||
<br />
|
||||
<CopyButton error={searchParams.error} />
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
<Button href="mailto:support@cal.com">{t("contact_support")}</Button>
|
||||
<Button color="secondary" href="javascript:history.back()" className="ml-2">
|
||||
{t("go_back")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default WithLayout({
|
||||
ServerPage: Error500,
|
||||
});
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
import type { Request, Response } from "express";
|
||||
import type { NextApiRequest, NextApiResponse, Redirect } from "next";
|
||||
import { redirect, notFound } from "next/navigation";
|
||||
import { createMocks } from "node-mocks-http";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import withEmbedSsrAppDir from "./WithEmbedSSR";
|
||||
|
||||
export type CustomNextApiRequest = NextApiRequest & Request;
|
||||
export type CustomNextApiResponse = NextApiResponse & Response;
|
||||
|
||||
export function createMockNextJsRequest(...args: Parameters<typeof createMocks>) {
|
||||
return createMocks<CustomNextApiRequest, CustomNextApiResponse>(...args);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
const noop = () => {};
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
redirect: vi.fn(),
|
||||
notFound: vi.fn(),
|
||||
}));
|
||||
|
||||
function getServerSidePropsFnGenerator(
|
||||
config:
|
||||
| { redirectUrl: string }
|
||||
| { props: Record<string, unknown> }
|
||||
| {
|
||||
notFound: true;
|
||||
}
|
||||
) {
|
||||
if ("redirectUrl" in config)
|
||||
return async () => {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: config.redirectUrl,
|
||||
} satisfies Redirect,
|
||||
};
|
||||
};
|
||||
|
||||
if ("props" in config)
|
||||
return async () => {
|
||||
return {
|
||||
props: config.props,
|
||||
};
|
||||
};
|
||||
|
||||
if ("notFound" in config)
|
||||
return async () => {
|
||||
return {
|
||||
notFound: true as const,
|
||||
};
|
||||
};
|
||||
|
||||
throw new Error("Invalid config");
|
||||
}
|
||||
|
||||
interface ServerSidePropsContext {
|
||||
embedRelatedParams?: Record<string, string>;
|
||||
}
|
||||
|
||||
function getServerSidePropsContextArg({ embedRelatedParams = {} }: ServerSidePropsContext) {
|
||||
const { req, res } = createMockNextJsRequest();
|
||||
return {
|
||||
req,
|
||||
res,
|
||||
query: {
|
||||
...embedRelatedParams,
|
||||
},
|
||||
resolvedUrl: "/MOCKED_RESOLVED_URL",
|
||||
};
|
||||
}
|
||||
|
||||
describe("withEmbedSsrAppDir", () => {
|
||||
describe("when gSSP returns redirect", () => {
|
||||
describe("when redirect destination is relative", () => {
|
||||
it("should redirect with layout and embed params from the current query", async () => {
|
||||
const withEmbedGetSsr = withEmbedSsrAppDir(
|
||||
getServerSidePropsFnGenerator({
|
||||
redirectUrl: "/reschedule",
|
||||
})
|
||||
);
|
||||
|
||||
await withEmbedGetSsr(
|
||||
getServerSidePropsContextArg({
|
||||
embedRelatedParams: {
|
||||
layout: "week_view",
|
||||
embed: "namespace1",
|
||||
},
|
||||
})
|
||||
).catch(noop);
|
||||
|
||||
expect(redirect).toHaveBeenCalledWith("/reschedule/embed?layout=week_view&embed=namespace1");
|
||||
});
|
||||
|
||||
it("should preserve existing query params in redirect URL", async () => {
|
||||
const withEmbedGetSsr = withEmbedSsrAppDir(
|
||||
getServerSidePropsFnGenerator({
|
||||
redirectUrl: "/reschedule?redirectParam=1",
|
||||
})
|
||||
);
|
||||
|
||||
await withEmbedGetSsr(
|
||||
getServerSidePropsContextArg({
|
||||
embedRelatedParams: {
|
||||
layout: "week_view",
|
||||
embed: "namespace1",
|
||||
},
|
||||
})
|
||||
).catch(noop);
|
||||
|
||||
expect(redirect).toHaveBeenCalledWith(
|
||||
"/reschedule/embed?redirectParam=1&layout=week_view&embed=namespace1"
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle empty embed namespace", async () => {
|
||||
const withEmbedGetSsr = withEmbedSsrAppDir(
|
||||
getServerSidePropsFnGenerator({
|
||||
redirectUrl: "/reschedule?redirectParam=1",
|
||||
})
|
||||
);
|
||||
|
||||
await withEmbedGetSsr(
|
||||
getServerSidePropsContextArg({
|
||||
embedRelatedParams: {
|
||||
layout: "week_view",
|
||||
embed: "",
|
||||
},
|
||||
})
|
||||
).catch(noop);
|
||||
|
||||
expect(redirect).toHaveBeenCalledWith("/reschedule/embed?redirectParam=1&layout=week_view&embed=");
|
||||
});
|
||||
});
|
||||
|
||||
describe("when redirect destination is absolute", () => {
|
||||
it("should handle HTTPS URLs", async () => {
|
||||
const withEmbedGetSsr = withEmbedSsrAppDir(
|
||||
getServerSidePropsFnGenerator({
|
||||
redirectUrl: "https://calcom.cal.local/owner",
|
||||
})
|
||||
);
|
||||
|
||||
await withEmbedGetSsr(
|
||||
getServerSidePropsContextArg({
|
||||
embedRelatedParams: {
|
||||
layout: "week_view",
|
||||
embed: "namespace1",
|
||||
},
|
||||
})
|
||||
).catch(noop);
|
||||
|
||||
expect(redirect).toHaveBeenCalledWith(
|
||||
"https://calcom.cal.local/owner/embed?layout=week_view&embed=namespace1"
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle HTTP URLs", async () => {
|
||||
const withEmbedGetSsr = withEmbedSsrAppDir(
|
||||
getServerSidePropsFnGenerator({
|
||||
redirectUrl: "http://calcom.cal.local/owner",
|
||||
})
|
||||
);
|
||||
|
||||
await withEmbedGetSsr(
|
||||
getServerSidePropsContextArg({
|
||||
embedRelatedParams: {
|
||||
layout: "week_view",
|
||||
embed: "namespace1",
|
||||
},
|
||||
})
|
||||
).catch(noop);
|
||||
|
||||
expect(redirect).toHaveBeenCalledWith(
|
||||
"http://calcom.cal.local/owner/embed?layout=week_view&embed=namespace1"
|
||||
);
|
||||
});
|
||||
|
||||
it("should treat URLs without protocol as relative", async () => {
|
||||
const withEmbedGetSsr = withEmbedSsrAppDir(
|
||||
getServerSidePropsFnGenerator({
|
||||
redirectUrl: "calcom.cal.local/owner",
|
||||
})
|
||||
);
|
||||
|
||||
await withEmbedGetSsr(
|
||||
getServerSidePropsContextArg({
|
||||
embedRelatedParams: {
|
||||
layout: "week_view",
|
||||
embed: "namespace1",
|
||||
},
|
||||
})
|
||||
).catch(noop);
|
||||
|
||||
expect(redirect).toHaveBeenCalledWith(
|
||||
"/calcom.cal.local/owner/embed?layout=week_view&embed=namespace1"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when gSSP returns props", () => {
|
||||
it("should add isEmbed=true prop", async () => {
|
||||
const withEmbedGetSsr = withEmbedSsrAppDir(
|
||||
getServerSidePropsFnGenerator({
|
||||
props: {
|
||||
prop1: "value1",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const ret = await withEmbedGetSsr(
|
||||
getServerSidePropsContextArg({
|
||||
embedRelatedParams: {
|
||||
layout: "week_view",
|
||||
embed: "",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(ret).toEqual({
|
||||
prop1: "value1",
|
||||
isEmbed: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when gSSP returns notFound", () => {
|
||||
it("should throw notFound", async () => {
|
||||
const withEmbedGetSsr = withEmbedSsrAppDir(
|
||||
getServerSidePropsFnGenerator({
|
||||
notFound: true,
|
||||
})
|
||||
);
|
||||
|
||||
await withEmbedGetSsr(
|
||||
getServerSidePropsContextArg({
|
||||
embedRelatedParams: {
|
||||
layout: "week_view",
|
||||
embed: "",
|
||||
},
|
||||
})
|
||||
).catch(noop);
|
||||
|
||||
expect(notFound).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,4 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { isNotFoundError } from "next/dist/client/components/not-found";
|
||||
import { getURLFromRedirectError, isRedirectError } from "next/dist/client/components/redirect";
|
||||
import type { GetServerSideProps, GetServerSidePropsContext } from "next";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
|
||||
import { WebAppURL } from "@calcom/lib/WebAppURL";
|
||||
@@ -9,48 +7,46 @@ export type EmbedProps = {
|
||||
isEmbed?: boolean;
|
||||
};
|
||||
|
||||
export default function withEmbedSsrAppDir<T extends Record<string, any>>(
|
||||
getData: (context: GetServerSidePropsContext) => Promise<T>
|
||||
) {
|
||||
return async (context: GetServerSidePropsContext): Promise<T> => {
|
||||
const withEmbedSsrAppDir =
|
||||
<T extends Record<string, any>>(getServerSideProps: GetServerSideProps<T>) =>
|
||||
async (context: GetServerSidePropsContext): Promise<T> => {
|
||||
const { embed, layout } = context.query;
|
||||
try {
|
||||
const props = await getData(context);
|
||||
const ssrResponse = await getServerSideProps(context);
|
||||
|
||||
return {
|
||||
...props,
|
||||
isEmbed: true,
|
||||
};
|
||||
} catch (e) {
|
||||
if (isRedirectError(e)) {
|
||||
const destinationUrl = getURLFromRedirectError(e);
|
||||
let urlPrefix = "";
|
||||
if ("redirect" in ssrResponse) {
|
||||
const destinationUrl = ssrResponse.redirect.destination;
|
||||
let urlPrefix = "";
|
||||
|
||||
// Get the URL parsed from URL so that we can reliably read pathname and searchParams from it.
|
||||
const destinationUrlObj = new WebAppURL(destinationUrl);
|
||||
// Get the URL parsed from URL so that we can reliably read pathname and searchParams from it.
|
||||
const destinationUrlObj = new WebAppURL(ssrResponse.redirect.destination);
|
||||
|
||||
// If it's a complete URL, use the origin as the prefix to ensure we redirect to the same domain.
|
||||
if (destinationUrl.search(/^(http:|https:).*/) !== -1) {
|
||||
urlPrefix = destinationUrlObj.origin;
|
||||
} else {
|
||||
// Don't use any prefix for relative URLs to ensure we stay on the same domain
|
||||
urlPrefix = "";
|
||||
}
|
||||
|
||||
const destinationQueryStr = destinationUrlObj.searchParams.toString();
|
||||
// Make sure that redirect happens to /embed page and pass on embed query param as is for preserving Cal JS API namespace
|
||||
const newDestinationUrl = `${urlPrefix}${destinationUrlObj.pathname}/embed?${
|
||||
destinationQueryStr ? `${destinationQueryStr}&` : ""
|
||||
}layout=${layout}&embed=${embed}`;
|
||||
|
||||
redirect(newDestinationUrl);
|
||||
// If it's a complete URL, use the origin as the prefix to ensure we redirect to the same domain.
|
||||
if (destinationUrl.search(/^(http:|https:).*/) !== -1) {
|
||||
urlPrefix = destinationUrlObj.origin;
|
||||
} else {
|
||||
// Don't use any prefix for relative URLs to ensure we stay on the same domain
|
||||
urlPrefix = "";
|
||||
}
|
||||
|
||||
if (isNotFoundError(e)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
throw e;
|
||||
const destinationQueryStr = destinationUrlObj.searchParams.toString();
|
||||
// Make sure that redirect happens to /embed page and pass on embed query param as is for preserving Cal JS API namespace
|
||||
const newDestinationUrl = `${urlPrefix}${destinationUrlObj.pathname}/embed?${
|
||||
destinationQueryStr ? `${destinationQueryStr}&` : ""
|
||||
}layout=${layout}&embed=${embed}`;
|
||||
redirect(newDestinationUrl);
|
||||
}
|
||||
|
||||
if ("notFound" in ssrResponse) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return {
|
||||
...ssrResponse.props,
|
||||
...("trpcState" in ssrResponse.props && {
|
||||
dehydratedState: ssrResponse.props.trpcState,
|
||||
}),
|
||||
isEmbed: true,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default withEmbedSsrAppDir;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { generateEventBookingPageMetadata } from "app/generateBookingPageMetadata";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { headers, cookies } from "next/headers";
|
||||
|
||||
@@ -18,7 +18,7 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const props = await getData(legacyCtx);
|
||||
|
||||
const { booking, user: username, slug: eventSlug } = props;
|
||||
const { booking, user: username, slug: eventSlug, isSEOIndexable, eventData, isBrandingHidden } = props;
|
||||
const rescheduleUid = booking?.uid;
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(legacyCtx.req, legacyCtx.params?.orgSlug);
|
||||
|
||||
@@ -31,12 +31,21 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
});
|
||||
|
||||
const profileName = event?.profile?.name ?? "";
|
||||
const profileImage = event?.profile.image;
|
||||
const title = event?.title ?? "";
|
||||
|
||||
return await _generateMetadata(
|
||||
(t) => `${rescheduleUid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
|
||||
(t) => `${rescheduleUid ? t("reschedule") : ""} ${title}`
|
||||
);
|
||||
const metadata = await generateEventBookingPageMetadata({
|
||||
event: {
|
||||
hidden: event?.hidden ?? false,
|
||||
title,
|
||||
users: event?.users ?? [],
|
||||
},
|
||||
hideBranding: isBrandingHidden,
|
||||
orgSlug: eventData?.entity.orgSlug ?? null,
|
||||
isSEOIndexable: !!isSEOIndexable,
|
||||
profile: { name: profileName, image: profileImage ?? "" },
|
||||
isReschedule: !!rescheduleUid,
|
||||
});
|
||||
return metadata;
|
||||
};
|
||||
const getData = withAppDirSsr<LegacyPageProps>(getServerSideProps);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { generateUserProfilePageMetadata } from "app/generateBookingPageMetadata";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { headers, cookies } from "next/headers";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
|
||||
import { getServerSideProps } from "@server/lib/[user]/getServerSideProps";
|
||||
|
||||
import type { PageProps as LegacyPageProps } from "~/users/views/users-public-view";
|
||||
import LegacyPage from "~/users/views/users-public-view";
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
|
||||
|
||||
const { profile, markdownStrippedBio, isOrgSEOIndexable, entity } = props;
|
||||
|
||||
const isOrg = !!profile?.organization;
|
||||
const allowSEOIndexing = !!(
|
||||
(!isOrg && profile.allowSEOIndexing) ||
|
||||
(isOrg && isOrgSEOIndexable && profile.allowSEOIndexing)
|
||||
);
|
||||
return await generateUserProfilePageMetadata({
|
||||
profile: {
|
||||
name: profile.name,
|
||||
image: profile.image ?? "",
|
||||
username: profile.username ?? "",
|
||||
markdownStrippedBio: markdownStrippedBio,
|
||||
},
|
||||
event: null,
|
||||
hideBranding: false,
|
||||
orgSlug: entity.orgSlug ?? null,
|
||||
isSEOIndexable: allowSEOIndexing,
|
||||
});
|
||||
};
|
||||
|
||||
const getData = withAppDirSsr<LegacyPageProps>(getServerSideProps);
|
||||
export default WithLayout({ getData, Page: LegacyPage })<"P">;
|
||||
+73
-31
@@ -3,28 +3,41 @@ import i18next from "i18next";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
import { constructGenericImage } from "@calcom/lib/OgImages";
|
||||
import { IS_CALCOM, WEBAPP_URL, APP_NAME, SEO_IMG_OGIMG } from "@calcom/lib/constants";
|
||||
import type { MeetingImageProps } from "@calcom/lib/OgImages";
|
||||
import { constructGenericImage, constructMeetingImage } from "@calcom/lib/OgImages";
|
||||
import { IS_CALCOM, WEBAPP_URL, APP_NAME, SEO_IMG_OGIMG, CAL_URL } from "@calcom/lib/constants";
|
||||
import { buildCanonical } from "@calcom/lib/next-seo.config";
|
||||
import { truncateOnWord } from "@calcom/lib/text";
|
||||
//@ts-expect-error no type definitions
|
||||
import config from "@calcom/web/next-i18next.config";
|
||||
|
||||
const create = async (locale: string, ns: string) => {
|
||||
const i18nInstanceCache: Record<string, any> = {};
|
||||
|
||||
const createI18nInstance = async (locale: string, ns: string) => {
|
||||
const cacheKey = `${locale}-${ns}`;
|
||||
// Check module-level cache first
|
||||
if (i18nInstanceCache[cacheKey]) {
|
||||
return i18nInstanceCache[cacheKey];
|
||||
}
|
||||
|
||||
const { _nextI18Next } = await serverSideTranslations(locale, [ns], config);
|
||||
|
||||
const _i18n = i18next.createInstance();
|
||||
_i18n.init({
|
||||
await _i18n.init({
|
||||
lng: locale,
|
||||
resources: _nextI18Next?.initialI18nStore,
|
||||
fallbackLng: _nextI18Next?.userConfig?.i18n.defaultLocale,
|
||||
});
|
||||
|
||||
// Cache the instance
|
||||
i18nInstanceCache[cacheKey] = _i18n;
|
||||
return _i18n;
|
||||
};
|
||||
|
||||
export const getFixedT = async (locale: string, ns = "common") => {
|
||||
const i18n = await create(locale, ns);
|
||||
|
||||
return i18n.getFixedT(locale, ns);
|
||||
const getTranslationWithCache = async (locale: string, ns = "common") => {
|
||||
const localeWithFallback = locale ?? "en";
|
||||
const i18n = await createI18nInstance(localeWithFallback, ns);
|
||||
return i18n.getFixedT(localeWithFallback, ns);
|
||||
};
|
||||
|
||||
export const getTranslate = async () => {
|
||||
@@ -32,51 +45,80 @@ export const getTranslate = async () => {
|
||||
// If "x-locale" does not exist in header,
|
||||
// ensure that config.matcher in middleware includes the page you are testing
|
||||
const locale = headersList.get("x-locale");
|
||||
const t = await getFixedT(locale ?? "en");
|
||||
const t = await getTranslationWithCache(locale ?? "en");
|
||||
return t;
|
||||
};
|
||||
|
||||
export const _generateMetadata = async (
|
||||
const _generateMetadataWithoutImage = async (
|
||||
getTitle: (t: TFunction<string, undefined>) => string,
|
||||
getDescription: (t: TFunction<string, undefined>) => string,
|
||||
excludeAppNameFromTitle?: boolean
|
||||
hideBranding?: boolean,
|
||||
origin?: string
|
||||
) => {
|
||||
const h = headers();
|
||||
const canonical = h.get("x-pathname") ?? "";
|
||||
const pathname = h.get("x-pathname") ?? "";
|
||||
const canonical = buildCanonical({ path: pathname, origin: origin ?? CAL_URL });
|
||||
const locale = h.get("x-locale") ?? "en";
|
||||
|
||||
const t = await getFixedT(locale, "common");
|
||||
const t = await getTranslationWithCache(locale);
|
||||
|
||||
const title = getTitle(t);
|
||||
const description = getDescription(t);
|
||||
|
||||
const metadataBase = new URL(IS_CALCOM ? "https://cal.com" : WEBAPP_URL);
|
||||
|
||||
const image =
|
||||
SEO_IMG_OGIMG +
|
||||
constructGenericImage({
|
||||
title,
|
||||
description,
|
||||
});
|
||||
|
||||
const titleSuffix = `| ${APP_NAME}`;
|
||||
const displayedTitle =
|
||||
title.includes(titleSuffix) || excludeAppNameFromTitle ? title : `${title} ${titleSuffix}`;
|
||||
const displayedTitle = title.includes(titleSuffix) || hideBranding ? title : `${title} ${titleSuffix}`;
|
||||
const metadataBase = new URL(IS_CALCOM ? "https://cal.com" : WEBAPP_URL);
|
||||
|
||||
return {
|
||||
title: title.length === 0 ? APP_NAME : displayedTitle,
|
||||
description,
|
||||
alternates: {
|
||||
canonical,
|
||||
},
|
||||
alternates: { canonical },
|
||||
openGraph: {
|
||||
description: truncateOnWord(description, 158),
|
||||
url: canonical,
|
||||
type: "website",
|
||||
siteName: APP_NAME,
|
||||
title,
|
||||
images: [image],
|
||||
title: displayedTitle,
|
||||
},
|
||||
metadataBase,
|
||||
};
|
||||
};
|
||||
|
||||
export const _generateMetadata = async (
|
||||
getTitle: (t: TFunction<string, undefined>) => string,
|
||||
getDescription: (t: TFunction<string, undefined>) => string,
|
||||
hideBranding?: boolean,
|
||||
origin?: string
|
||||
) => {
|
||||
const metadata = await _generateMetadataWithoutImage(getTitle, getDescription, hideBranding, origin);
|
||||
const image =
|
||||
SEO_IMG_OGIMG +
|
||||
constructGenericImage({
|
||||
title: metadata.title,
|
||||
description: metadata.description,
|
||||
});
|
||||
|
||||
return {
|
||||
...metadata,
|
||||
openGraph: {
|
||||
...metadata.openGraph,
|
||||
images: [image],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const generateMeetingMetadata = async (
|
||||
meeting: MeetingImageProps,
|
||||
getTitle: (t: TFunction<string, undefined>) => string,
|
||||
getDescription: (t: TFunction<string, undefined>) => string,
|
||||
hideBranding?: boolean,
|
||||
origin?: string
|
||||
) => {
|
||||
const metadata = await _generateMetadataWithoutImage(getTitle, getDescription, hideBranding, origin);
|
||||
const image = SEO_IMG_OGIMG + constructMeetingImage(meeting);
|
||||
return {
|
||||
...metadata,
|
||||
openGraph: {
|
||||
...metadata.openGraph,
|
||||
images: [image],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -20,12 +20,13 @@ const inputSchema = z.object({
|
||||
cardKeys: z.array(z.string()),
|
||||
});
|
||||
|
||||
export async function handler(request: Request) {
|
||||
async function handler(request: Request) {
|
||||
const headersList = headers();
|
||||
const requestBody = await request.json();
|
||||
|
||||
// HMAC verification
|
||||
const incomingSignature = headersList.get("plain-request-signature");
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const expectedSignature = createHmac("sha-256", process.env.PLAIN_HMAC_SECRET_KEY!)
|
||||
.update(JSON.stringify(requestBody))
|
||||
.digest("hex");
|
||||
@@ -88,18 +89,8 @@ export async function handler(request: Request) {
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentBadge: {
|
||||
badgeLabel:
|
||||
customer.emailVerified === undefined
|
||||
? "Unknown"
|
||||
: customer.emailVerified
|
||||
? "Yes"
|
||||
: "No",
|
||||
badgeColor:
|
||||
customer.emailVerified === undefined
|
||||
? "YELLOW"
|
||||
: customer.emailVerified
|
||||
? "GREEN"
|
||||
: "RED",
|
||||
badgeLabel: customer.emailVerified ? "Yes" : "No",
|
||||
badgeColor: customer.emailVerified ? "GREEN" : "RED",
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -195,18 +186,8 @@ export async function handler(request: Request) {
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentBadge: {
|
||||
badgeLabel:
|
||||
customer.twoFactorEnabled === undefined
|
||||
? "Unknown"
|
||||
: customer.twoFactorEnabled
|
||||
? "Yes"
|
||||
: "No",
|
||||
badgeColor:
|
||||
customer.twoFactorEnabled === undefined
|
||||
? "YELLOW"
|
||||
: customer.twoFactorEnabled
|
||||
? "GREEN"
|
||||
: "RED",
|
||||
badgeLabel: customer.twoFactorEnabled ? "Yes" : "No",
|
||||
badgeColor: customer.twoFactorEnabled ? "GREEN" : "RED",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { createHmac } from "crypto";
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { apiRouteMiddleware } from "@calcom/lib/server/apiRouteMiddleware";
|
||||
|
||||
const responseSchema = z.object({
|
||||
hash: z.string(),
|
||||
email: z.string().email(),
|
||||
shortName: z.string(),
|
||||
appId: z.string(),
|
||||
fullName: z.string(),
|
||||
chatAvatarUrl: z.string(),
|
||||
});
|
||||
|
||||
async function handler(request: Request) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const session = await getServerSession({ req: request as any });
|
||||
if (!session?.user?.email) {
|
||||
return new Response("Unauthorized - No session email found", { status: 401 });
|
||||
}
|
||||
|
||||
const secret = process.env.PLAIN_CHAT_HMAC_SECRET_KEY;
|
||||
if (!secret) {
|
||||
return new Response("Missing Plain Chat secret", { status: 500 });
|
||||
}
|
||||
|
||||
const hmac = createHmac("sha256", secret);
|
||||
hmac.update(session.user.email.toLowerCase().trim());
|
||||
const hash = hmac.digest("hex");
|
||||
|
||||
const shortName =
|
||||
(session.user.name?.split(" ")[0] || session.user.email).charAt(0).toUpperCase() +
|
||||
(session.user.name?.split(" ")[0] || session.user.email).slice(1) || "User";
|
||||
|
||||
const response = responseSchema.parse({
|
||||
hash,
|
||||
email: session.user.email || "user@example.com",
|
||||
shortName,
|
||||
appId: process.env.PLAIN_CHAT_ID,
|
||||
fullName: session.user.name || "User",
|
||||
chatAvatarUrl: session.user.avatarUrl || "",
|
||||
});
|
||||
|
||||
return NextResponse.json(response);
|
||||
}
|
||||
|
||||
export const POST = apiRouteMiddleware(handler);
|
||||
+1
-4
@@ -1,12 +1,9 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import withEmbedSsrAppDir from "app/WithEmbedSSR";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import OldPage from "~/bookings/views/bookings-single-view";
|
||||
import { getServerSideProps, type PageProps } from "~/bookings/views/bookings-single-view.getServerSideProps";
|
||||
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
|
||||
const getEmbedData = withEmbedSsrAppDir(getData);
|
||||
const getEmbedData = withEmbedSsrAppDir<PageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({ getLayout: null, getData: getEmbedData, Page: OldPage });
|
||||
@@ -4,6 +4,7 @@ import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
@@ -12,7 +13,7 @@ import OldPage from "~/bookings/views/bookings-single-view";
|
||||
import { getServerSideProps, type PageProps } from "~/bookings/views/bookings-single-view.getServerSideProps";
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
|
||||
const { bookingInfo, eventType, recurringBookings } = await getData(
|
||||
const { bookingInfo, eventType, recurringBookings, orgSlug } = await getData(
|
||||
buildLegacyCtx(headers(), cookies(), params, searchParams)
|
||||
);
|
||||
const needsConfirmation = bookingInfo.status === BookingStatus.PENDING && eventType.requiresConfirmation;
|
||||
@@ -21,7 +22,9 @@ export const generateMetadata = async ({ params, searchParams }: _PageProps) =>
|
||||
(t) =>
|
||||
t(`booking_${needsConfirmation ? "submitted" : "confirmed"}${recurringBookings ? "_recurring" : ""}`),
|
||||
(t) =>
|
||||
t(`booking_${needsConfirmation ? "submitted" : "confirmed"}${recurringBookings ? "_recurring" : ""}`)
|
||||
t(`booking_${needsConfirmation ? "submitted" : "confirmed"}${recurringBookings ? "_recurring" : ""}`),
|
||||
false,
|
||||
getOrgFullOrigin(orgSlug)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import BookingDryRunSuccessView from "~/bookings/views/booking-dry-run-success-view";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
(t) => t("booking_dry_run_successful"),
|
||||
(t) => t("booking_dry_run_successful_description")
|
||||
);
|
||||
};
|
||||
|
||||
const Page = async () => {
|
||||
return <BookingDryRunSuccessView />;
|
||||
};
|
||||
|
||||
export default WithLayout({ ServerPage: Page });
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { redirect } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
import { validStatuses } from "~/bookings/lib/validStatuses";
|
||||
import BookingsList from "~/bookings/views/bookings-listing-view";
|
||||
|
||||
const querySchema = z.object({
|
||||
status: z.enum(validStatuses),
|
||||
});
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("bookings"),
|
||||
(t) => t("bookings_description")
|
||||
);
|
||||
|
||||
export const generateStaticParams = async () => {
|
||||
return validStatuses.map((status) => ({ status }));
|
||||
};
|
||||
|
||||
const Page = ({ params, searchParams }: PageProps) => {
|
||||
const parsed = querySchema.safeParse({ ...params, ...searchParams });
|
||||
if (!parsed.success) {
|
||||
redirect("/bookings/upcoming");
|
||||
}
|
||||
|
||||
return <BookingsList status={parsed.data.status} />;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -0,0 +1,5 @@
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getLayout } from "~/bookings/views/bookings-listing-view";
|
||||
|
||||
export default WithLayout({ getLayout });
|
||||
@@ -17,7 +17,7 @@ export const generateMetadata = async ({ params, searchParams }: _PageProps) =>
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const pageProps = await getData(legacyCtx);
|
||||
|
||||
const { booking, user: username, slug: eventSlug, isTeamEvent } = pageProps;
|
||||
const { booking, user: username, slug: eventSlug, isTeamEvent, isBrandingHidden } = pageProps;
|
||||
const rescheduleUid = booking?.uid;
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(legacyCtx.req);
|
||||
const org = isValidOrgDomain ? currentOrgDomain : null;
|
||||
@@ -34,7 +34,8 @@ export const generateMetadata = async ({ params, searchParams }: _PageProps) =>
|
||||
const title = event?.title ?? "";
|
||||
return await _generateMetadata(
|
||||
(t) => `${rescheduleUid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
|
||||
(t) => `${rescheduleUid ? t("reschedule") : ""} ${title}`
|
||||
(t) => `${rescheduleUid ? t("reschedule") : ""} ${title}`,
|
||||
isBrandingHidden
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getLayout } from "@calcom/features/MainLayoutAppDir";
|
||||
|
||||
import EnterprisePage from "@components/EnterprisePage";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
@@ -11,4 +9,4 @@ export const generateMetadata = async () =>
|
||||
(t) => t("create_your_org_description")
|
||||
);
|
||||
|
||||
export default WithLayout({ getLayout, Page: EnterprisePage })<"P">;
|
||||
export default WithLayout({ Page: EnterprisePage })<"P">;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import withEmbedSsrAppDir from "app/WithEmbedSSR";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
@@ -6,10 +5,6 @@ import { getServerSideProps } from "@server/lib/[user]/[type]/getServerSideProps
|
||||
|
||||
import LegacyPage, { type PageProps } from "~/users/views/users-type-public-view";
|
||||
|
||||
export { generateMetadata } from "../page";
|
||||
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
|
||||
const getEmbedData = withEmbedSsrAppDir(getData);
|
||||
const getEmbedData = withEmbedSsrAppDir<PageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({ getLayout: null, getData: getEmbedData, Page: LegacyPage })<"P">;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import withEmbedSsrAppDir from "app/WithEmbedSSR";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
@@ -7,10 +6,6 @@ import { getServerSideProps } from "@server/lib/[user]/getServerSideProps";
|
||||
import type { PageProps as UserPageProps } from "~/users/views/users-public-view";
|
||||
import LegacyPage from "~/users/views/users-public-view";
|
||||
|
||||
export { generateMetadata } from "../page";
|
||||
|
||||
const getData = withAppDirSsr<UserPageProps>(getServerSideProps);
|
||||
|
||||
const getEmbedData = withEmbedSsrAppDir(getData);
|
||||
const getEmbedData = withEmbedSsrAppDir<UserPageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({ getLayout: null, getData: getEmbedData, Page: LegacyPage })<"P">;
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { headers, cookies } from "next/headers";
|
||||
|
||||
import { getLayout } from "@calcom/features/MainLayoutAppDir";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
|
||||
import { getServerSideProps } from "@server/lib/[user]/getServerSideProps";
|
||||
|
||||
import type { PageProps as LegacyPageProps } from "~/users/views/users-public-view";
|
||||
import LegacyPage from "~/users/views/users-public-view";
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
|
||||
|
||||
const { profile, markdownStrippedBio } = props;
|
||||
return await _generateMetadata(
|
||||
() => profile.name,
|
||||
() => markdownStrippedBio
|
||||
);
|
||||
};
|
||||
|
||||
const getData = withAppDirSsr<LegacyPageProps>(getServerSideProps);
|
||||
export default WithLayout({ getLayout, getData, Page: LegacyPage })<"P">;
|
||||
@@ -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";
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { withAppDirSsg } from "app/WithAppDirSsg";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import type { InferGetStaticPropsType } from "next";
|
||||
|
||||
import { validStatuses } from "~/bookings/lib/validStatuses";
|
||||
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);
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("bookings"),
|
||||
(t) => t("bookings_description")
|
||||
);
|
||||
|
||||
export const generateStaticParams = async () => {
|
||||
return validStatuses.map((status) => ({ status }));
|
||||
};
|
||||
|
||||
export default WithLayout({ getLayout: null, getData, Page })<"P">;
|
||||
|
||||
export const dynamic = "force-static";
|
||||
@@ -1,12 +1,21 @@
|
||||
import { type PageProps } from "@pages/org/[orgSlug]/[user]/[type]";
|
||||
import Page from "@pages/org/[orgSlug]/[user]/[type]/embed";
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import withEmbedSsrAppDir from "app/WithEmbedSSR";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { type OrgTypePageProps } from "app/org/[orgSlug]/[user]/[type]/page";
|
||||
|
||||
import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
|
||||
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
const getEmbedData = withEmbedSsrAppDir(getData);
|
||||
import type { PageProps as TeamTypePageProps } from "~/team/type-view";
|
||||
import TeamTypePage from "~/team/type-view";
|
||||
import UserTypePage from "~/users/views/users-type-public-view";
|
||||
import type { PageProps as UserTypePageProps } from "~/users/views/users-type-public-view";
|
||||
|
||||
export default WithLayout({ getLayout: null, getData: getEmbedData, isBookingPage: true, Page });
|
||||
const getEmbedData = withEmbedSsrAppDir<OrgTypePageProps>(getServerSideProps);
|
||||
|
||||
const Page = async (props: OrgTypePageProps) => {
|
||||
if ((props as TeamTypePageProps)?.teamId) {
|
||||
return <TeamTypePage {...(props as TeamTypePageProps)} />;
|
||||
}
|
||||
return <UserTypePage {...(props as UserTypePageProps)} />;
|
||||
};
|
||||
|
||||
export default WithLayout({ getLayout: null, getData: getEmbedData, isBookingPage: true, ServerPage: Page });
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import Page, { type PageProps } from "@pages/org/[orgSlug]/[user]/[type]";
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
|
||||
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({ getLayout: null, getData, Page });
|
||||
@@ -1,11 +1,21 @@
|
||||
import { getServerSideProps, type PageProps } from "@pages/org/[orgSlug]/[user]";
|
||||
import Page from "@pages/org/[orgSlug]/[user]/embed";
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import withEmbedSsrAppDir from "app/WithEmbedSSR";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { type OrgPageProps } from "app/org/[orgSlug]/[user]/page";
|
||||
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/getServerSideProps";
|
||||
|
||||
const getEmbedData = withEmbedSsrAppDir(getData);
|
||||
import type { PageProps as TeamPageProps } from "~/team/team-view";
|
||||
import TeamPage from "~/team/team-view";
|
||||
import UserPage from "~/users/views/users-public-view";
|
||||
import type { PageProps as UserPageProps } from "~/users/views/users-public-view";
|
||||
|
||||
export default WithLayout({ getLayout: null, getData: getEmbedData, isBookingPage: true, Page });
|
||||
const getEmbedData = withEmbedSsrAppDir<OrgPageProps>(getServerSideProps);
|
||||
|
||||
const Page = async (props: OrgPageProps) => {
|
||||
if ((props as TeamPageProps)?.team) {
|
||||
return <TeamPage {...(props as TeamPageProps)} />;
|
||||
}
|
||||
return <UserPage {...(props as UserPageProps)} />;
|
||||
};
|
||||
|
||||
export default WithLayout({ getLayout: null, getData: getEmbedData, isBookingPage: true, ServerPage: Page });
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import Page, { getServerSideProps, type PageProps } from "@pages/org/[orgSlug]/[user]";
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({ getLayout: null, getData, Page });
|
||||
@@ -1,28 +1,12 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import withEmbedSsrAppDir from "app/WithEmbedSSR";
|
||||
import type { PageProps as _PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
|
||||
|
||||
import type { PageProps } from "~/team/team-view";
|
||||
import TeamPage from "~/team/team-view";
|
||||
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
|
||||
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
|
||||
|
||||
return await _generateMetadata(
|
||||
(t) => props.team.name || t("nameless_team"),
|
||||
(t) => props.team.name || t("nameless_team")
|
||||
);
|
||||
};
|
||||
|
||||
const getEmbedData = withEmbedSsrAppDir(getData);
|
||||
const getEmbedData = withEmbedSsrAppDir<PageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({
|
||||
Page: TeamPage,
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps as _PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
|
||||
|
||||
import type { PageProps } from "~/team/team-view";
|
||||
import TeamPage from "~/team/team-view";
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
|
||||
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
|
||||
|
||||
return await _generateMetadata(
|
||||
(t) => props.team.name || t("nameless_team"),
|
||||
(t) => props.team.name || t("nameless_team")
|
||||
);
|
||||
};
|
||||
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({
|
||||
Page: TeamPage,
|
||||
getData,
|
||||
getLayout: null,
|
||||
isBookingPage: true,
|
||||
})<"P">;
|
||||
@@ -1,45 +0,0 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps as _PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { EventRepository } from "@calcom/lib/server/repository/event";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
|
||||
|
||||
import type { PageProps } from "~/team/type-view";
|
||||
import TypePage from "~/team/type-view";
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const props = await getData(legacyCtx);
|
||||
const { user: username, slug: eventSlug, booking } = props;
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(legacyCtx.req, legacyCtx.params?.orgSlug);
|
||||
|
||||
const event = await EventRepository.getPublicEvent({
|
||||
username,
|
||||
eventSlug,
|
||||
isTeamEvent: true,
|
||||
org: isValidOrgDomain ? currentOrgDomain : null,
|
||||
fromRedirectOfNonOrgLink: legacyCtx.query.orgRedirection === "true",
|
||||
});
|
||||
|
||||
const profileName = event?.profile?.name ?? "";
|
||||
const title = event?.title ?? "";
|
||||
|
||||
return await _generateMetadata(
|
||||
(t) => `${booking?.uid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
|
||||
(t) => `${booking?.uid ? t("reschedule") : ""} ${title}`
|
||||
);
|
||||
};
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({
|
||||
Page: TypePage,
|
||||
getData,
|
||||
getLayout: null,
|
||||
isBookingPage: true,
|
||||
})<"P">;
|
||||
@@ -1,29 +0,0 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps as _PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
|
||||
|
||||
import type { PageProps } from "~/team/team-view";
|
||||
import TeamPage from "~/team/team-view";
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
|
||||
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
|
||||
|
||||
return await _generateMetadata(
|
||||
(t) => props.team.name || t("nameless_team"),
|
||||
(t) => props.team.name || t("nameless_team")
|
||||
);
|
||||
};
|
||||
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({
|
||||
Page: TeamPage,
|
||||
getData,
|
||||
getLayout: null,
|
||||
isBookingPage: true,
|
||||
})<"P">;
|
||||
@@ -1,43 +1,11 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import withEmbedSsrAppDir from "app/WithEmbedSSR";
|
||||
import type { PageProps as _PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { EventRepository } from "@calcom/lib/server/repository/event";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
|
||||
|
||||
import type { PageProps } from "~/team/type-view";
|
||||
import LegacyPage from "~/team/type-view";
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const props = await getData(legacyCtx);
|
||||
const { user: username, slug: eventSlug, booking } = props;
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(legacyCtx.req, legacyCtx.params?.orgSlug);
|
||||
|
||||
const event = await EventRepository.getPublicEvent({
|
||||
username,
|
||||
eventSlug,
|
||||
isTeamEvent: true,
|
||||
org: isValidOrgDomain ? currentOrgDomain : null,
|
||||
fromRedirectOfNonOrgLink: legacyCtx.query.orgRedirection === "true",
|
||||
});
|
||||
|
||||
const profileName = event?.profile?.name ?? "";
|
||||
const title = event?.title ?? "";
|
||||
|
||||
return await _generateMetadata(
|
||||
(t) => `${booking?.uid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
|
||||
(t) => `${booking?.uid ? t("reschedule") : ""} ${title}`
|
||||
);
|
||||
};
|
||||
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
const getEmbedData = withEmbedSsrAppDir(getData);
|
||||
const getEmbedData = withEmbedSsrAppDir<PageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({ getLayout: null, getData: getEmbedData, Page: LegacyPage })<"P">;
|
||||
|
||||
@@ -1,26 +1,11 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import withEmbedSsrAppDir from "app/WithEmbedSSR";
|
||||
import type { PageProps as _PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
|
||||
|
||||
import type { PageProps } from "~/team/team-view";
|
||||
import LegacyPage from "~/team/team-view";
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
|
||||
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
|
||||
|
||||
return await _generateMetadata(
|
||||
(t) => props.team.name || t("nameless_team"),
|
||||
(t) => props.team.name || t("nameless_team")
|
||||
);
|
||||
};
|
||||
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
const getEmbedData = withEmbedSsrAppDir(getData);
|
||||
const getEmbedData = withEmbedSsrAppDir<PageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({ getLayout: null, getData: getEmbedData, Page: LegacyPage })<"P">;
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import type { TFunction } from "next-i18next";
|
||||
|
||||
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
|
||||
import { generateMeetingMetadata } from "./_utils";
|
||||
|
||||
type MetadataPayload = {
|
||||
title: (t: TFunction) => string;
|
||||
description: (t: TFunction) => string;
|
||||
meeting: {
|
||||
title: string;
|
||||
profile: { name: string; image: string };
|
||||
users?: { name: string; username: string }[];
|
||||
};
|
||||
robots: {
|
||||
index: boolean;
|
||||
follow: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
type UserProfilePageProfile = {
|
||||
name: string | null;
|
||||
username: string | null;
|
||||
image: string | null;
|
||||
markdownStrippedBio: string | null | undefined;
|
||||
};
|
||||
|
||||
type TeamProfilePageProfile = {
|
||||
teamName: string | null;
|
||||
image: string | null;
|
||||
markdownStrippedBio: string | null | undefined;
|
||||
};
|
||||
|
||||
type EventBookingPageProfile = { name: string; image: string };
|
||||
type BaseMetadataArgs = {
|
||||
hideBranding: boolean;
|
||||
orgSlug: string | null;
|
||||
isSEOIndexable: boolean;
|
||||
isReschedule?: boolean;
|
||||
};
|
||||
|
||||
type TeamProfilePageArgs = BaseMetadataArgs & {
|
||||
profile: TeamProfilePageProfile;
|
||||
event: null;
|
||||
};
|
||||
|
||||
type UserProfilePageArgs = BaseMetadataArgs & {
|
||||
profile: UserProfilePageProfile;
|
||||
event: null;
|
||||
};
|
||||
|
||||
type EventBookingPageArgs = BaseMetadataArgs & {
|
||||
profile: EventBookingPageProfile;
|
||||
event: {
|
||||
title: string;
|
||||
hidden: boolean;
|
||||
users: { name: string | null; username: string | null }[];
|
||||
};
|
||||
};
|
||||
|
||||
export const generateEventBookingPageMetadata = async (props: EventBookingPageArgs) => {
|
||||
const { profile, isReschedule, hideBranding, orgSlug, isSEOIndexable, event } = props;
|
||||
const fullOrigin = getOrgFullOrigin(orgSlug);
|
||||
|
||||
const payload: MetadataPayload = {
|
||||
title: (t) => `${isReschedule ? t("reschedule") : ""} ${event.title} | ${profile.name}`,
|
||||
description: (t) => `${isReschedule ? t("reschedule") : ""} ${event.title}`,
|
||||
meeting: {
|
||||
title: event.title,
|
||||
profile: { name: profile.name, image: profile.image },
|
||||
users: event.users.map((user) => ({
|
||||
name: `${user.name}`,
|
||||
username: `${user.username}`,
|
||||
})),
|
||||
},
|
||||
robots: {
|
||||
index: !(event.hidden || !isSEOIndexable),
|
||||
follow: !(event.hidden || !isSEOIndexable),
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
...(await generateMeetingMetadata(
|
||||
payload.meeting,
|
||||
payload.title,
|
||||
payload.description,
|
||||
hideBranding,
|
||||
fullOrigin
|
||||
)),
|
||||
robots: payload.robots,
|
||||
};
|
||||
};
|
||||
|
||||
export const generateUserProfilePageMetadata = async (props: UserProfilePageArgs) => {
|
||||
const { profile, hideBranding, orgSlug, isSEOIndexable } = props;
|
||||
const fullOrigin = getOrgFullOrigin(orgSlug);
|
||||
|
||||
const payload: MetadataPayload = {
|
||||
title: () => profile.name ?? "",
|
||||
description: () => profile.markdownStrippedBio ?? "",
|
||||
meeting: {
|
||||
title: profile.markdownStrippedBio ?? "",
|
||||
profile: { name: profile.name ?? "", image: profile.image ?? "" },
|
||||
users: [{ name: profile.name ?? "", username: profile.username ?? "" }],
|
||||
},
|
||||
robots: {
|
||||
index: isSEOIndexable,
|
||||
follow: isSEOIndexable,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
...(await generateMeetingMetadata(
|
||||
payload.meeting,
|
||||
payload.title,
|
||||
payload.description,
|
||||
hideBranding,
|
||||
fullOrigin
|
||||
)),
|
||||
robots: payload.robots,
|
||||
};
|
||||
};
|
||||
|
||||
export const generateTeamProfilePageMetadata = async (props: TeamProfilePageArgs) => {
|
||||
const { profile, hideBranding, orgSlug, isSEOIndexable } = props;
|
||||
const fullOrigin = getOrgFullOrigin(orgSlug);
|
||||
|
||||
const payload: MetadataPayload = {
|
||||
title: (t) => profile.teamName || t("nameless_team"),
|
||||
description: () => profile.markdownStrippedBio ?? "",
|
||||
meeting: {
|
||||
title: profile.markdownStrippedBio ?? "",
|
||||
profile: { name: `${profile.teamName}`, image: profile.image ?? "" },
|
||||
},
|
||||
robots: {
|
||||
index: isSEOIndexable,
|
||||
follow: isSEOIndexable,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
...(await generateMeetingMetadata(
|
||||
payload.meeting,
|
||||
payload.title,
|
||||
payload.description,
|
||||
hideBranding,
|
||||
fullOrigin
|
||||
)),
|
||||
robots: payload.robots,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { Icon } from "@calcom/ui";
|
||||
import type { IconName } from "@calcom/ui";
|
||||
|
||||
export const IconGrid = (props: {
|
||||
title: string;
|
||||
icons: IconName[];
|
||||
rootClassName?: string;
|
||||
iconClassName?: string;
|
||||
}) => (
|
||||
<div className={props.rootClassName}>
|
||||
<h2 className="font-cal mt-6 text-lg font-medium">{props.title}</h2>
|
||||
<div className="grid grid-cols-2 lg:grid-cols-6">
|
||||
{props.icons.map((icon) => {
|
||||
return (
|
||||
<div key={icon} className="flex items-center gap-1">
|
||||
<Icon name={icon} className={props.iconClassName} />
|
||||
<div>{icon}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,46 @@
|
||||
import { _generateMetadata, getTranslate } from "app/_utils";
|
||||
import { Inter } from "next/font/google";
|
||||
import localFont from "next/font/local";
|
||||
|
||||
import { type IconName, IconSprites } from "@calcom/ui";
|
||||
|
||||
import { lucideIconList } from "../../../../packages/ui/components/icon/icon-list.mjs";
|
||||
import { IconGrid } from "./IconGrid";
|
||||
|
||||
const interFont = Inter({ subsets: ["latin"], variable: "--font-inter", preload: true, display: "swap" });
|
||||
const calFont = localFont({
|
||||
src: "../../fonts/CalSans-SemiBold.woff2",
|
||||
variable: "--font-cal",
|
||||
preload: true,
|
||||
display: "swap",
|
||||
weight: "600",
|
||||
});
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
(t) => t("icon_showcase"),
|
||||
() => ""
|
||||
);
|
||||
};
|
||||
export default async function IconsPage() {
|
||||
const icons = Array.from(lucideIconList).sort() as IconName[];
|
||||
const t = await getTranslate();
|
||||
|
||||
return (
|
||||
<div className={`${interFont.variable} ${calFont.variable}`}>
|
||||
<div className="bg-subtle flex h-screen">
|
||||
<IconSprites />
|
||||
<div className="bg-default m-auto min-w-full rounded-md p-10 text-right ltr:text-left">
|
||||
<h1 className="text-emphasis font-cal text-2xl font-medium">{t("icons_showcase")}</h1>
|
||||
<IconGrid title="Regular Icons" icons={icons} />
|
||||
<IconGrid
|
||||
title="Filled Icons"
|
||||
icons={icons}
|
||||
rootClassName="bg-darkgray-100 text-gray-50"
|
||||
iconClassName="fill-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export const dynamic = "force-static";
|
||||
@@ -0,0 +1,5 @@
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getInsightsLayout } from "~/insights/layout";
|
||||
|
||||
export default WithLayout({ getLayout: getInsightsLayout });
|
||||
@@ -1,10 +1,8 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { getServerSideProps } from "@lib/insights/getServerSideProps";
|
||||
import { getFeatureFlag } from "@calcom/features/flags/server/utils";
|
||||
|
||||
import type { PageProps } from "~/insights/insights-view";
|
||||
import InsightsPage from "~/insights/insights-view";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
@@ -13,6 +11,13 @@ export const generateMetadata = async () =>
|
||||
(t) => t("insights_subtitle")
|
||||
);
|
||||
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
export default async function Page() {
|
||||
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
|
||||
const insightsEnabled = await getFeatureFlag(prisma, "insights");
|
||||
|
||||
export default WithLayout({ getLayout: null, getData, Page: InsightsPage });
|
||||
if (!insightsEnabled) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
return <InsightsPage />;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { getServerSideProps } from "@lib/insights/getServerSideProps";
|
||||
import { getFeatureFlag } from "@calcom/features/flags/server/utils";
|
||||
|
||||
import InsightsRoutingPage from "~/insights/insights-routing-view";
|
||||
|
||||
@@ -12,6 +11,13 @@ export const generateMetadata = async () =>
|
||||
(t) => t("insights_subtitle")
|
||||
);
|
||||
|
||||
const getData = withAppDirSsr(getServerSideProps);
|
||||
export default async function Page() {
|
||||
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
|
||||
const insightsEnabled = await getFeatureFlag(prisma, "insights");
|
||||
|
||||
export default WithLayout({ getLayout: null, getData, Page: InsightsRoutingPage });
|
||||
if (!insightsEnabled) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
return <InsightsRoutingPage />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { getFeatureFlag } from "@calcom/features/flags/server/utils";
|
||||
|
||||
import InsightsVirtualQueuesPage from "~/insights/insights-virtual-queues-view";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("insights"),
|
||||
(t) => t("insights_subtitle")
|
||||
);
|
||||
|
||||
export default async function Page() {
|
||||
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
|
||||
const insightsEnabled = await getFeatureFlag(prisma, "insights");
|
||||
|
||||
if (!insightsEnabled) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
return <InsightsVirtualQueuesPage />;
|
||||
}
|
||||
+8
-12
@@ -1,4 +1,3 @@
|
||||
import { TrpcProvider } from "app/_trpc/trpc-provider";
|
||||
import { dir } from "i18next";
|
||||
import { Inter } from "next/font/google";
|
||||
import localFont from "next/font/local";
|
||||
@@ -11,6 +10,7 @@ import { IconSprites } from "@calcom/ui";
|
||||
import { prepareRootMetadata } from "@lib/metadata";
|
||||
|
||||
import "../styles/globals.css";
|
||||
import { Providers } from "./providers";
|
||||
|
||||
const interFont = Inter({ subsets: ["latin"], variable: "--font-inter", preload: true, display: "swap" });
|
||||
const calFont = localFont({
|
||||
@@ -21,20 +21,15 @@ const calFont = localFont({
|
||||
weight: "600",
|
||||
});
|
||||
|
||||
export const generateMetadata = () =>
|
||||
prepareRootMetadata({
|
||||
twitterCreator: "@calcom",
|
||||
twitterSite: "@calcom",
|
||||
robots: {
|
||||
index: false,
|
||||
follow: false,
|
||||
},
|
||||
});
|
||||
export const generateMetadata = () => prepareRootMetadata();
|
||||
|
||||
const getInitialProps = async (url: string) => {
|
||||
const { pathname, searchParams } = new URL(url);
|
||||
|
||||
const isEmbed = pathname.endsWith("/embed") || (searchParams?.get("embedType") ?? null) !== null;
|
||||
const isEmbedSnippetGeneratorPath = pathname.startsWith("/event-types");
|
||||
const isEmbed =
|
||||
(pathname.endsWith("/embed") || (searchParams?.get("embedType") ?? null) !== null) &&
|
||||
!isEmbedSnippetGeneratorPath;
|
||||
const embedColorScheme = searchParams?.get("ui.color-scheme");
|
||||
|
||||
const req = { headers: headers(), cookies: cookies() };
|
||||
@@ -84,6 +79,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
<script
|
||||
nonce={nonce}
|
||||
id="headScript"
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
window.calNewLocale = "${locale}";
|
||||
@@ -152,7 +148,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<TrpcProvider>{children}</TrpcProvider>
|
||||
<Providers>{children}</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { generateEventBookingPageMetadata } from "app/generateBookingPageMetadata";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
|
||||
|
||||
import type { PageProps as TeamTypePageProps } from "~/team/type-view";
|
||||
import TeamTypePage from "~/team/type-view";
|
||||
import UserTypePage from "~/users/views/users-type-public-view";
|
||||
import type { PageProps as UserTypePageProps } from "~/users/views/users-type-public-view";
|
||||
|
||||
export type OrgTypePageProps = UserTypePageProps | TeamTypePageProps;
|
||||
const getData = withAppDirSsr<OrgTypePageProps>(getServerSideProps);
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const props = await getData(legacyCtx);
|
||||
|
||||
const { booking, isSEOIndexable, eventData, isBrandingHidden } = props;
|
||||
const rescheduleUid = booking?.uid;
|
||||
|
||||
const profileName = eventData?.profile?.name ?? "";
|
||||
const profileImage = eventData?.profile.image ?? "";
|
||||
|
||||
return await generateEventBookingPageMetadata({
|
||||
profile: { name: profileName, image: profileImage },
|
||||
event: {
|
||||
title: eventData?.title ?? "",
|
||||
hidden: eventData?.hidden ?? false,
|
||||
users: [
|
||||
...(eventData?.users || []).map((user) => ({
|
||||
name: `${user.name}`,
|
||||
username: `${user.username}`,
|
||||
})),
|
||||
],
|
||||
},
|
||||
hideBranding: isBrandingHidden,
|
||||
orgSlug: eventData?.entity.orgSlug ?? null,
|
||||
isSEOIndexable: !!isSEOIndexable,
|
||||
isReschedule: !!rescheduleUid,
|
||||
});
|
||||
};
|
||||
|
||||
const Page = async (props: OrgTypePageProps) => {
|
||||
if ((props as TeamTypePageProps)?.teamId) {
|
||||
return <TeamTypePage {...(props as TeamTypePageProps)} />;
|
||||
}
|
||||
return <UserTypePage {...(props as UserTypePageProps)} />;
|
||||
};
|
||||
|
||||
export default WithLayout({ getLayout: null, getData, ServerPage: Page, isBookingPage: true });
|
||||
@@ -0,0 +1,64 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps } from "app/_types";
|
||||
import {
|
||||
generateTeamProfilePageMetadata,
|
||||
generateUserProfilePageMetadata,
|
||||
} from "app/generateBookingPageMetadata";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { getOrgOrTeamAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/getServerSideProps";
|
||||
|
||||
import type { PageProps as TeamPageProps } from "~/team/team-view";
|
||||
import TeamPage from "~/team/team-view";
|
||||
import UserPage from "~/users/views/users-public-view";
|
||||
import type { PageProps as UserPageProps } from "~/users/views/users-public-view";
|
||||
|
||||
export type OrgPageProps = UserPageProps | TeamPageProps;
|
||||
const getData = withAppDirSsr<OrgPageProps>(getServerSideProps);
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const props = await getData(legacyCtx);
|
||||
|
||||
if ((props as TeamPageProps)?.team) {
|
||||
const { team, markdownStrippedBio, isSEOIndexable, currentOrgDomain } = props as TeamPageProps;
|
||||
return await generateTeamProfilePageMetadata({
|
||||
profile: {
|
||||
teamName: team.name,
|
||||
image: getOrgOrTeamAvatar(team),
|
||||
markdownStrippedBio: markdownStrippedBio,
|
||||
},
|
||||
event: null,
|
||||
hideBranding: false,
|
||||
orgSlug: currentOrgDomain ?? null,
|
||||
isSEOIndexable: !!isSEOIndexable,
|
||||
});
|
||||
} else {
|
||||
const { profile, markdownStrippedBio, isOrgSEOIndexable, entity } = props as UserPageProps;
|
||||
|
||||
const isOrg = !!profile?.organization;
|
||||
const allowSEOIndexing =
|
||||
(!isOrg && profile.allowSEOIndexing) || (isOrg && isOrgSEOIndexable && profile.allowSEOIndexing);
|
||||
|
||||
return await generateUserProfilePageMetadata({
|
||||
profile: { name: profile.name, username: profile.username, image: profile.image, markdownStrippedBio },
|
||||
hideBranding: false,
|
||||
orgSlug: entity.orgSlug ?? null,
|
||||
isSEOIndexable: !!allowSEOIndexing,
|
||||
event: null,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const Page = async (props: OrgPageProps) => {
|
||||
if ((props as TeamPageProps)?.team) {
|
||||
return <TeamPage {...(props as TeamPageProps)} />;
|
||||
}
|
||||
return <UserPage {...(props as UserPageProps)} />;
|
||||
};
|
||||
|
||||
export default WithLayout({ getLayout: null, getData, ServerPage: Page, isBookingPage: true });
|
||||
+4
-4
@@ -4,7 +4,6 @@ import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { headers, cookies } from "next/headers";
|
||||
|
||||
import { getLayout } from "@calcom/features/MainLayoutAppDir";
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { EventRepository } from "@calcom/lib/server/repository/event";
|
||||
|
||||
@@ -16,7 +15,7 @@ import Page from "~/org/[orgSlug]/instant-meeting/team/[slug]/[type]/instant-mee
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
|
||||
const context = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const { slug: eventSlug, user: username } = await getData(context);
|
||||
const { slug: eventSlug, user: username, isBrandingHidden } = await getData(context);
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
|
||||
|
||||
const org = isValidOrgDomain ? currentOrgDomain : null;
|
||||
@@ -34,9 +33,10 @@ export const generateMetadata = async ({ params, searchParams }: _PageProps) =>
|
||||
|
||||
return await _generateMetadata(
|
||||
() => `${title} | ${profileName}`,
|
||||
() => `${title}`
|
||||
() => `${title}`,
|
||||
isBrandingHidden
|
||||
);
|
||||
};
|
||||
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
export default WithLayout({ getLayout, getData, Page })<"P">;
|
||||
export default WithLayout({ getData, Page, isBookingPage: true })<"P">;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user