Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c581456586 | ||
|
|
36dd3e9e6c | ||
|
|
f137aee436 | ||
|
|
4f494d5a32 | ||
|
|
cce6e83578 | ||
|
|
c21ba636d2 | ||
|
|
5ccf5e6be8 | ||
|
|
66b3e735d8 | ||
|
|
7a8b9ededb | ||
|
|
501c49f369 | ||
|
|
536604455b | ||
|
|
cfb76a9689 | ||
|
|
30180edc0b | ||
|
|
2aa3d1d4e0 | ||
|
|
21694c039a | ||
|
|
a8c29b5459 | ||
|
|
6cd7454f43 | ||
|
|
99b2419dd7 | ||
|
|
f6d0a2ca09 | ||
|
|
f5ab9bd40b | ||
|
|
07085b9ffb | ||
|
|
14151b827f | ||
|
|
28b757f19c | ||
|
|
68455db860 | ||
|
|
96ff88d266 | ||
|
|
f88a91dad0 | ||
|
|
35059d7727 | ||
|
|
68f76e2acc | ||
|
|
80aac06ff3 | ||
|
|
0f47528ba2 | ||
|
|
113dc5fca6 | ||
|
|
0befdde5d7 | ||
|
|
b3ed6e39e3 | ||
|
|
e07b1eda32 | ||
|
|
8262894bde | ||
|
|
a3d7bcd22e | ||
|
|
62b753afba | ||
|
|
bb40b2f6bf | ||
|
|
f628dc83a8 | ||
|
|
2fd2578504 | ||
|
|
a29407349d | ||
|
|
e7df1ef493 | ||
|
|
b652ae7714 | ||
|
|
cec1c9ee46 | ||
|
|
3813fd70f7 | ||
|
|
dabd3b0fc3 | ||
|
|
95f9a02d29 | ||
|
|
f26d2998cb | ||
|
|
f558f8aca5 | ||
|
|
a6dd5185ea | ||
|
|
3da7694002 | ||
|
|
1ffb376789 | ||
|
|
8761b91f71 | ||
|
|
1dbef7f8f6 | ||
|
|
d4d5ac2b28 | ||
|
|
88c8ad8b5c | ||
|
|
0a08680084 | ||
|
|
65828545b9 | ||
|
|
3adb8e84fc | ||
|
|
2a29155641 | ||
|
|
4902c6a0f8 | ||
|
|
a22fd0c151 | ||
|
|
b6bd0eda43 | ||
|
|
25d052e8f0 | ||
|
|
f933432d2b | ||
|
|
ccf1d4430a | ||
|
|
2fd8d24204 | ||
|
|
b4c18f7bcc | ||
|
|
23c93d75ca | ||
|
|
91526d0bc7 | ||
|
|
a9f95937c4 | ||
|
|
74580157ba | ||
|
|
fcdbcceac0 | ||
|
|
aa7c924c2a | ||
|
|
3406ca72bc | ||
|
|
c29a2fc7cb | ||
|
|
0a4c7bcecc | ||
|
|
803dfa008c | ||
|
|
0eb6f1beb6 | ||
|
|
edd2e5e584 | ||
|
|
99a99b4b7e | ||
|
|
2ba6f4cb24 | ||
|
|
8a8c870513 | ||
|
|
eba335c903 | ||
|
|
9b2161cc19 | ||
|
|
b997c656df | ||
|
|
c9c5493de6 | ||
|
|
20a5775f31 | ||
|
|
b9b0a86b32 | ||
|
|
ad93066cf6 | ||
|
|
e6d76b630c | ||
|
|
b941b716d5 | ||
|
|
1ab9622416 | ||
|
|
15156c2225 | ||
|
|
cc202ed847 | ||
|
|
79d51a4989 | ||
|
|
53a846d10c | ||
|
|
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 |
+6
-10
@@ -114,6 +114,7 @@ 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=
|
||||
@@ -354,16 +355,6 @@ E2E_TEST_OIDC_USER_PASSWORD=
|
||||
# provide a value between 0 and 100 to ensure the percentage of traffic
|
||||
# redirected from the legacy to the future pages
|
||||
AB_TEST_BUCKET_PROBABILITY=50
|
||||
# whether we redirect to the future/event-types from event-types or not
|
||||
APP_ROUTER_EVENT_TYPES_ENABLED=0
|
||||
APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED=0
|
||||
APP_ROUTER_APPS_SLUG_ENABLED=0
|
||||
APP_ROUTER_APPS_SLUG_SETUP_ENABLED=0
|
||||
# whether we redirect to the future/apps/categories from /apps/categories or not
|
||||
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_APPS_ENABLED=0
|
||||
APP_ROUTER_TEAM_ENABLED=0
|
||||
APP_ROUTER_AUTH_FORGOT_PASSWORD_ENABLED=0
|
||||
APP_ROUTER_AUTH_LOGIN_ENABLED=0
|
||||
@@ -415,3 +406,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=
|
||||
|
||||
@@ -12,16 +12,6 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- name: Run Linting with Reports
|
||||
run: yarn lint:report
|
||||
- name: Run Lint
|
||||
run: yarn lint
|
||||
continue-on-error: true
|
||||
|
||||
- name: Merge lint reports
|
||||
run: jq -s '[.[]]|flatten' lint-results/*.json &> lint-results/eslint_report.json
|
||||
|
||||
- name: Upload ESLint report
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lint-results
|
||||
path: lint-results
|
||||
|
||||
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
+3
@@ -0,0 +1,3 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@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/prisma"
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@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
+3
@@ -0,0 +1,3 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/prisma"
|
||||
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();
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.78",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.82",
|
||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
@@ -59,6 +59,7 @@
|
||||
"googleapis": "^84.0.0",
|
||||
"helmet": "^7.1.0",
|
||||
"ioredis": "^5.3.2",
|
||||
"jsforce": "^1.11.0",
|
||||
"luxon": "^3.4.4",
|
||||
"nest-winston": "^1.9.4",
|
||||
"next-auth": "^4.22.1",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -59,6 +59,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
|
||||
let team1EventTypeId: number;
|
||||
let team2EventTypeId: number;
|
||||
let phoneOnlyEventTypeId: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
@@ -205,6 +206,83 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
|
||||
team1EventTypeId = team1EventType.id;
|
||||
|
||||
const phoneOnlyEventType = await eventTypesRepositoryFixture.createTeamEventType({
|
||||
schedulingType: "ROUND_ROBIN",
|
||||
team: {
|
||||
connect: { id: team1.id },
|
||||
},
|
||||
title: "Phone Only Event Type",
|
||||
slug: "phone-only-event-type",
|
||||
length: 15,
|
||||
assignAllTeamMembers: false,
|
||||
hosts: {
|
||||
connectOrCreate: [
|
||||
{
|
||||
where: {
|
||||
userId_eventTypeId: {
|
||||
userId: teamUser.id,
|
||||
eventTypeId: team1EventTypeId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
userId: teamUser.id,
|
||||
isFixed: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
bookingFields: [
|
||||
{
|
||||
name: "name",
|
||||
type: "name",
|
||||
label: "your name",
|
||||
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||
variant: "fullName",
|
||||
editable: "system",
|
||||
required: true,
|
||||
defaultLabel: "your_name",
|
||||
variantsConfig: {
|
||||
variants: {
|
||||
fullName: {
|
||||
fields: [{ name: "fullName", type: "text", label: "your name", required: true }],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "email",
|
||||
type: "email",
|
||||
label: "your email",
|
||||
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||
editable: "system",
|
||||
required: false,
|
||||
defaultLabel: "email_address",
|
||||
},
|
||||
{
|
||||
name: "attendeePhoneNumber",
|
||||
type: "phone",
|
||||
label: "phone_number",
|
||||
sources: [{ id: "user", type: "user", label: "User", fieldRequired: true }],
|
||||
editable: "user",
|
||||
required: true,
|
||||
placeholder: "",
|
||||
},
|
||||
{
|
||||
name: "rescheduleReason",
|
||||
type: "textarea",
|
||||
views: [{ id: "reschedule", label: "Reschedule View" }],
|
||||
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||
editable: "system-but-optional",
|
||||
required: false,
|
||||
defaultLabel: "reason_for_reschedule",
|
||||
defaultPlaceholder: "reschedule_placeholder",
|
||||
},
|
||||
],
|
||||
locations: [],
|
||||
});
|
||||
|
||||
phoneOnlyEventTypeId = phoneOnlyEventType.id;
|
||||
|
||||
const team2EventType = await eventTypesRepositoryFixture.createTeamEventType({
|
||||
schedulingType: "COLLECTIVE",
|
||||
team: {
|
||||
@@ -322,6 +400,60 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should create a phone based booking", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 15, 0, 0)).toISOString(),
|
||||
eventTypeId: phoneOnlyEventTypeId,
|
||||
attendee: {
|
||||
name: "alice",
|
||||
phoneNumber: "+919876543210",
|
||||
timeZone: "Europe/Madrid",
|
||||
language: "es",
|
||||
},
|
||||
meetingUrl: "https://meet.google.com/abc-def-ghi",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts.length).toEqual(1);
|
||||
expect(data.hosts[0].id).toEqual(teamUser.id);
|
||||
expect(data.status).toEqual("accepted");
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 15, 15, 0)).toISOString());
|
||||
expect(data.duration).toEqual(15);
|
||||
expect(data.eventTypeId).toEqual(phoneOnlyEventTypeId);
|
||||
expect(data.attendees.length).toEqual(1);
|
||||
expect(data.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: "919876543210@sms.cal.com",
|
||||
phoneNumber: body.attendee.phoneNumber,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
});
|
||||
expect(data.meetingUrl).toEqual(body.meetingUrl);
|
||||
expect(data.absentHost).toEqual(false);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should create a team 2 booking", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 10, 0, 0)).toISOString(),
|
||||
@@ -398,7 +530,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(1);
|
||||
expect(data.length).toEqual(2);
|
||||
expect(data[0].eventTypeId).toEqual(team1EventTypeId);
|
||||
});
|
||||
});
|
||||
@@ -436,7 +568,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(2);
|
||||
expect(data.length).toEqual(3);
|
||||
expect(data.find((booking) => booking.eventTypeId === team1EventTypeId)).toBeDefined();
|
||||
expect(data.find((booking) => booking.eventTypeId === team2EventTypeId)).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -13,7 +13,6 @@ import { Request } from "express";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
handleNewBooking,
|
||||
handleNewRecurringBooking,
|
||||
getAllUserBookings,
|
||||
handleInstantMeeting,
|
||||
@@ -23,6 +22,7 @@ import {
|
||||
handleMarkNoShow,
|
||||
confirmBookingHandler,
|
||||
} from "@calcom/platform-libraries";
|
||||
import { handleNewBooking } from "@calcom/platform-libraries";
|
||||
import {
|
||||
CreateBookingInput_2024_08_13,
|
||||
CreateBookingInput,
|
||||
|
||||
@@ -160,9 +160,14 @@ export class InputBookingsService_2024_08_13 {
|
||||
? {
|
||||
...inputBooking.bookingFieldsResponses,
|
||||
name: inputBooking.attendee.name,
|
||||
email: inputBooking.attendee.email,
|
||||
email: inputBooking.attendee.email ?? "",
|
||||
attendeePhoneNumber: inputBooking.attendee.phoneNumber,
|
||||
}
|
||||
: { name: inputBooking.attendee.name, email: inputBooking.attendee.email },
|
||||
: {
|
||||
name: inputBooking.attendee.name,
|
||||
email: inputBooking.attendee.email ?? "",
|
||||
attendeePhoneNumber: inputBooking.attendee.phoneNumber,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ type DatabaseBooking = Booking & {
|
||||
email: string;
|
||||
timeZone: string;
|
||||
locale: string | null;
|
||||
phoneNumber?: string | null;
|
||||
noShow: boolean | null;
|
||||
bookingSeat?: BookingSeat | null;
|
||||
}[];
|
||||
@@ -121,6 +122,7 @@ export class OutputBookingsService_2024_08_13 {
|
||||
timeZone: attendee.timeZone,
|
||||
language: attendee.locale,
|
||||
absent: !!attendee.noShow,
|
||||
phoneNumber: attendee.phoneNumber ?? undefined,
|
||||
})),
|
||||
guests: bookingResponses.guests,
|
||||
location,
|
||||
|
||||
+164
-84
@@ -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",
|
||||
@@ -379,30 +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", disableOnPrefill: false },
|
||||
// note(Lauris): location booking field is added if multiple locations are passed
|
||||
{ ...defaultResponseBookingFieldName, ...nameBookingField },
|
||||
{ ...defaultResponseBookingFieldEmail },
|
||||
{ ...defaultResponseBookingFieldLocation },
|
||||
{ ...defaultResponseBookingFieldTitle },
|
||||
{ ...defaultResponseBookingFieldGuests },
|
||||
{ ...defaultResponseBookingFieldRescheduleReason },
|
||||
{
|
||||
isDefault: true,
|
||||
required: false,
|
||||
slug: "location",
|
||||
type: "radioInput",
|
||||
disableOnPrefill: false,
|
||||
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,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
required: false,
|
||||
slug: "rescheduleReason",
|
||||
type: "textarea",
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
...requestBookingFields
|
||||
.filter((field) => field.type !== "name" && field.type !== "email")
|
||||
.map((field) => ({ isDefault: false, ...field })),
|
||||
{ ...defaultResponseBookingFieldNotes },
|
||||
];
|
||||
|
||||
expect(createdEventType.bookingFields).toEqual(expectedBookingFields);
|
||||
@@ -432,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);
|
||||
@@ -455,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)
|
||||
@@ -636,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),
|
||||
@@ -779,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 = {
|
||||
@@ -804,6 +875,7 @@ describe("Event types Endpoints", () => {
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
notesBookingField,
|
||||
],
|
||||
bookingLimitsCount: {
|
||||
day: 4,
|
||||
@@ -866,21 +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", disableOnPrefill: false },
|
||||
{ ...defaultResponseBookingFieldName, ...nameBookingField },
|
||||
{ ...defaultResponseBookingFieldEmail },
|
||||
{ ...defaultResponseBookingFieldLocation },
|
||||
{ ...defaultResponseBookingFieldTitle },
|
||||
{ ...defaultResponseBookingFieldGuests },
|
||||
{ ...defaultResponseBookingFieldRescheduleReason },
|
||||
{
|
||||
isDefault: true,
|
||||
required: false,
|
||||
slug: "rescheduleReason",
|
||||
type: "textarea",
|
||||
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,
|
||||
},
|
||||
...requestBookingFields
|
||||
.filter((field) => field.type !== "name" && field.type !== "email")
|
||||
.map((field) => ({ isDefault: false, ...field })),
|
||||
{ ...defaultResponseBookingFieldNotes, ...notesBookingField },
|
||||
];
|
||||
|
||||
expect(updatedEventType.bookingFields).toEqual(expectedBookingFields);
|
||||
@@ -997,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);
|
||||
@@ -1091,14 +1169,6 @@ describe("Event types Endpoints", () => {
|
||||
placeholder: "",
|
||||
},
|
||||
{ isDefault: true, required: true, slug: "title", type: "text", disableOnPrefill: false, hidden: true },
|
||||
{
|
||||
isDefault: true,
|
||||
required: false,
|
||||
slug: "notes",
|
||||
type: "textarea",
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
required: false,
|
||||
@@ -1116,6 +1186,14 @@ describe("Event types Endpoints", () => {
|
||||
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 () => {
|
||||
@@ -1239,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",
|
||||
@@ -1283,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);
|
||||
@@ -1369,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",
|
||||
@@ -1413,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);
|
||||
@@ -1426,19 +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,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
]);
|
||||
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)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1541,14 +1621,6 @@ describe("Event types Endpoints", () => {
|
||||
disableOnPrefill: false,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "textarea",
|
||||
slug: "notes",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "multiemail",
|
||||
@@ -1565,6 +1637,14 @@ describe("Event types Endpoints", () => {
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "textarea",
|
||||
slug: "notes",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
+38
-16
@@ -5,16 +5,19 @@ import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable, BadRequestException } from "@nestjs/common";
|
||||
|
||||
import {
|
||||
transformBookingFieldsApiRequestToInternal,
|
||||
transformBookingFieldsApiToInternal,
|
||||
transformLocationsApiToInternal,
|
||||
transformIntervalLimitsApiToInternal,
|
||||
transformFutureBookingLimitsApiToInternal,
|
||||
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
|
||||
? transformBookingFieldsApiRequestToInternal(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"]) {
|
||||
|
||||
+14
-1
@@ -128,6 +128,7 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
const bookingFields = databaseEventType.bookingFields
|
||||
? this.transformBookingFields(databaseEventType.bookingFields)
|
||||
: this.getDefaultBookingFields(isOrgTeamEvent);
|
||||
|
||||
const recurrence = this.transformRecurringEvent(databaseEventType.recurringEvent);
|
||||
const metadata = this.transformMetadata(databaseEventType.metadata) || {};
|
||||
const users = this.transformUsers(databaseEventType.users || []);
|
||||
@@ -242,7 +243,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) {
|
||||
|
||||
@@ -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}'`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { UserOOORepository } from "@/modules/ooo/repositories/ooo.repository";
|
||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
@Injectable()
|
||||
export class IsUserOOO implements CanActivate {
|
||||
constructor(private oooRepo: UserOOORepository) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
const oooId: string = request.params.oooId;
|
||||
const userId: string = request.params.userId;
|
||||
|
||||
if (!userId) {
|
||||
throw new ForbiddenException("No user id found in request params.");
|
||||
}
|
||||
|
||||
if (!oooId) {
|
||||
throw new ForbiddenException("No ooo entry id found in request params.");
|
||||
}
|
||||
|
||||
const ooo = await this.oooRepo.getUserOOOByIdAndUserId(Number(oooId), Number(userId));
|
||||
|
||||
if (ooo) {
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new ForbiddenException("This OOO entry does not belong to this user.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { BadRequestException } from "@nestjs/common";
|
||||
import { ApiProperty, ApiPropertyOptional, PartialType } from "@nestjs/swagger";
|
||||
import { Transform } from "class-transformer";
|
||||
import { IsDate, IsInt, IsOptional, IsString, IsEnum, isDate } from "class-validator";
|
||||
|
||||
import { SkipTakePagination } from "@calcom/platform-types";
|
||||
|
||||
export enum OutOfOfficeReason {
|
||||
UNSPECIFIED = "unspecified",
|
||||
VACATION = "vacation",
|
||||
TRAVEL = "travel",
|
||||
SICK_LEAVE = "sick",
|
||||
PUBLIC_HOLIDAY = "public_holiday",
|
||||
}
|
||||
|
||||
export type OutOfOfficeReasonType = `${OutOfOfficeReason}`;
|
||||
|
||||
const isDateString = (dateString: string) => {
|
||||
try {
|
||||
const isoDateRegex = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d{3})?Z$/;
|
||||
return isoDateRegex.test(dateString);
|
||||
} catch {
|
||||
throw new BadRequestException("Invalid Date.");
|
||||
}
|
||||
};
|
||||
|
||||
export class CreateOutOfOfficeEntryDto {
|
||||
@Transform(({ value }: { value: string }) => {
|
||||
if (isDateString(value)) {
|
||||
const date = new Date(value);
|
||||
date.setUTCHours(0, 0, 0, 0);
|
||||
return date;
|
||||
}
|
||||
throw new BadRequestException("Invalid Date.");
|
||||
})
|
||||
@IsDate()
|
||||
@ApiProperty({
|
||||
description: "The start date and time of the out of office period in ISO 8601 format in UTC timezone.",
|
||||
example: "2023-05-01T00:00:00.000Z",
|
||||
})
|
||||
start!: Date;
|
||||
|
||||
@Transform(({ value }: { value: string }) => {
|
||||
if (isDateString(value)) {
|
||||
const date = new Date(value);
|
||||
date.setUTCHours(23, 59, 59, 999);
|
||||
return date;
|
||||
}
|
||||
throw new BadRequestException("Invalid Date.");
|
||||
})
|
||||
@IsDate()
|
||||
@ApiProperty({
|
||||
description: "The end date and time of the out of office period in ISO 8601 format in UTC timezone.",
|
||||
example: "2023-05-10T23:59:59.999Z",
|
||||
})
|
||||
end!: Date;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
description: "Optional notes for the out of office entry.",
|
||||
example: "Vacation in Hawaii",
|
||||
})
|
||||
notes?: string;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
description: "The ID of the user covering for the out of office period, if applicable.",
|
||||
example: 2,
|
||||
})
|
||||
toUserId?: number;
|
||||
|
||||
@IsEnum(OutOfOfficeReason)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
description: "the reason for the out of office entry, if applicable",
|
||||
example: "vacation",
|
||||
enum: OutOfOfficeReason,
|
||||
})
|
||||
reason?: OutOfOfficeReasonType;
|
||||
}
|
||||
|
||||
export class UpdateOutOfOfficeEntryDto extends PartialType(CreateOutOfOfficeEntryDto) {}
|
||||
|
||||
export enum SortOrder {
|
||||
asc = "asc",
|
||||
desc = "desc",
|
||||
}
|
||||
type SortOrderType = keyof typeof SortOrder;
|
||||
|
||||
export class GetOutOfOfficeEntryFiltersDTO extends SkipTakePagination {
|
||||
@IsOptional()
|
||||
@IsEnum(SortOrder, {
|
||||
message: 'SortStart must be either "asc" or "desc".',
|
||||
})
|
||||
@ApiProperty({
|
||||
required: false,
|
||||
description: "Sort results by their start time in ascending or descending order.",
|
||||
example: "?sortStart=asc OR ?sortStart=desc",
|
||||
enum: SortOrder,
|
||||
})
|
||||
sortStart?: SortOrderType;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(SortOrder, {
|
||||
message: 'SortEnd must be either "asc" or "desc".',
|
||||
})
|
||||
@ApiProperty({
|
||||
required: false,
|
||||
description: "Sort results by their end time in ascending or descending order.",
|
||||
example: "?sortEnd=asc OR ?sortEnd=desc",
|
||||
enum: SortOrder,
|
||||
})
|
||||
sortEnd?: SortOrderType;
|
||||
}
|
||||
|
||||
export class GetOrgUsersOutOfOfficeEntryFiltersDTO extends GetOutOfOfficeEntryFiltersDTO {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
required: false,
|
||||
description: "Filter ooo entries by the user email address. user must be within your organization.",
|
||||
example: "example@domain.com",
|
||||
})
|
||||
email?: string;
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { OutOfOfficeReason } from "@/modules/ooo/inputs/ooo.input";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsInt, IsEnum, ValidateNested, IsString, IsDateString, IsOptional } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class UserOooOutputDto {
|
||||
@IsInt()
|
||||
@Expose()
|
||||
@ApiProperty({
|
||||
description: "The ID of the user.",
|
||||
example: 2,
|
||||
})
|
||||
readonly userId!: number;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
description: "The ID of the user covering for the out of office period, if applicable.",
|
||||
example: 2,
|
||||
})
|
||||
@Expose()
|
||||
readonly toUserId?: number;
|
||||
|
||||
@IsInt()
|
||||
@Expose()
|
||||
@ApiProperty({
|
||||
description: "The ID of the ooo entry.",
|
||||
example: 2,
|
||||
})
|
||||
readonly id!: number;
|
||||
|
||||
@IsString()
|
||||
@Expose()
|
||||
@ApiProperty({
|
||||
description: "The UUID of the ooo entry.",
|
||||
example: 2,
|
||||
})
|
||||
readonly uuid!: string;
|
||||
|
||||
@IsDateString()
|
||||
@ApiProperty({
|
||||
description: "The start date and time of the out of office period in ISO 8601 format in UTC timezone.",
|
||||
example: "2023-05-01T00:00:00.000Z",
|
||||
})
|
||||
@Expose()
|
||||
start!: Date;
|
||||
|
||||
@IsDateString()
|
||||
@ApiProperty({
|
||||
description: "The end date and time of the out of office period in ISO 8601 format in UTC timezone.",
|
||||
example: "2023-05-10T23:59:59.999Z",
|
||||
})
|
||||
@Expose()
|
||||
end!: Date;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
description: "Optional notes for the out of office entry.",
|
||||
example: "Vacation in Hawaii",
|
||||
})
|
||||
@Expose()
|
||||
notes?: string;
|
||||
|
||||
@IsEnum(OutOfOfficeReason)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
description: "the reason for the out of office entry, if applicable",
|
||||
example: "vacation",
|
||||
enum: OutOfOfficeReason,
|
||||
})
|
||||
@Expose()
|
||||
reason?: OutOfOfficeReason;
|
||||
}
|
||||
|
||||
export class UserOooOutputResponseDto {
|
||||
@Expose()
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@Type(() => UserOooOutputDto)
|
||||
data!: UserOooOutputDto;
|
||||
}
|
||||
|
||||
export class UserOoosOutputResponseDto {
|
||||
@Expose()
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@Type(() => UserOooOutputDto)
|
||||
data!: UserOooOutputDto[];
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { Prisma } from "@calcom/prisma/client";
|
||||
|
||||
import { PrismaWriteService } from "../../prisma/prisma-write.service";
|
||||
|
||||
type OOOInputData = Omit<Prisma.OutOfOfficeEntryCreateInput, "user" | "toUser" | "reason" | "uuid"> & {
|
||||
toUserId?: number;
|
||||
userId: number;
|
||||
reasonId?: number;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class UserOOORepository {
|
||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||
|
||||
async createUserOOO(data: OOOInputData) {
|
||||
const uuid = uuidv4();
|
||||
return this.dbWrite.prisma.outOfOfficeEntry.create({
|
||||
data: { ...data, uuid },
|
||||
include: { reason: true },
|
||||
});
|
||||
}
|
||||
|
||||
async updateUserOOO(oooId: number, data: Partial<OOOInputData>) {
|
||||
return this.dbWrite.prisma.outOfOfficeEntry.update({
|
||||
where: { id: oooId },
|
||||
data,
|
||||
include: { reason: true },
|
||||
});
|
||||
}
|
||||
|
||||
async getUserOOOById(oooId: number) {
|
||||
return this.dbRead.prisma.outOfOfficeEntry.findFirst({
|
||||
where: { id: oooId },
|
||||
include: { reason: true },
|
||||
});
|
||||
}
|
||||
|
||||
async getUserOOOByIdAndUserId(oooId: number, userId: number) {
|
||||
return this.dbRead.prisma.outOfOfficeEntry.findFirst({
|
||||
where: { id: oooId, userId },
|
||||
include: { reason: true },
|
||||
});
|
||||
}
|
||||
|
||||
async getUserOOOPaginated(
|
||||
userId: number,
|
||||
skip: number,
|
||||
take: number,
|
||||
sort?: { sortStart?: "asc" | "desc"; sortEnd?: "asc" | "desc" }
|
||||
) {
|
||||
return this.dbRead.prisma.outOfOfficeEntry.findMany({
|
||||
where: { userId },
|
||||
skip,
|
||||
take,
|
||||
include: { reason: true },
|
||||
...(sort?.sortStart && { orderBy: { start: sort.sortStart } }),
|
||||
...(sort?.sortEnd && { orderBy: { end: sort.sortEnd } }),
|
||||
});
|
||||
}
|
||||
|
||||
async deleteUserOOO(oooId: number) {
|
||||
return this.dbWrite.prisma.outOfOfficeEntry.delete({
|
||||
where: { id: oooId },
|
||||
include: { reason: true },
|
||||
});
|
||||
}
|
||||
|
||||
async findExistingOooRedirect(userId: number, start: Date, end: Date, toUserId?: number) {
|
||||
const existingOutOfOfficeEntry = await this.dbRead.prisma.outOfOfficeEntry.findFirst({
|
||||
select: {
|
||||
userId: true,
|
||||
toUserId: true,
|
||||
},
|
||||
where: {
|
||||
...(toUserId && { userId: toUserId }),
|
||||
toUserId: userId,
|
||||
// Check for time overlap or collision
|
||||
OR: [
|
||||
// Outside of range
|
||||
{
|
||||
AND: [{ start: { lte: end } }, { end: { gte: start } }],
|
||||
},
|
||||
// Inside of range
|
||||
{
|
||||
AND: [{ start: { gte: start } }, { end: { lte: end } }],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
return existingOutOfOfficeEntry;
|
||||
}
|
||||
|
||||
async getOooByUserIdAndTime(userId: number, start: Date, end: Date) {
|
||||
return await this.dbRead.prisma.outOfOfficeEntry.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
start,
|
||||
end,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import {
|
||||
CreateOutOfOfficeEntryDto,
|
||||
UpdateOutOfOfficeEntryDto,
|
||||
OutOfOfficeReason,
|
||||
GetOutOfOfficeEntryFiltersDTO,
|
||||
SortOrder,
|
||||
} from "@/modules/ooo/inputs/ooo.input";
|
||||
import { UserOOORepository } from "@/modules/ooo/repositories/ooo.repository";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { BadRequestException, ConflictException, Injectable } from "@nestjs/common";
|
||||
|
||||
import { OutOfOfficeEntry } from "@calcom/prisma/client";
|
||||
|
||||
const OOO_REASON_ID_TO_REASON = {
|
||||
1: OutOfOfficeReason["UNSPECIFIED"],
|
||||
2: OutOfOfficeReason["VACATION"],
|
||||
3: OutOfOfficeReason["TRAVEL"],
|
||||
4: OutOfOfficeReason["SICK_LEAVE"],
|
||||
5: OutOfOfficeReason["PUBLIC_HOLIDAY"],
|
||||
};
|
||||
|
||||
const OOO_REASON_TO_REASON_ID = {
|
||||
[OutOfOfficeReason["UNSPECIFIED"]]: 1,
|
||||
[OutOfOfficeReason["VACATION"]]: 2,
|
||||
[OutOfOfficeReason["TRAVEL"]]: 3,
|
||||
[OutOfOfficeReason["SICK_LEAVE"]]: 4,
|
||||
[OutOfOfficeReason["PUBLIC_HOLIDAY"]]: 5,
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class UserOOOService {
|
||||
constructor(
|
||||
private readonly oooRepository: UserOOORepository,
|
||||
private readonly usersRepository: UsersRepository
|
||||
) {}
|
||||
|
||||
formatOooReason(ooo: OutOfOfficeEntry) {
|
||||
return {
|
||||
...ooo,
|
||||
reason: ooo.reasonId
|
||||
? OOO_REASON_ID_TO_REASON[ooo.reasonId as keyof typeof OOO_REASON_ID_TO_REASON]
|
||||
: OOO_REASON_ID_TO_REASON[1],
|
||||
};
|
||||
}
|
||||
|
||||
isStartBeforeEnd(start?: Date, end?: Date) {
|
||||
if ((end && !start) || (start && !end)) {
|
||||
throw new BadRequestException("Please specify both ooo start and end time.");
|
||||
}
|
||||
|
||||
if (start && end) {
|
||||
if (start.getTime() > end.getTime()) {
|
||||
throw new BadRequestException("Start date must be before end date.");
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async checkUserEligibleForRedirect(userId: number, toUserId?: number) {
|
||||
if (toUserId) {
|
||||
const user = await this.usersRepository.findUserOOORedirectEligible(userId, toUserId);
|
||||
if (!user) {
|
||||
throw new BadRequestException("Cannot redirect to this user.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async checkExistingOooRedirect(userId: number, start?: Date, end?: Date, toUserId?: number) {
|
||||
if (start && end) {
|
||||
const existingOooRedirect = await this.oooRepository.findExistingOooRedirect(
|
||||
userId,
|
||||
start,
|
||||
end,
|
||||
toUserId
|
||||
);
|
||||
|
||||
if (existingOooRedirect) {
|
||||
throw new BadRequestException("Booking redirect infinite not allowed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async checkDuplicateOOOEntry(userId: number, start?: Date, end?: Date) {
|
||||
if (start && end) {
|
||||
const duplicateEntry = await this.oooRepository.getOooByUserIdAndTime(userId, start, end);
|
||||
|
||||
if (duplicateEntry) {
|
||||
throw new ConflictException("Ooo entry already exists.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkRedirectToSelf(userId: number, toUserId?: number) {
|
||||
if (toUserId && toUserId === userId) {
|
||||
throw new BadRequestException("Cannot redirect to self.");
|
||||
}
|
||||
}
|
||||
|
||||
async checkIsValidOOO(userId: number, ooo: CreateOutOfOfficeEntryDto | UpdateOutOfOfficeEntryDto) {
|
||||
this.isStartBeforeEnd(ooo.start, ooo.end);
|
||||
await this.checkExistingOooRedirect(userId, ooo.start, ooo.end, ooo.toUserId);
|
||||
await this.checkDuplicateOOOEntry(userId, ooo.start, ooo.end);
|
||||
await this.checkRedirectToSelf(userId, ooo.toUserId);
|
||||
await this.checkUserEligibleForRedirect(userId, ooo.toUserId);
|
||||
}
|
||||
|
||||
async createUserOOO(userId: number, body: CreateOutOfOfficeEntryDto) {
|
||||
await this.checkIsValidOOO(userId, body);
|
||||
const { reason, ...rest } = body;
|
||||
const ooo = await this.oooRepository.createUserOOO({
|
||||
...rest,
|
||||
userId,
|
||||
reasonId: OOO_REASON_TO_REASON_ID[reason ?? OutOfOfficeReason["UNSPECIFIED"]],
|
||||
});
|
||||
return this.formatOooReason(ooo);
|
||||
}
|
||||
|
||||
async updateUserOOO(userId: number, oooId: number, body: UpdateOutOfOfficeEntryDto) {
|
||||
await this.checkIsValidOOO(userId, body);
|
||||
const { reason, ...rest } = body;
|
||||
const data = reason
|
||||
? { ...rest, reasonId: OOO_REASON_TO_REASON_ID[reason ?? OutOfOfficeReason["UNSPECIFIED"]] }
|
||||
: rest;
|
||||
const ooo = await this.oooRepository.updateUserOOO(oooId, data);
|
||||
return this.formatOooReason(ooo);
|
||||
}
|
||||
|
||||
async deleteUserOOO(oooId: number) {
|
||||
const ooo = await this.oooRepository.deleteUserOOO(oooId);
|
||||
return this.formatOooReason(ooo);
|
||||
}
|
||||
|
||||
async getUserOOOPaginated(
|
||||
userId: number,
|
||||
skip: number,
|
||||
take: number,
|
||||
sort?: { sortStart?: "asc" | "desc"; sortEnd?: "asc" | "desc" }
|
||||
) {
|
||||
const ooos = await this.oooRepository.getUserOOOPaginated(userId, skip, take, sort);
|
||||
return ooos.map((ooo) => this.formatOooReason(ooo));
|
||||
}
|
||||
}
|
||||
+108
-34
@@ -577,6 +577,98 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should be able to configure phone-only event type", async () => {
|
||||
const body: UpdateTeamEventTypeInput_2024_06_14 = {
|
||||
bookingFields: [
|
||||
{
|
||||
type: "email",
|
||||
required: false,
|
||||
label: "Email",
|
||||
},
|
||||
{
|
||||
type: "phone",
|
||||
slug: "attendeePhoneNumber",
|
||||
required: true,
|
||||
label: "Phone number",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${collectiveEventType.id}`)
|
||||
.send(body)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<TeamEventTypeOutput_2024_06_14> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const data = responseBody.data;
|
||||
expect(data.bookingFields).toEqual([
|
||||
{
|
||||
isDefault: true,
|
||||
type: "name",
|
||||
slug: "name",
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "email",
|
||||
slug: "email",
|
||||
required: false,
|
||||
label: "Email",
|
||||
disableOnPrefill: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "radioInput",
|
||||
slug: "location",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "phone",
|
||||
slug: "attendeePhoneNumber",
|
||||
required: true,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "text",
|
||||
slug: "title",
|
||||
required: true,
|
||||
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,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it("should assign all members to managed event-type", async () => {
|
||||
const body: UpdateTeamEventTypeInput_2024_06_14 = {
|
||||
assignAllTeamMembers: true,
|
||||
@@ -677,27 +769,9 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
const fetchedEventType = responseBody.data;
|
||||
|
||||
expect(fetchedEventType.bookingFields).toEqual([
|
||||
{
|
||||
isDefault: true,
|
||||
type: "name",
|
||||
slug: "name",
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "email",
|
||||
slug: "email",
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "phone",
|
||||
slug: "attendeePhoneNumber",
|
||||
required: false,
|
||||
hidden: true,
|
||||
},
|
||||
{ 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",
|
||||
@@ -708,33 +782,33 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "text",
|
||||
slug: "title",
|
||||
required: true,
|
||||
slug: "title",
|
||||
type: "text",
|
||||
disableOnPrefill: false,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "textarea",
|
||||
slug: "notes",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: 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,
|
||||
},
|
||||
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
|
||||
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
import { IsUserInOrg } from "@/modules/auth/guards/users/is-user-in-org.guard";
|
||||
import { IsUserOOO } from "@/modules/ooo/guards/is-user-ooo";
|
||||
import {
|
||||
CreateOutOfOfficeEntryDto,
|
||||
UpdateOutOfOfficeEntryDto,
|
||||
GetOutOfOfficeEntryFiltersDTO,
|
||||
GetOrgUsersOutOfOfficeEntryFiltersDTO,
|
||||
} from "@/modules/ooo/inputs/ooo.input";
|
||||
import {
|
||||
UserOooOutputDto,
|
||||
UserOooOutputResponseDto,
|
||||
UserOoosOutputResponseDto,
|
||||
} from "@/modules/ooo/outputs/ooo.output";
|
||||
import { UserOOOService } from "@/modules/ooo/services/ooo.service";
|
||||
import { OrgUsersOOOService } from "@/modules/organizations/controllers/users/ooo/services/organization-users-ooo.service";
|
||||
import {
|
||||
Controller,
|
||||
UseGuards,
|
||||
Get,
|
||||
Post,
|
||||
Patch,
|
||||
Delete,
|
||||
Param,
|
||||
ParseIntPipe,
|
||||
Body,
|
||||
UseInterceptors,
|
||||
Query,
|
||||
} from "@nestjs/common";
|
||||
import { ClassSerializerInterceptor } from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { plainToInstance } from "class-transformer";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/organizations/:orgId",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseInterceptors(ClassSerializerInterceptor)
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||
@UseGuards(IsOrgGuard)
|
||||
@DocsTags("Orgs / Users / OOO")
|
||||
export class OrganizationsUsersOOOController {
|
||||
constructor(
|
||||
private readonly userOOOService: UserOOOService,
|
||||
private readonly orgUsersOOOService: OrgUsersOOOService
|
||||
) {}
|
||||
|
||||
@Get("/users/:userId/ooo")
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(IsUserInOrg)
|
||||
@ApiOperation({ summary: "Get all ooo entries of a user" })
|
||||
async getOrganizationUserOOO(
|
||||
@Param("userId", ParseIntPipe) userId: number,
|
||||
@Query() query: GetOutOfOfficeEntryFiltersDTO
|
||||
): Promise<UserOoosOutputResponseDto> {
|
||||
const { skip, take, ...rest } = query ?? { skip: 0, take: 250 };
|
||||
const ooos = await this.userOOOService.getUserOOOPaginated(userId, skip ?? 0, take ?? 250, rest);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: ooos.map((ooo) => plainToInstance(UserOooOutputDto, ooo, { strategy: "excludeAll" })),
|
||||
};
|
||||
}
|
||||
|
||||
@Post("/users/:userId/ooo")
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(IsUserInOrg)
|
||||
@ApiOperation({ summary: "Create an ooo entry for user" })
|
||||
async createOrganizationUserOOO(
|
||||
@Param("userId", ParseIntPipe) userId: number,
|
||||
@Body() input: CreateOutOfOfficeEntryDto
|
||||
): Promise<UserOooOutputResponseDto> {
|
||||
const ooo = await this.userOOOService.createUserOOO(userId, input);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToInstance(UserOooOutputDto, ooo, { strategy: "excludeAll" }),
|
||||
};
|
||||
}
|
||||
|
||||
@Patch("/users/:userId/ooo/:oooId")
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(IsUserInOrg, IsUserOOO)
|
||||
@ApiOperation({ summary: "Update ooo entry of a user" })
|
||||
async updateOrganizationUserOOO(
|
||||
@Param("userId", ParseIntPipe) userId: number,
|
||||
@Param("oooId", ParseIntPipe) oooId: number,
|
||||
|
||||
@Body() input: UpdateOutOfOfficeEntryDto
|
||||
): Promise<UserOooOutputResponseDto> {
|
||||
const ooo = await this.userOOOService.updateUserOOO(userId, oooId, input);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToInstance(UserOooOutputDto, ooo, { strategy: "excludeAll" }),
|
||||
};
|
||||
}
|
||||
|
||||
@Delete("/users/:userId/ooo/:oooId")
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(IsUserInOrg, IsUserOOO)
|
||||
@ApiOperation({ summary: "Delete ooo entry of a user" })
|
||||
async deleteOrganizationUserOOO(
|
||||
@Param("oooId", ParseIntPipe) oooId: number
|
||||
): Promise<UserOooOutputResponseDto> {
|
||||
const ooo = await this.userOOOService.deleteUserOOO(oooId);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToInstance(UserOooOutputDto, ooo, { strategy: "excludeAll" }),
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/ooo")
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@ApiOperation({ summary: "Get all OOO entries of org users" })
|
||||
async getOrganizationUsersOOO(
|
||||
@Param("orgId", ParseIntPipe) orgId: number,
|
||||
@Query() query: GetOrgUsersOutOfOfficeEntryFiltersDTO
|
||||
): Promise<UserOoosOutputResponseDto> {
|
||||
const { skip, take, email, ...rest } = query ?? { skip: 0, take: 250 };
|
||||
const ooos = await this.orgUsersOOOService.getOrgUsersOOOPaginated(orgId, skip ?? 0, take ?? 250, rest, {
|
||||
email,
|
||||
});
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: ooos.map((ooo) => plainToInstance(UserOooOutputDto, ooo, { strategy: "excludeAll" })),
|
||||
};
|
||||
}
|
||||
}
|
||||
+446
@@ -0,0 +1,446 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { UserOooOutputDto } from "@/modules/ooo/outputs/ooo.output";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.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 { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { Team } from "@calcom/prisma/client";
|
||||
|
||||
describe("Organizations User OOO Endpoints", () => {
|
||||
describe("User Authentication - User is Org Admin", () => {
|
||||
let app: INestApplication;
|
||||
let oooCreatedViaApiId: number;
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||
let teamsRepositoryFixture: TeamRepositoryFixture;
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
let profileRepositoryFixture: ProfileRepositoryFixture;
|
||||
|
||||
let org: Team;
|
||||
let team: Team;
|
||||
let falseTestOrg: Team;
|
||||
let falseTestTeam: Team;
|
||||
|
||||
const userEmail = "org-admin-ooo-controller-e222e@api.com";
|
||||
let userAdmin: User;
|
||||
|
||||
const teammate1Email = "teammate111ooo@team.com";
|
||||
const teammate2Email = "teammate221ooo@team.com";
|
||||
const falseTestUserEmail = "false-user-ooo@false-team.com";
|
||||
let teammate1: User;
|
||||
let teammate2: User;
|
||||
let falseTestUser: User;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
||||
})
|
||||
).compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
|
||||
userAdmin = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
username: userEmail,
|
||||
role: "ADMIN",
|
||||
});
|
||||
|
||||
teammate1 = await userRepositoryFixture.create({
|
||||
email: teammate1Email,
|
||||
username: teammate1Email,
|
||||
});
|
||||
|
||||
teammate2 = await userRepositoryFixture.create({
|
||||
email: teammate2Email,
|
||||
username: teammate2Email,
|
||||
});
|
||||
|
||||
falseTestUser = await userRepositoryFixture.create({
|
||||
email: falseTestUserEmail,
|
||||
username: falseTestUserEmail,
|
||||
});
|
||||
|
||||
org = await organizationsRepositoryFixture.create({
|
||||
name: "Test Organization ooo",
|
||||
isOrganization: true,
|
||||
});
|
||||
|
||||
falseTestOrg = await organizationsRepositoryFixture.create({
|
||||
name: "False test org ooo",
|
||||
isOrganization: true,
|
||||
});
|
||||
|
||||
team = await teamsRepositoryFixture.create({
|
||||
name: "Test org team ooo",
|
||||
isOrganization: false,
|
||||
parent: { connect: { id: org.id } },
|
||||
});
|
||||
|
||||
falseTestTeam = await teamsRepositoryFixture.create({
|
||||
name: "Outside org team ooo",
|
||||
isOrganization: false,
|
||||
parent: { connect: { id: falseTestOrg.id } },
|
||||
});
|
||||
|
||||
await profileRepositoryFixture.create({
|
||||
uid: `usr-${userAdmin.id}`,
|
||||
username: userEmail,
|
||||
organization: {
|
||||
connect: {
|
||||
id: org.id,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: userAdmin.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await profileRepositoryFixture.create({
|
||||
uid: `usr-${teammate1.id}`,
|
||||
username: teammate1Email,
|
||||
organization: {
|
||||
connect: {
|
||||
id: org.id,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: teammate1.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "ADMIN",
|
||||
user: { connect: { id: userAdmin.id } },
|
||||
team: { connect: { id: org.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
user: { connect: { id: teammate1.id } },
|
||||
team: { connect: { id: team.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
user: { connect: { id: teammate2.id } },
|
||||
team: { connect: { id: team.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
user: { connect: { id: falseTestUser.id } },
|
||||
team: { connect: { id: falseTestTeam.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it("should be defined", () => {
|
||||
expect(userRepositoryFixture).toBeDefined();
|
||||
expect(organizationsRepositoryFixture).toBeDefined();
|
||||
expect(userAdmin).toBeDefined();
|
||||
expect(org).toBeDefined();
|
||||
});
|
||||
|
||||
it("should create a ooo entry without redirect", async () => {
|
||||
const body = {
|
||||
start: "2025-05-01T01:00:00.000Z",
|
||||
end: "2025-05-10T13:59:59.999Z",
|
||||
notes: "ooo numero uno",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`)
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then((response) => {
|
||||
const responseBody = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
|
||||
const data = responseBody.data as UserOooOutputDto;
|
||||
expect(data.reason).toEqual("unspecified");
|
||||
expect(data.userId).toEqual(teammate1.id);
|
||||
expect(data.start).toEqual("2025-05-01T00:00:00.000Z");
|
||||
expect(data.end).toEqual("2025-05-10T23:59:59.999Z");
|
||||
oooCreatedViaApiId = data.id;
|
||||
});
|
||||
});
|
||||
|
||||
it("should create a ooo entry with redirect", async () => {
|
||||
const body = {
|
||||
start: "2025-08-01T01:00:00.000Z",
|
||||
end: "2025-10-10T13:59:59.999Z",
|
||||
notes: "ooo numero dos with redirect",
|
||||
toUserId: teammate2.id,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`)
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then((response) => {
|
||||
const responseBody = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const data = responseBody.data as UserOooOutputDto;
|
||||
expect(data.reason).toEqual("unspecified");
|
||||
expect(data.userId).toEqual(teammate1.id);
|
||||
expect(data.start).toEqual("2025-08-01T00:00:00.000Z");
|
||||
expect(data.end).toEqual("2025-10-10T23:59:59.999Z");
|
||||
expect(data.toUserId).toEqual(teammate2.id);
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail to create a ooo entry with start after end", async () => {
|
||||
const body = {
|
||||
start: "2025-07-01T00:00:00.000Z",
|
||||
end: "2025-05-10T23:59:59.999Z",
|
||||
notes: "ooo numero uno duplicate",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`)
|
||||
.send(body)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should fail to create a duplicate ooo entry", async () => {
|
||||
const body = {
|
||||
start: "2025-05-01T00:00:00.000Z",
|
||||
end: "2025-05-10T23:59:59.999Z",
|
||||
notes: "ooo numero uno duplicate",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`)
|
||||
.send(body)
|
||||
.expect(409);
|
||||
});
|
||||
|
||||
it("should fail to create an ooo entry that redirects to self", async () => {
|
||||
const body = {
|
||||
start: "2025-05-02T00:00:00.000Z",
|
||||
end: "2025-05-03T23:59:59.999Z",
|
||||
notes: "ooo infinite redirect",
|
||||
toUserId: teammate1.id,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`)
|
||||
.send(body)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should fail to create an ooo entry that redirects to member outside of org", async () => {
|
||||
const body = {
|
||||
start: "2025-05-02T00:00:00.000Z",
|
||||
end: "2025-05-03T23:59:59.999Z",
|
||||
notes: "ooo invalid redirect",
|
||||
toUserId: falseTestUser.id,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`)
|
||||
.send(body)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should update a ooo entry without redirect", async () => {
|
||||
const body = {
|
||||
start: "2025-06-01T01:00:00.000Z",
|
||||
end: "2025-06-10T13:59:59.999Z",
|
||||
notes: "ooo numero uno",
|
||||
reason: "vacation",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo/${oooCreatedViaApiId}`)
|
||||
.send(body)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
|
||||
const data = responseBody.data as UserOooOutputDto;
|
||||
expect(data.reason).toEqual("vacation");
|
||||
expect(data.userId).toEqual(teammate1.id);
|
||||
expect(data.start).toEqual("2025-06-01T00:00:00.000Z");
|
||||
expect(data.end).toEqual("2025-06-10T23:59:59.999Z");
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail to update a ooo entry with redirect outside of org", async () => {
|
||||
const body = {
|
||||
toUserId: falseTestUser.id,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo/${oooCreatedViaApiId}`)
|
||||
.send(body)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should fail to update a ooo entry with start after end ", async () => {
|
||||
const body = {
|
||||
start: "2025-07-01T00:00:00.000Z",
|
||||
end: "2025-05-10T23:59:59.999Z",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo/${oooCreatedViaApiId}`)
|
||||
.send(body)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should fail to update a ooo entry with redirect to self ", async () => {
|
||||
const body = {
|
||||
toUserId: teammate1.id,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo/${oooCreatedViaApiId}`)
|
||||
.send(body)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should fail to update a ooo entry with duplicate time", async () => {
|
||||
const body = {
|
||||
start: "2025-06-01T01:00:00.000Z",
|
||||
end: "2025-06-10T13:59:59.999Z",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo/${oooCreatedViaApiId}`)
|
||||
.send(body)
|
||||
.expect(409);
|
||||
});
|
||||
|
||||
it("should update a ooo entry without redirect", async () => {
|
||||
const body = {
|
||||
toUserId: teammate2.id,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo/${oooCreatedViaApiId}`)
|
||||
.send(body)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
|
||||
const data = responseBody.data as UserOooOutputDto;
|
||||
expect(data.reason).toEqual("vacation");
|
||||
expect(data.toUserId).toEqual(teammate2.id);
|
||||
expect(data.userId).toEqual(teammate1.id);
|
||||
expect(data.start).toEqual("2025-06-01T00:00:00.000Z");
|
||||
expect(data.end).toEqual("2025-06-10T23:59:59.999Z");
|
||||
});
|
||||
});
|
||||
|
||||
it("should get 2 ooo entries", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${org.id}/ooo?sortEnd=desc&email=${teammate1Email}`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
|
||||
const data = responseBody.data as UserOooOutputDto[];
|
||||
expect(data.length).toEqual(2);
|
||||
const oooUno = data.find((ooo) => ooo.id === oooCreatedViaApiId);
|
||||
expect(oooUno).toBeDefined();
|
||||
if (oooUno) {
|
||||
expect(oooUno.reason).toEqual("vacation");
|
||||
expect(oooUno.toUserId).toEqual(teammate2.id);
|
||||
expect(oooUno.userId).toEqual(teammate1.id);
|
||||
expect(oooUno.start).toEqual("2025-06-01T00:00:00.000Z");
|
||||
expect(oooUno.end).toEqual("2025-06-10T23:59:59.999Z");
|
||||
}
|
||||
// test sort
|
||||
expect(data[1].id).toEqual(oooCreatedViaApiId);
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
it("should get 2 ooo entries", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
|
||||
const data = responseBody.data as UserOooOutputDto[];
|
||||
expect(data.length).toEqual(2);
|
||||
const oooUno = data.find((ooo) => ooo.id === oooCreatedViaApiId);
|
||||
expect(oooUno).toBeDefined();
|
||||
if (oooUno) {
|
||||
expect(oooUno.reason).toEqual("vacation");
|
||||
expect(oooUno.toUserId).toEqual(teammate2.id);
|
||||
expect(oooUno.userId).toEqual(teammate1.id);
|
||||
expect(oooUno.start).toEqual("2025-06-01T00:00:00.000Z");
|
||||
expect(oooUno.end).toEqual("2025-06-10T23:59:59.999Z");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should delete ooo entry", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.delete(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo/${oooCreatedViaApiId}`)
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it("user should have 1 ooo entries", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const data = responseBody.data as UserOooOutputDto[];
|
||||
expect(data.length).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(userAdmin.email);
|
||||
await userRepositoryFixture.deleteByEmail(teammate1.email);
|
||||
await userRepositoryFixture.deleteByEmail(teammate2.email);
|
||||
await userRepositoryFixture.deleteByEmail(falseTestUser.email);
|
||||
await teamsRepositoryFixture.delete(team.id);
|
||||
await teamsRepositoryFixture.delete(falseTestTeam.id);
|
||||
await organizationsRepositoryFixture.delete(org.id);
|
||||
await organizationsRepositoryFixture.delete(falseTestOrg.id);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class OrgUsersOOORepository {
|
||||
constructor(private readonly dbRead: PrismaReadService) {}
|
||||
async getOrgUsersOOOPaginated(
|
||||
orgId: number,
|
||||
skip: number,
|
||||
take: number,
|
||||
sort?: { sortStart?: "asc" | "desc"; sortEnd?: "asc" | "desc" },
|
||||
filters?: { email?: string }
|
||||
) {
|
||||
console.log({ sort, filters });
|
||||
return this.dbRead.prisma.outOfOfficeEntry.findMany({
|
||||
where: {
|
||||
user: {
|
||||
...(filters?.email && { email: filters.email }),
|
||||
profiles: {
|
||||
some: {
|
||||
organizationId: orgId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
skip,
|
||||
take,
|
||||
include: { reason: true },
|
||||
...(sort?.sortStart && { orderBy: { start: sort.sortStart } }),
|
||||
...(sort?.sortEnd && { orderBy: { end: sort.sortEnd } }),
|
||||
});
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { UserOOORepository } from "@/modules/ooo/repositories/ooo.repository";
|
||||
import { UserOOOService } from "@/modules/ooo/services/ooo.service";
|
||||
import { OrgUsersOOORepository } from "@/modules/organizations/controllers/users/ooo/repositories/organizations-users-ooo.repository";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class OrgUsersOOOService {
|
||||
constructor(
|
||||
private readonly oooRepository: UserOOORepository,
|
||||
private readonly oooUserService: UserOOOService,
|
||||
private readonly usersRepository: UsersRepository,
|
||||
private readonly orgUsersOOORepository: OrgUsersOOORepository
|
||||
) {}
|
||||
|
||||
async getOrgUsersOOOPaginated(
|
||||
orgId: number,
|
||||
skip: number,
|
||||
take: number,
|
||||
sort?: { sortStart?: "asc" | "desc"; sortEnd?: "asc" | "desc" },
|
||||
filters?: { email?: string }
|
||||
) {
|
||||
const ooos = await this.orgUsersOOORepository.getOrgUsersOOOPaginated(orgId, skip, take, sort, filters);
|
||||
return ooos.map((ooo) => this.oooUserService.formatOooReason(ooo));
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/
|
||||
import { EmailModule } from "@/modules/email/email.module";
|
||||
import { EmailService } from "@/modules/email/email.service";
|
||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||
import { UserOOORepository } from "@/modules/ooo/repositories/ooo.repository";
|
||||
import { UserOOOService } from "@/modules/ooo/services/ooo.service";
|
||||
import { OrganizationsOptionsAttributesController } from "@/modules/organizations/controllers/attributes/organizations-attributes-options.controller";
|
||||
import { OrganizationsAttributesController } from "@/modules/organizations/controllers/attributes/organizations-attributes.controller";
|
||||
import { OrganizationsEventTypesController } from "@/modules/organizations/controllers/event-types/organizations-event-types.controller";
|
||||
@@ -12,6 +14,9 @@ import { OrganizationsSchedulesController } from "@/modules/organizations/contro
|
||||
import { OrganizationsTeamsMembershipsController } from "@/modules/organizations/controllers/teams/memberships/organizations-teams-memberships.controller";
|
||||
import { OrganizationsTeamsController } from "@/modules/organizations/controllers/teams/organizations-teams.controller";
|
||||
import { OrganizationsTeamsSchedulesController } from "@/modules/organizations/controllers/teams/schedules/organizations-teams-schedules.controller";
|
||||
import { OrganizationsUsersOOOController } from "@/modules/organizations/controllers/users/ooo/organizations-users-ooo-controller";
|
||||
import { OrgUsersOOORepository } from "@/modules/organizations/controllers/users/ooo/repositories/organizations-users-ooo.repository";
|
||||
import { OrgUsersOOOService } from "@/modules/organizations/controllers/users/ooo/services/organization-users-ooo.service";
|
||||
import { OrganizationsUsersController } from "@/modules/organizations/controllers/users/organizations-users.controller";
|
||||
import { OrganizationsWebhooksController } from "@/modules/organizations/controllers/webhooks/organizations-webhooks.controller";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
@@ -86,6 +91,10 @@ import { Module } from "@nestjs/common";
|
||||
WebhooksRepository,
|
||||
WebhooksService,
|
||||
OutputTeamEventTypesResponsePipe,
|
||||
UserOOOService,
|
||||
UserOOORepository,
|
||||
OrgUsersOOOService,
|
||||
OrgUsersOOORepository,
|
||||
],
|
||||
exports: [
|
||||
OrganizationsService,
|
||||
@@ -118,6 +127,7 @@ import { Module } from "@nestjs/common";
|
||||
OrganizationsOptionsAttributesController,
|
||||
OrganizationsWebhooksController,
|
||||
OrganizationsTeamsSchedulesController,
|
||||
OrganizationsUsersOOOController,
|
||||
],
|
||||
})
|
||||
export class OrganizationsModule {}
|
||||
|
||||
@@ -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],
|
||||
})
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Transform } from "class-transformer";
|
||||
import { IsOptional, Validate } from "class-validator";
|
||||
|
||||
import { SkipTakePagination } from "@calcom/platform-types";
|
||||
import { IsNumber, IsOptional, Max, Min, Validate } from "class-validator";
|
||||
|
||||
import { IsEmailStringOrArray } from "../validators/isEmailStringOrArray";
|
||||
|
||||
export class GetUsersInput extends SkipTakePagination {
|
||||
export class GetUsersInput {
|
||||
@ApiProperty({ required: false, description: "The number of items to return", example: 10 })
|
||||
@Transform(({ value }: { value: string }) => value && parseInt(value))
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Max(1000)
|
||||
@IsOptional()
|
||||
take?: number;
|
||||
|
||||
@ApiProperty({ required: false, description: "The number of items to skip", example: 0 })
|
||||
@Transform(({ value }: { value: string }) => value && parseInt(value))
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@IsOptional()
|
||||
skip?: number;
|
||||
|
||||
@IsOptional()
|
||||
@Validate(IsEmailStringOrArray)
|
||||
@Transform(({ value }: { value: string | string[] }) => {
|
||||
|
||||
@@ -265,4 +265,27 @@ export class UsersRepository {
|
||||
where: { id: userId },
|
||||
});
|
||||
}
|
||||
|
||||
async findUserOOORedirectEligible(userId: number, toTeamUserId: number) {
|
||||
return await this.dbRead.prisma.user.findUnique({
|
||||
where: {
|
||||
id: toTeamUserId,
|
||||
teams: {
|
||||
some: {
|
||||
team: {
|
||||
members: {
|
||||
some: {
|
||||
userId: userId,
|
||||
accepted: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2516,6 +2516,8 @@
|
||||
"description": "The number of items to return",
|
||||
"example": 10,
|
||||
"schema": {
|
||||
"minimum": 1,
|
||||
"maximum": 1000,
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
@@ -2526,6 +2528,7 @@
|
||||
"description": "The number of items to skip",
|
||||
"example": 0,
|
||||
"schema": {
|
||||
"minimum": 0,
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
@@ -2684,6 +2687,256 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/organizations/{orgId}/users/{userId}/ooo": {
|
||||
"get": {
|
||||
"operationId": "OrganizationsUsersOOOController_getOrganizationUserOOO",
|
||||
"summary": "Get all ooo entries of a user",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "take",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "The number of items to return",
|
||||
"example": 10,
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "skip",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "The number of items to skip",
|
||||
"example": 0,
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "sortStart",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Sort results by their start time in ascending or descending order.",
|
||||
"example": "?sortStart=asc OR ?sortStart=desc",
|
||||
"schema": {
|
||||
"enum": [
|
||||
"asc",
|
||||
"desc"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "sortEnd",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Sort results by their end time in ascending or descending order.",
|
||||
"example": "?sortEnd=asc OR ?sortEnd=desc",
|
||||
"schema": {
|
||||
"enum": [
|
||||
"asc",
|
||||
"desc"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Orgs / Users / OOO"
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"operationId": "OrganizationsUsersOOOController_createOrganizationUserOOO",
|
||||
"summary": "Create an ooo entry for user",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateOutOfOfficeEntryDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Orgs / Users / OOO"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/organizations/{orgId}/users/{userId}/ooo/{oooId}": {
|
||||
"patch": {
|
||||
"operationId": "OrganizationsUsersOOOController_updateOrganizationUserOOO",
|
||||
"summary": "Update ooo entry of a user",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "oooId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateOutOfOfficeEntryDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Orgs / Users / OOO"
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"operationId": "OrganizationsUsersOOOController_deleteOrganizationUserOOO",
|
||||
"summary": "Delete ooo entry of a user",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "oooId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Orgs / Users / OOO"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/organizations/{orgId}/ooo": {
|
||||
"get": {
|
||||
"operationId": "OrganizationsUsersOOOController_getOrganizationUsersOOO",
|
||||
"summary": "Get all OOO entries of org users",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "orgId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "take",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "The number of items to return",
|
||||
"example": 10,
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "skip",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "The number of items to skip",
|
||||
"example": 0,
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "sortStart",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Sort results by their start time in ascending or descending order.",
|
||||
"example": "?sortStart=asc OR ?sortStart=desc",
|
||||
"schema": {
|
||||
"enum": [
|
||||
"asc",
|
||||
"desc"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "sortEnd",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Sort results by their end time in ascending or descending order.",
|
||||
"example": "?sortEnd=asc OR ?sortEnd=desc",
|
||||
"schema": {
|
||||
"enum": [
|
||||
"asc",
|
||||
"desc"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "email",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Filter ooo entries by the user email address. user must be within your organization.",
|
||||
"example": "example@domain.com",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Orgs / Users / OOO"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/organizations/{orgId}/webhooks": {
|
||||
"get": {
|
||||
"operationId": "OrganizationsWebhooksController_getAllOrganizationWebhooks",
|
||||
@@ -5139,7 +5392,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 +5401,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": {
|
||||
@@ -7741,13 +7995,13 @@
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"NameFieldInput_2024_06_14": {
|
||||
"NameDefaultFieldInput_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"example": "name",
|
||||
"description": "only allowed value for type is `name`"
|
||||
"description": "only allowed value for type is `name`. Used for having 1 booking field for both first name and last name."
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
@@ -7766,7 +8020,7 @@
|
||||
"placeholder"
|
||||
]
|
||||
},
|
||||
"EmailFieldInput_2024_06_14": {
|
||||
"EmailDefaultFieldInput_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
@@ -7777,6 +8031,9 @@
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": {
|
||||
"type": "object"
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -7788,9 +8045,130 @@
|
||||
"required": [
|
||||
"type",
|
||||
"label",
|
||||
"required",
|
||||
"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": {
|
||||
@@ -7921,10 +8299,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"
|
||||
@@ -8355,6 +8745,10 @@
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": {
|
||||
"type": "object",
|
||||
"default": true
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -8371,16 +8765,13 @@
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"default": "email"
|
||||
},
|
||||
"required": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"required",
|
||||
"isDefault",
|
||||
"slug",
|
||||
"required"
|
||||
"slug"
|
||||
]
|
||||
},
|
||||
"NameDefaultFieldOutput_2024_06_14": {
|
||||
@@ -8404,7 +8795,7 @@
|
||||
"boolean"
|
||||
],
|
||||
"example": "name",
|
||||
"description": "only allowed value for type is `name`",
|
||||
"description": "only allowed value for type is `name`. Used for having 1 booking field for both first name and last name.",
|
||||
"default": "name"
|
||||
},
|
||||
"label": {
|
||||
@@ -8475,20 +8866,19 @@
|
||||
"RescheduleReasonDefaultFieldOutput_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"isDefault": {
|
||||
"type": "object",
|
||||
"default": true,
|
||||
"description": "This property is always true because it's a default field",
|
||||
"example": true
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"name",
|
||||
"email",
|
||||
"title",
|
||||
"notes",
|
||||
"guests"
|
||||
],
|
||||
"example": "rescheduleReason",
|
||||
"description": "only allowed value for type is `rescheduleReason`",
|
||||
"default": "rescheduleReason"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"default": "textarea"
|
||||
},
|
||||
"required": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -8505,33 +8895,39 @@
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `&rescheduleReason=busy`, the reschedule reason field will be prefilled with this value and disabled."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"isDefault",
|
||||
"slug",
|
||||
"type",
|
||||
"required",
|
||||
"hidden",
|
||||
"disableOnPrefill"
|
||||
]
|
||||
},
|
||||
"TitleDefaultFieldOutput_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
},
|
||||
"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"
|
||||
"default": "textarea"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"slug",
|
||||
"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"
|
||||
@@ -8549,33 +8945,39 @@
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `&title=masterclass`, the title field will be prefilled with this value and disabled."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"isDefault",
|
||||
"slug",
|
||||
"type",
|
||||
"required",
|
||||
"hidden",
|
||||
"disableOnPrefill"
|
||||
]
|
||||
},
|
||||
"NotesDefaultFieldOutput_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
},
|
||||
"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"
|
||||
"default": "text"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"slug",
|
||||
"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"
|
||||
@@ -8593,33 +8995,39 @@
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `¬es=hello`, the notes field will be prefilled with this value and disabled."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"isDefault",
|
||||
"slug",
|
||||
"type",
|
||||
"required",
|
||||
"hidden",
|
||||
"disableOnPrefill"
|
||||
]
|
||||
},
|
||||
"GuestsDefaultFieldOutput_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
},
|
||||
"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"
|
||||
"default": "textarea"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"slug",
|
||||
"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"
|
||||
@@ -8637,15 +9045,22 @@
|
||||
"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
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"default": "multiemail"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"isDefault",
|
||||
"slug",
|
||||
"type",
|
||||
"required",
|
||||
"hidden",
|
||||
"disableOnPrefill"
|
||||
"isDefault",
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"AddressFieldOutput_2024_06_14": {
|
||||
@@ -9776,10 +10191,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"
|
||||
@@ -9943,7 +10370,7 @@
|
||||
},
|
||||
"customName": {
|
||||
"type": "string",
|
||||
"description": "Customizable event name with valid variables: \n {Event type title}, {Organiser}, {Scheduler}, {Location}, {Organiser first name}, \n {Scheduler first name}, {Scheduler last name}, {Event duration}, {LOCATION}, \n {HOST/ATTENDEE}, {HOST}, {ATTENDEE}, {USER}",
|
||||
"description": "Customizable event name with valid variables:\n {Event type title}, {Organiser}, {Scheduler}, {Location}, {Organiser first name},\n {Scheduler first name}, {Scheduler last name}, {Event duration}, {LOCATION},\n {HOST/ATTENDEE}, {HOST}, {ATTENDEE}, {USER}",
|
||||
"example": "{Event type title} between {Organiser} and {Scheduler}"
|
||||
},
|
||||
"destinationCalendar": {
|
||||
@@ -11353,10 +11780,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"
|
||||
@@ -12178,10 +12617,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"
|
||||
@@ -12345,7 +12796,7 @@
|
||||
},
|
||||
"customName": {
|
||||
"type": "string",
|
||||
"description": "Customizable event name with valid variables: \n {Event type title}, {Organiser}, {Scheduler}, {Location}, {Organiser first name}, \n {Scheduler first name}, {Scheduler last name}, {Event duration}, {LOCATION}, \n {HOST/ATTENDEE}, {HOST}, {ATTENDEE}, {USER}",
|
||||
"description": "Customizable event name with valid variables:\n {Event type title}, {Organiser}, {Scheduler}, {Location}, {Organiser first name},\n {Scheduler first name}, {Scheduler last name}, {Event duration}, {LOCATION},\n {HOST/ATTENDEE}, {HOST}, {ATTENDEE}, {USER}",
|
||||
"example": "{Event type title} between {Organiser} and {Scheduler}"
|
||||
},
|
||||
"destinationCalendar": {
|
||||
@@ -13256,6 +13707,88 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateOutOfOfficeEntryDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"start": {
|
||||
"format": "date-time",
|
||||
"type": "string",
|
||||
"description": "The start date and time of the out of office period in ISO 8601 format in UTC timezone.",
|
||||
"example": "2023-05-01T00:00:00.000Z"
|
||||
},
|
||||
"end": {
|
||||
"format": "date-time",
|
||||
"type": "string",
|
||||
"description": "The end date and time of the out of office period in ISO 8601 format in UTC timezone.",
|
||||
"example": "2023-05-10T23:59:59.999Z"
|
||||
},
|
||||
"notes": {
|
||||
"type": "string",
|
||||
"description": "Optional notes for the out of office entry.",
|
||||
"example": "Vacation in Hawaii"
|
||||
},
|
||||
"toUserId": {
|
||||
"type": "number",
|
||||
"description": "The ID of the user covering for the out of office period, if applicable.",
|
||||
"example": 2
|
||||
},
|
||||
"reason": {
|
||||
"type": "string",
|
||||
"description": "the reason for the out of office entry, if applicable",
|
||||
"example": "vacation",
|
||||
"enum": [
|
||||
"unspecified",
|
||||
"vacation",
|
||||
"travel",
|
||||
"sick",
|
||||
"public_holiday"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"start",
|
||||
"end"
|
||||
]
|
||||
},
|
||||
"UpdateOutOfOfficeEntryDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"start": {
|
||||
"format": "date-time",
|
||||
"type": "string",
|
||||
"description": "The start date and time of the out of office period in ISO 8601 format in UTC timezone.",
|
||||
"example": "2023-05-01T00:00:00.000Z"
|
||||
},
|
||||
"end": {
|
||||
"format": "date-time",
|
||||
"type": "string",
|
||||
"description": "The end date and time of the out of office period in ISO 8601 format in UTC timezone.",
|
||||
"example": "2023-05-10T23:59:59.999Z"
|
||||
},
|
||||
"notes": {
|
||||
"type": "string",
|
||||
"description": "Optional notes for the out of office entry.",
|
||||
"example": "Vacation in Hawaii"
|
||||
},
|
||||
"toUserId": {
|
||||
"type": "number",
|
||||
"description": "The ID of the user covering for the out of office period, if applicable.",
|
||||
"example": 2
|
||||
},
|
||||
"reason": {
|
||||
"type": "string",
|
||||
"description": "the reason for the out of office entry, if applicable",
|
||||
"example": "vacation",
|
||||
"enum": [
|
||||
"unspecified",
|
||||
"vacation",
|
||||
"travel",
|
||||
"sick",
|
||||
"public_holiday"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"StripConnectOutputDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -14278,6 +14811,11 @@
|
||||
"description": "The time zone of the attendee.",
|
||||
"example": "America/New_York"
|
||||
},
|
||||
"phoneNumber": {
|
||||
"type": "string",
|
||||
"description": "The phone number of the attendee in international format.",
|
||||
"example": "+919876543210"
|
||||
},
|
||||
"language": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -14331,7 +14869,6 @@
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"email",
|
||||
"timeZone"
|
||||
]
|
||||
},
|
||||
@@ -14939,6 +15476,10 @@
|
||||
"type": "boolean",
|
||||
"example": false
|
||||
},
|
||||
"phoneNumber": {
|
||||
"type": "string",
|
||||
"example": "+1234567890"
|
||||
},
|
||||
"seatUid": {
|
||||
"type": "string",
|
||||
"example": "3be561a9-31f1-4b8e-aefc-9d9a085f0dd1"
|
||||
|
||||
@@ -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,17 +1,10 @@
|
||||
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";
|
||||
|
||||
const ROUTES: [URLPattern, boolean][] = [
|
||||
["/event-types", process.env.APP_ROUTER_EVENT_TYPES_ENABLED === "1"] as const,
|
||||
["/apps/installed/:category", process.env.APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED === "1"] as const,
|
||||
["/apps/:slug", process.env.APP_ROUTER_APPS_SLUG_ENABLED === "1"] as const,
|
||||
["/apps/:slug/setup", process.env.APP_ROUTER_APPS_SLUG_SETUP_ENABLED === "1"] as const,
|
||||
["/apps/categories", process.env.APP_ROUTER_APPS_CATEGORIES_ENABLED === "1"] as const,
|
||||
["/apps/categories/:category", process.env.APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED === "1"] as const,
|
||||
["/auth/forgot-password/:path*", process.env.APP_ROUTER_AUTH_FORGOT_PASSWORD_ENABLED === "1"] as const,
|
||||
["/auth/login", process.env.APP_ROUTER_AUTH_LOGIN_ENABLED === "1"] as const,
|
||||
["/auth/logout", process.env.APP_ROUTER_AUTH_LOGOUT_ENABLED === "1"] as const,
|
||||
@@ -28,8 +21,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) => {
|
||||
@@ -46,11 +37,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();
|
||||
|
||||
@@ -69,7 +58,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,
|
||||
});
|
||||
@@ -13,6 +13,9 @@ 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(),
|
||||
@@ -86,7 +89,7 @@ describe("withEmbedSsrAppDir", () => {
|
||||
embed: "namespace1",
|
||||
},
|
||||
})
|
||||
).catch(() => {});
|
||||
).catch(noop);
|
||||
|
||||
expect(redirect).toHaveBeenCalledWith("/reschedule/embed?layout=week_view&embed=namespace1");
|
||||
});
|
||||
@@ -105,7 +108,7 @@ describe("withEmbedSsrAppDir", () => {
|
||||
embed: "namespace1",
|
||||
},
|
||||
})
|
||||
).catch(() => {});
|
||||
).catch(noop);
|
||||
|
||||
expect(redirect).toHaveBeenCalledWith(
|
||||
"/reschedule/embed?redirectParam=1&layout=week_view&embed=namespace1"
|
||||
@@ -126,7 +129,7 @@ describe("withEmbedSsrAppDir", () => {
|
||||
embed: "",
|
||||
},
|
||||
})
|
||||
).catch(() => {});
|
||||
).catch(noop);
|
||||
|
||||
expect(redirect).toHaveBeenCalledWith("/reschedule/embed?redirectParam=1&layout=week_view&embed=");
|
||||
});
|
||||
@@ -147,7 +150,7 @@ describe("withEmbedSsrAppDir", () => {
|
||||
embed: "namespace1",
|
||||
},
|
||||
})
|
||||
).catch(() => {});
|
||||
).catch(noop);
|
||||
|
||||
expect(redirect).toHaveBeenCalledWith(
|
||||
"https://calcom.cal.local/owner/embed?layout=week_view&embed=namespace1"
|
||||
@@ -168,7 +171,7 @@ describe("withEmbedSsrAppDir", () => {
|
||||
embed: "namespace1",
|
||||
},
|
||||
})
|
||||
).catch(() => {});
|
||||
).catch(noop);
|
||||
|
||||
expect(redirect).toHaveBeenCalledWith(
|
||||
"http://calcom.cal.local/owner/embed?layout=week_view&embed=namespace1"
|
||||
@@ -189,7 +192,7 @@ describe("withEmbedSsrAppDir", () => {
|
||||
embed: "namespace1",
|
||||
},
|
||||
})
|
||||
).catch(() => {});
|
||||
).catch(noop);
|
||||
|
||||
expect(redirect).toHaveBeenCalledWith(
|
||||
"/calcom.cal.local/owner/embed?layout=week_view&embed=namespace1"
|
||||
@@ -239,7 +242,7 @@ describe("withEmbedSsrAppDir", () => {
|
||||
embed: "",
|
||||
},
|
||||
})
|
||||
).catch(() => {});
|
||||
).catch(noop);
|
||||
|
||||
expect(notFound).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
+46
-5
@@ -3,7 +3,8 @@ import i18next from "i18next";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
import { constructGenericImage } from "@calcom/lib/OgImages";
|
||||
import type { AppImageProps, MeetingImageProps } from "@calcom/lib/OgImages";
|
||||
import { constructGenericImage, constructAppImage, 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";
|
||||
@@ -33,7 +34,7 @@ const createI18nInstance = async (locale: string, ns: string) => {
|
||||
return _i18n;
|
||||
};
|
||||
|
||||
const getTranslationWithCache = async (locale: string, ns: string = "common") => {
|
||||
const getTranslationWithCache = async (locale: string, ns = "common") => {
|
||||
const localeWithFallback = locale ?? "en";
|
||||
const i18n = await createI18nInstance(localeWithFallback, ns);
|
||||
return i18n.getFixedT(localeWithFallback, ns);
|
||||
@@ -61,17 +62,18 @@ const _generateMetadataWithoutImage = async (
|
||||
const t = await getTranslationWithCache(locale);
|
||||
|
||||
const title = getTitle(t);
|
||||
const description = getDescription(t);
|
||||
|
||||
const titleSuffix = `| ${APP_NAME}`;
|
||||
const displayedTitle = title.includes(titleSuffix) || hideBranding ? title : `${title} ${titleSuffix}`;
|
||||
const metadataBase = new URL(IS_CALCOM ? "https://cal.com" : WEBAPP_URL);
|
||||
const truncatedDescription = truncateOnWord(getDescription(t), 158);
|
||||
|
||||
return {
|
||||
title: title.length === 0 ? APP_NAME : displayedTitle,
|
||||
description,
|
||||
description: truncatedDescription,
|
||||
alternates: { canonical },
|
||||
openGraph: {
|
||||
description: truncateOnWord(description, 158),
|
||||
description: truncatedDescription,
|
||||
url: canonical,
|
||||
type: "website",
|
||||
siteName: APP_NAME,
|
||||
@@ -103,3 +105,42 @@ export const _generateMetadata = async (
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const generateAppMetadata = async (
|
||||
app: AppImageProps,
|
||||
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 + constructAppImage({ ...app, 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],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -10,17 +10,23 @@ import { userMetadata } from "@calcom/prisma/zod-utils";
|
||||
|
||||
const inputSchema = z.object({
|
||||
customer: z.object({
|
||||
name: z.string().optional(),
|
||||
email: z.string().email(),
|
||||
username: z.string().optional(),
|
||||
timeZone: z.string().optional(),
|
||||
emailVerified: z.boolean().optional(),
|
||||
identityProvider: z.string().optional(),
|
||||
twoFactorEnabled: z.boolean().optional(),
|
||||
lastActiveAt: z.string().optional(),
|
||||
teamName: z.string().optional(),
|
||||
teamSlug: z.string().optional(),
|
||||
isOrganization: z.boolean().optional(),
|
||||
stripeCustomerId: z.string().optional(),
|
||||
}),
|
||||
cardKeys: z.array(z.string()),
|
||||
});
|
||||
|
||||
export async function handler(request: Request) {
|
||||
async function handler(request: Request) {
|
||||
const headersList = headers();
|
||||
const requestBody = await request.json();
|
||||
|
||||
@@ -53,168 +59,10 @@ export async function handler(request: Request) {
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Email",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: customer.email || "Unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Email Verified?",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentBadge: {
|
||||
badgeLabel: customer.emailVerified ? "Yes" : "No",
|
||||
badgeColor: customer.emailVerified ? "GREEN" : "RED",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Username",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: customer.username || "Unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "User ID",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Time Zone",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: customer.timeZone || "Unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Two Factor Enabled?",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentBadge: {
|
||||
badgeLabel: customer.twoFactorEnabled ? "Yes" : "No",
|
||||
badgeColor: customer.twoFactorEnabled ? "GREEN" : "RED",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Identity Provider",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: customer.identityProvider || "Unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
componentText: {
|
||||
textSize: "L",
|
||||
textColor: "MUTED",
|
||||
text: "User does not exist!",
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -223,18 +71,29 @@ export async function handler(request: Request) {
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch team details including userId and team name
|
||||
const teamMemberships = await UserRepository.findTeamsByUserId({ userId: user.id });
|
||||
const firstTeam = teamMemberships.teams[0] ?? null;
|
||||
|
||||
// Parse user metadata
|
||||
const parsedMetadata = userMetadata.parse(user.metadata);
|
||||
|
||||
const cards = await Promise.all(
|
||||
cardExamples.map(async (cardFn) => {
|
||||
return cardFn(
|
||||
user.name || "Unknown",
|
||||
user.email,
|
||||
user.id.toString(),
|
||||
user.username || "Unknown",
|
||||
user.timeZone,
|
||||
user.emailVerified,
|
||||
user.twoFactorEnabled
|
||||
user.twoFactorEnabled,
|
||||
user.identityProvider,
|
||||
user.lastActiveAt,
|
||||
firstTeam?.name || "Unknown",
|
||||
firstTeam?.slug || "Unknown",
|
||||
firstTeam?.isOrganization || false,
|
||||
(user.metadata as { stripeCustomerId?: string })?.stripeCustomerId || "Unknown"
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { PageProps as _PageProps } from "app/_types";
|
||||
import { generateAppMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { notFound } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
import { AppRepository } from "@calcom/lib/server/repository/app";
|
||||
import { isPrismaAvailableCheck } from "@calcom/prisma/is-prisma-available-check";
|
||||
|
||||
import { getStaticProps } from "@lib/apps/[slug]/getStaticProps";
|
||||
|
||||
import AppView from "~/apps/[slug]/slug-view";
|
||||
|
||||
const paramsSchema = z.object({
|
||||
slug: z.string(),
|
||||
});
|
||||
|
||||
export const generateMetadata = async ({ params }: _PageProps) => {
|
||||
const p = paramsSchema.safeParse(params);
|
||||
|
||||
if (!p.success) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const props = await getStaticProps(p.data.slug);
|
||||
|
||||
if (!props) {
|
||||
notFound();
|
||||
}
|
||||
const { name, logo, description } = props.data;
|
||||
|
||||
return await generateAppMetadata(
|
||||
{ slug: logo, name, description },
|
||||
() => name,
|
||||
() => description
|
||||
);
|
||||
};
|
||||
|
||||
export const generateStaticParams = async () => {
|
||||
const isPrismaAvailable = await isPrismaAvailableCheck();
|
||||
|
||||
if (!isPrismaAvailable) {
|
||||
// Database is not available at build time. Make sure we fall back to building these pages on demand
|
||||
return [];
|
||||
}
|
||||
const appStore = await AppRepository.findAppStore();
|
||||
return appStore.map(({ slug }) => ({ slug }));
|
||||
};
|
||||
|
||||
async function Page({ params }: _PageProps) {
|
||||
const p = paramsSchema.safeParse(params);
|
||||
|
||||
if (!p.success) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const props = await getStaticProps(p.data.slug);
|
||||
|
||||
if (!props) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return <AppView {...props} />;
|
||||
}
|
||||
|
||||
export default WithLayout({ getLayout: null, ServerPage: Page });
|
||||
|
||||
export const dynamic = "force-static";
|
||||
+20
-2
@@ -2,16 +2,34 @@ import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps as _PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { notFound } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getServerSideProps } from "@calcom/app-store/_pages/setup/_getServerSideProps";
|
||||
|
||||
import Page, { type PageProps } from "~/apps/[slug]/setup/setup-view";
|
||||
|
||||
const paramsSchema = z.object({
|
||||
slug: z.string(),
|
||||
});
|
||||
|
||||
export const generateMetadata = async ({ params }: _PageProps) => {
|
||||
return await _generateMetadata(
|
||||
() => `${params.slug}`,
|
||||
const p = paramsSchema.safeParse(params);
|
||||
|
||||
if (!p.success) {
|
||||
return notFound();
|
||||
}
|
||||
const metadata = await _generateMetadata(
|
||||
() => `${p.data.slug}`,
|
||||
() => ""
|
||||
);
|
||||
return {
|
||||
...metadata,
|
||||
robots: {
|
||||
follow: false,
|
||||
index: false,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { redirect } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
import { AppCategories } from "@calcom/prisma/enums";
|
||||
|
||||
import { getStaticProps } from "@lib/apps/categories/[category]/getStaticProps";
|
||||
|
||||
import CategoryPage from "~/apps/categories/[category]/category-view";
|
||||
|
||||
const querySchema = z.object({
|
||||
category: z.enum(Object.values(AppCategories) as [AppCategories, ...AppCategories[]]),
|
||||
});
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
(t) => t("app_store"),
|
||||
(t) => t("app_store_description")
|
||||
);
|
||||
};
|
||||
|
||||
export const generateStaticParams = async () => {
|
||||
const paths = Object.keys(AppCategories);
|
||||
return paths.map((category) => ({ category }));
|
||||
};
|
||||
|
||||
async function Page({ params, searchParams }: PageProps) {
|
||||
const parsed = querySchema.safeParse({ ...params, ...searchParams });
|
||||
if (!parsed.success) {
|
||||
redirect("/apps/categories/calendar");
|
||||
}
|
||||
|
||||
const props = await getStaticProps(parsed.data.category);
|
||||
|
||||
return <CategoryPage {...props} />;
|
||||
}
|
||||
|
||||
export default WithLayout({ getLayout: null, ServerPage: Page });
|
||||
-1
@@ -12,7 +12,6 @@ import Page from "~/apps/installation/[[...step]]/step-view";
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
|
||||
const { appMetadata } = await getData(legacyCtx);
|
||||
return await _generateMetadata(
|
||||
(t) => `${t("install")} ${appMetadata?.name ?? ""}`,
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { redirect } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
import { AppCategories } from "@calcom/prisma/enums";
|
||||
|
||||
import InstalledApps from "~/apps/installed/[category]/installed-category-view";
|
||||
|
||||
const querySchema = z.object({
|
||||
category: z.nativeEnum(AppCategories),
|
||||
});
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
(t) => t("installed_apps"),
|
||||
(t) => t("manage_your_connected_apps")
|
||||
);
|
||||
};
|
||||
|
||||
const InstalledAppsWrapper = async ({ params }: PageProps) => {
|
||||
const parsedParams = querySchema.safeParse(params);
|
||||
|
||||
if (!parsedParams.success) {
|
||||
redirect("/apps/installed/calendar");
|
||||
}
|
||||
|
||||
return <InstalledApps category={parsedParams.data.category} />;
|
||||
};
|
||||
|
||||
export default WithLayout({ getLayout: null, ServerPage: InstalledAppsWrapper });
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "app/routing-forms/[...pages]/page";
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "app/routing-forms/page";
|
||||
@@ -23,8 +23,8 @@ const querySchema = z.object({
|
||||
|
||||
const getSchedule = cache((id: number) => ScheduleRepository.findScheduleById({ id }));
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
const parsed = querySchema.safeParse({ ...params, ...searchParams });
|
||||
export const generateMetadata = async ({ params }: PageProps) => {
|
||||
const parsed = querySchema.safeParse(params);
|
||||
if (!parsed.success) {
|
||||
notFound();
|
||||
}
|
||||
@@ -41,8 +41,8 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const Page = async ({ params, searchParams }: PageProps) => {
|
||||
const parsed = querySchema.safeParse({ ...params, ...searchParams });
|
||||
const Page = async ({ params }: PageProps) => {
|
||||
const parsed = querySchema.safeParse(params);
|
||||
if (!parsed.success) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
@@ -1,6 +1,5 @@
|
||||
import { PageProps } from "app/_types";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { redirect } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -21,8 +20,8 @@ export const generateStaticParams = async () => {
|
||||
return validStatuses.map((status) => ({ status }));
|
||||
};
|
||||
|
||||
const Page = async ({ params, searchParams }: PageProps) => {
|
||||
const parsed = querySchema.safeParse({ ...params, ...searchParams });
|
||||
const Page = ({ params }: PageProps) => {
|
||||
const parsed = querySchema.safeParse(params);
|
||||
if (!parsed.success) {
|
||||
redirect("/bookings/upcoming");
|
||||
}
|
||||
@@ -30,4 +29,4 @@ const Page = async ({ params, searchParams }: PageProps) => {
|
||||
return <BookingsList status={parsed.data.status} />;
|
||||
};
|
||||
|
||||
export default WithLayout({ ServerPage: Page })<"P">;
|
||||
export default Page;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getLayout } from "~/bookings/views/bookings-listing-view";
|
||||
|
||||
export default WithLayout({ getLayout });
|
||||
@@ -4,9 +4,6 @@ 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/d/[link]/[slug]/getServerSideProps";
|
||||
import { type PageProps } from "@lib/d/[link]/[slug]/getServerSideProps";
|
||||
@@ -17,24 +14,15 @@ 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, eventData, isBrandingHidden } = pageProps;
|
||||
const rescheduleUid = booking?.uid;
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(legacyCtx.req);
|
||||
const org = isValidOrgDomain ? currentOrgDomain : null;
|
||||
|
||||
const event = await EventRepository.getPublicEvent({
|
||||
username,
|
||||
eventSlug,
|
||||
isTeamEvent,
|
||||
org,
|
||||
fromRedirectOfNonOrgLink: legacyCtx.query.orgRedirection === "true",
|
||||
});
|
||||
|
||||
const profileName = event?.profile?.name ?? "";
|
||||
const title = event?.title ?? "";
|
||||
const profileName = eventData?.profile?.name ?? "";
|
||||
const title = eventData?.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">;
|
||||
|
||||
+45
-50
@@ -1,64 +1,59 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Typescript class based component for custom-error
|
||||
* @link https://nextjs.org/docs/advanced-features/custom-error-page
|
||||
*/
|
||||
import type { NextPage } from "next";
|
||||
import type { ErrorProps } from "next/error";
|
||||
import { captureException } from "@sentry/nextjs";
|
||||
import React from "react";
|
||||
|
||||
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { redactError } from "@calcom/lib/redactError";
|
||||
|
||||
import { ErrorPage } from "@components/error/error-page";
|
||||
|
||||
type NextError = Error & { digest?: string };
|
||||
|
||||
// Ref: https://nextjs.org/docs/app/api-reference/file-conventions/error#props
|
||||
export type DefaultErrorProps = {
|
||||
error: NextError;
|
||||
reset: () => void; // A function to reset the error boundary
|
||||
};
|
||||
|
||||
type AugmentedError = NextError | HttpError | null;
|
||||
|
||||
type CustomErrorProps = {
|
||||
err?: AugmentedError;
|
||||
statusCode?: number;
|
||||
message?: string;
|
||||
} & Omit<ErrorProps, "err" | "statusCode">;
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["[error]"] });
|
||||
|
||||
const CustomError: NextPage<DefaultErrorProps> = (props) => {
|
||||
const { error } = props;
|
||||
let errorObject: CustomErrorProps = {
|
||||
message: error.message,
|
||||
err: error,
|
||||
};
|
||||
|
||||
if (error instanceof HttpError) {
|
||||
const redactedError = redactError(error);
|
||||
errorObject = {
|
||||
statusCode: error.statusCode,
|
||||
title: redactedError.name,
|
||||
message: redactedError.message,
|
||||
err: {
|
||||
...redactedError,
|
||||
...error,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// `error.digest` property contains an automatically generated hash of the error that can be used to match the corresponding error in server-side logs
|
||||
log.debug(`${error?.toString() ?? JSON.stringify(error)}`);
|
||||
log.info("errorObject: ", errorObject);
|
||||
|
||||
return (
|
||||
<ErrorPage statusCode={errorObject.statusCode} error={errorObject.err} message={errorObject.message} />
|
||||
);
|
||||
type ErrorProps = {
|
||||
error: Error;
|
||||
reset: () => void;
|
||||
};
|
||||
|
||||
export default CustomError;
|
||||
export default function Error({ error, reset }: ErrorProps) {
|
||||
React.useEffect(() => {
|
||||
log.error(error);
|
||||
|
||||
// Log the error to Sentry
|
||||
captureException(error);
|
||||
}, [error]);
|
||||
|
||||
const processedError = React.useMemo(() => {
|
||||
const err = getErrorFromUnknown(error);
|
||||
|
||||
if (err instanceof HttpError) {
|
||||
const redactedError = redactError(err);
|
||||
return {
|
||||
statusCode: err.statusCode,
|
||||
title: redactedError.name,
|
||||
name: redactedError.name,
|
||||
message: redactedError.message,
|
||||
url: err.url,
|
||||
method: err.method,
|
||||
cause: err.cause,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 500,
|
||||
title: "Internal Server Error",
|
||||
name: "Internal Server Error",
|
||||
message: "An unexpected error occurred.",
|
||||
};
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<ErrorPage
|
||||
statusCode={processedError.statusCode}
|
||||
error={processedError}
|
||||
message={processedError.message}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+4
-5
@@ -4,11 +4,10 @@ import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { EventType } from "@calcom/atoms/monorepo";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/event-types/[type]/getServerSideProps";
|
||||
import type { PageProps as EventTypePageProps } from "@lib/event-types/[type]/getServerSideProps";
|
||||
|
||||
import EventTypePageWrapper from "~/event-types/views/event-types-single-view";
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
@@ -21,5 +20,5 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
};
|
||||
|
||||
const getData = withAppDirSsr(getServerSideProps);
|
||||
const Page = ({ type, ...rest }: EventTypePageProps) => <EventType {...rest} id={type} isAppDir={true} />;
|
||||
export default WithLayout({ getLayout: null, getData, Page })<"P">;
|
||||
|
||||
export default WithLayout({ getLayout: null, getData, Page: EventTypePageWrapper })<"P">;
|
||||
@@ -4,8 +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 { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
|
||||
import { getServerSideProps } from "@server/lib/[user]/getServerSideProps";
|
||||
@@ -24,4 +22,4 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
};
|
||||
|
||||
const getData = withAppDirSsr<LegacyPageProps>(getServerSideProps);
|
||||
export default WithLayout({ getLayout, getData, Page: LegacyPage })<"P">;
|
||||
export default WithLayout({ getData, Page: LegacyPage })<"P">;
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { withAppDirSsg } from "app/WithAppDirSsg";
|
||||
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 { AppRepository } from "@calcom/lib/server/repository/app";
|
||||
|
||||
import { getStaticProps } from "@lib/apps/[slug]/getStaticProps";
|
||||
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, "future/apps/[slug]");
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
|
||||
const legacyContext = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const res = await getData(legacyContext);
|
||||
|
||||
return await _generateMetadata(
|
||||
() => res?.data.name ?? "",
|
||||
() => res?.data.description ?? ""
|
||||
);
|
||||
};
|
||||
|
||||
export const generateStaticParams = async () => {
|
||||
try {
|
||||
const appStore = await AppRepository.findAppStore();
|
||||
return appStore.map(({ slug }) => ({ slug }));
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Prisma.PrismaClientInitializationError) {
|
||||
// Database is not available at build time, but that's ok – we fall back to resolving paths on demand
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
export default WithLayout({ getLayout: null, Page, getData });
|
||||
|
||||
export const dynamic = "force-static";
|
||||
@@ -1,34 +0,0 @@
|
||||
import { withAppDirSsg } from "app/WithAppDirSsg";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { AppCategories } from "@calcom/prisma/enums";
|
||||
import { isPrismaAvailableCheck } from "@calcom/prisma/is-prisma-available-check";
|
||||
|
||||
import { getStaticProps } from "@lib/apps/categories/[category]/getStaticProps";
|
||||
|
||||
import CategoryPage, { type PageProps } from "~/apps/categories/[category]/category-view";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
(t) => t("app_store"),
|
||||
(t) => t("app_store_description")
|
||||
);
|
||||
};
|
||||
|
||||
export const generateStaticParams = async () => {
|
||||
const paths = Object.keys(AppCategories);
|
||||
const isPrismaAvailable = await isPrismaAvailableCheck();
|
||||
|
||||
if (!isPrismaAvailable) {
|
||||
// Database is not available at build time. Make sure we fall back to building these pages on demand
|
||||
return [];
|
||||
}
|
||||
|
||||
return paths.map((category) => ({ category }));
|
||||
};
|
||||
|
||||
const getData = withAppDirSsg<PageProps>(getStaticProps, "future/apps/categories/[category]");
|
||||
|
||||
export default WithLayout({ getData, Page: CategoryPage, getLayout: null })<"P">;
|
||||
export const dynamic = "force-static";
|
||||
@@ -1,18 +0,0 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getServerSidePropsAppDir } from "@lib/apps/installed/[category]/getServerSideProps";
|
||||
|
||||
import Page from "~/apps/installed/[category]/installed-category-view";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
(t) => t("installed_apps"),
|
||||
(t) => t("manage_your_connected_apps")
|
||||
);
|
||||
};
|
||||
|
||||
const getData = withAppDirSsr(getServerSidePropsAppDir);
|
||||
|
||||
export default WithLayout({ getLayout: null, getData, Page });
|
||||
@@ -1,10 +1,10 @@
|
||||
import { type PageProps } from "@pages/org/[orgSlug]/[user]/[type]";
|
||||
import Page from "@pages/org/[orgSlug]/[user]/[type]/embed";
|
||||
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 getEmbedData = withEmbedSsrAppDir<PageProps>(getServerSideProps);
|
||||
const getEmbedData = withEmbedSsrAppDir<OrgTypePageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({ getLayout: null, getData: getEmbedData, isBookingPage: true, 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,6 +1,4 @@
|
||||
import withEmbedSsrAppDir from "app/WithEmbedSSR";
|
||||
import type { PageProps as _PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -39,4 +38,4 @@ export const generateMetadata = async ({ params, searchParams }: _PageProps) =>
|
||||
};
|
||||
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
export default WithLayout({ getLayout, getData, Page })<"P">;
|
||||
export default WithLayout({ getData, Page })<"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,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,13 +0,0 @@
|
||||
import withEmbedSsrAppDir from "app/WithEmbedSSR";
|
||||
import type { PageProps as _PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
|
||||
|
||||
import type { PageProps } from "~/team/type-view";
|
||||
import LegacyPage from "~/team/type-view";
|
||||
|
||||
const getEmbedData = withEmbedSsrAppDir<PageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({ getLayout: null, getData: getEmbedData, Page: LegacyPage })<"P">;
|
||||
@@ -1,44 +0,0 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { 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 LegacyPage, { type PageProps as LegacyPageProps } 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<LegacyPageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({
|
||||
Page: LegacyPage,
|
||||
getData,
|
||||
getLayout: null,
|
||||
isBookingPage: true,
|
||||
})<"P">;
|
||||
@@ -1,14 +0,0 @@
|
||||
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 { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
|
||||
|
||||
import type { PageProps } from "~/team/team-view";
|
||||
import LegacyPage from "~/team/team-view";
|
||||
|
||||
const getEmbedData = withEmbedSsrAppDir<PageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({ getLayout: null, getData: getEmbedData, Page: LegacyPage })<"P">;
|
||||
@@ -1,28 +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 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);
|
||||
|
||||
export default WithLayout({
|
||||
Page: LegacyPage,
|
||||
getData,
|
||||
getLayout: null,
|
||||
})<"P">;
|
||||
@@ -1,17 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { type NextPage } from "next";
|
||||
|
||||
import CustomError, { type DefaultErrorProps } from "./error";
|
||||
|
||||
export const GlobalError: NextPage<DefaultErrorProps> = (props) => {
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
<CustomError {...props} />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalError;
|
||||
@@ -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 />;
|
||||
}
|
||||
|
||||
@@ -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 InsightsVirtualQueuesPage from "~/insights/insights-virtual-queues-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: InsightsVirtualQueuesPage });
|
||||
if (!insightsEnabled) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
return <InsightsVirtualQueuesPage />;
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
@@ -76,6 +76,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}";
|
||||
@@ -118,7 +119,6 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
--font-cal: ${calFont.style.fontFamily.replace(/\'/g, "")};
|
||||
}
|
||||
`}</style>
|
||||
<IconSprites />
|
||||
</head>
|
||||
<body
|
||||
className="dark:bg-darkgray-50 bg-subtle antialiased"
|
||||
@@ -134,6 +134,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
}
|
||||
: {}
|
||||
}>
|
||||
<IconSprites />
|
||||
{!!process.env.NEXT_PUBLIC_BODY_SCRIPTS && (
|
||||
<script
|
||||
nonce={nonce}
|
||||
@@ -144,7 +145,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<TrpcProvider>{children}</TrpcProvider>
|
||||
<Providers>{children}</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
+240
-6
@@ -1,12 +1,246 @@
|
||||
import React from "react";
|
||||
import { _generateMetadata, getTranslate } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { headers } from "next/headers";
|
||||
import Link from "next/link";
|
||||
|
||||
import {
|
||||
getOrgDomainConfigFromHostname,
|
||||
subdomainSuffix,
|
||||
} from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { DOCS_URL, IS_CALCOM, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { Icon } from "@calcom/ui";
|
||||
|
||||
enum PageType {
|
||||
ORG = "ORG",
|
||||
TEAM = "TEAM",
|
||||
USER = "USER",
|
||||
OTHER = "OTHER",
|
||||
}
|
||||
|
||||
function getPageInfo(pathname: string, host: string) {
|
||||
const { isValidOrgDomain, currentOrgDomain } = getOrgDomainConfigFromHostname({ hostname: host });
|
||||
const [routerUsername] = pathname?.replace("%20", "-").split(/[?#]/) ?? [];
|
||||
if (!routerUsername || (isValidOrgDomain && currentOrgDomain)) {
|
||||
return {
|
||||
username: currentOrgDomain ?? "",
|
||||
pageType: PageType.ORG,
|
||||
url: `${WEBSITE_URL}/signup?callbackUrl=settings/organizations/new%3Fslug%3D${
|
||||
currentOrgDomain?.replace("/", "") ?? ""
|
||||
}`,
|
||||
};
|
||||
}
|
||||
|
||||
const splitPath = routerUsername.split("/");
|
||||
if (splitPath[1] === "team" && splitPath.length === 3) {
|
||||
return {
|
||||
username: splitPath[2],
|
||||
pageType: PageType.TEAM,
|
||||
url: `${WEBSITE_URL}/signup?callbackUrl=settings/teams/new%3Fslug%3D${splitPath[2].replace("/", "")}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
username: routerUsername,
|
||||
pageType: PageType.USER,
|
||||
url: `${WEBSITE_URL}/signup?username=${routerUsername.replace("/", "")}`,
|
||||
};
|
||||
}
|
||||
|
||||
async function NotFound() {
|
||||
const t = await getTranslate();
|
||||
const headersList = headers();
|
||||
const host = headersList.get("x-forwarded-host") ?? "";
|
||||
const pathname = headersList.get("x-pathname") ?? "";
|
||||
|
||||
// This makes more sense after full migration to App Router
|
||||
// if (!pathname) {
|
||||
// redirect("/500");
|
||||
// }
|
||||
|
||||
const { username, pageType, url } = getPageInfo(pathname, host);
|
||||
const isBookingSuccessPage = pathname?.startsWith("/booking");
|
||||
const isSubpage = pathname?.includes("/", 2) || isBookingSuccessPage;
|
||||
const isInsights = pathname?.startsWith("/insights");
|
||||
|
||||
const links = [
|
||||
{
|
||||
title: t("enterprise"),
|
||||
description: "Learn more about organizations and subdomains in our enterprise plan.",
|
||||
icon: "shield" as const,
|
||||
href: `${WEBSITE_URL}/enterprise`,
|
||||
},
|
||||
{
|
||||
title: t("documentation"),
|
||||
description: t("documentation_description"),
|
||||
icon: "file-text" as const,
|
||||
href: DOCS_URL,
|
||||
},
|
||||
{
|
||||
title: t("blog"),
|
||||
description: t("blog_description"),
|
||||
icon: "book-open" as const,
|
||||
href: `${WEBSITE_URL}/blog`,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* If we're on 404 and the route is insights it means it is disabled
|
||||
* TODO: Abstract this for all disabled features
|
||||
**/
|
||||
if (isInsights) {
|
||||
return (
|
||||
<div className="min-h-screen bg-white px-4" data-testid="404-page">
|
||||
<main className="mx-auto max-w-xl pb-6 pt-16 sm:pt-24">
|
||||
<div className="text-center">
|
||||
<p className="text-sm font-semibold uppercase tracking-wide text-black">{t("error_404")}</p>
|
||||
<h1 className="font-cal mt-2 text-4xl font-extrabold text-gray-900 sm:text-5xl">
|
||||
{t("feature_currently_disabled") ?? "Feature is currently disabled"}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="mt-12">
|
||||
<div className="mt-8">
|
||||
<Link href={WEBSITE_URL} className="text-base font-medium text-black hover:text-gray-500">
|
||||
{t("or_go_back_home")}
|
||||
<span aria-hidden="true"> →</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const NotFound = () => {
|
||||
return (
|
||||
<div data-testid="404-page">
|
||||
<h1>404 - Page Not Found</h1>
|
||||
<p>Sorry, the page you are looking for does not exist.</p>
|
||||
<div className="bg-default min-h-screen px-4" data-testid="404-page">
|
||||
<main className="mx-auto max-w-xl pb-6 pt-16 sm:pt-24">
|
||||
<div className="text-center">
|
||||
<p className="text-emphasis text-sm font-semibold uppercase tracking-wide">{t("error_404")}</p>
|
||||
<h1 className="font-cal text-emphasis mt-2 text-4xl font-extrabold sm:text-5xl">
|
||||
{isBookingSuccessPage ? "Booking not found" : t("page_doesnt_exist")}
|
||||
</h1>
|
||||
{isSubpage && pageType !== PageType.TEAM ? (
|
||||
<span className="mt-2 inline-block text-lg">{t("check_spelling_mistakes_or_go_back")}</span>
|
||||
) : IS_CALCOM ? (
|
||||
<a target="_blank" href={url} className="mt-2 inline-block text-lg" rel="noreferrer">
|
||||
{t(`404_the_${pageType.toLowerCase()}`)}
|
||||
|
||||
{username ? (
|
||||
<>
|
||||
<strong className="text-blue-500">{username}</strong> {t("is_still_available")}{" "}
|
||||
<span className="text-blue-500">{t("register_now")}</span>.
|
||||
</>
|
||||
) : null}
|
||||
</a>
|
||||
) : (
|
||||
<span className="mt-2 inline-block text-lg">
|
||||
{t(`404_the_${pageType.toLowerCase()}`)}{" "}
|
||||
{username ? (
|
||||
<>
|
||||
<strong className="text-lgtext-green-500 mt-2 inline-block">{username}</strong>{" "}
|
||||
{t("is_still_available")}
|
||||
</>
|
||||
) : null}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-12">
|
||||
{((!isSubpage && IS_CALCOM) || pageType === PageType.ORG || pageType === PageType.TEAM) && (
|
||||
<ul role="list" className="my-4">
|
||||
<li className="border-2 border-green-500 px-4 py-2">
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse"
|
||||
rel="noreferrer">
|
||||
<div className="flex-shrink-0">
|
||||
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-green-50">
|
||||
<Icon name="check" className="h-6 w-6 text-green-500" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="text-emphasis text-base font-medium">
|
||||
<span className="focus-within:ring-empthasis rounded-sm focus-within:ring-2 focus-within:ring-offset-2">
|
||||
<span className="focus:outline-none">
|
||||
<span className="absolute inset-0" aria-hidden="true" />
|
||||
{t("register")}{" "}
|
||||
<strong className="text-green-500">{`${
|
||||
pageType === PageType.TEAM ? `${new URL(WEBSITE_URL).host}/team/` : ""
|
||||
}${username}${pageType === PageType.ORG ? `.${subdomainSuffix()}` : ""}`}</strong>
|
||||
</span>
|
||||
</span>
|
||||
</h3>
|
||||
<p className="text-subtle text-base">{t(`404_claim_entity_${pageType.toLowerCase()}`)}</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0 self-center">
|
||||
<Icon name="chevron-right" className="text-muted h-5 w-5" aria-hidden="true" />
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
<h2 className="text-subtle text-sm font-semibold uppercase tracking-wide">{t("popular_pages")}</h2>
|
||||
<ul role="list" className="border-subtle divide-subtle divide-y">
|
||||
{links
|
||||
.filter((_, idx) => pageType === PageType.ORG || idx !== 0)
|
||||
.map((link, linkIdx) => (
|
||||
<li key={linkIdx} className="px-4 py-2">
|
||||
<a
|
||||
href={link.href}
|
||||
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
|
||||
<div className="flex-shrink-0">
|
||||
<span className="bg-muted flex h-12 w-12 items-center justify-center rounded-lg">
|
||||
<Icon name={link.icon} className="text-default h-6 w-6" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="text-emphasis text-base font-medium">
|
||||
<span className="focus-within:ring-empthasis rounded-sm focus-within:ring-2 focus-within:ring-offset-2">
|
||||
<span className="absolute inset-0" aria-hidden="true" />
|
||||
{link.title}
|
||||
</span>
|
||||
</h3>
|
||||
<p className="text-subtle text-base">{link.description}</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0 self-center">
|
||||
<Icon name="chevron-right" className="text-muted h-5 w-5" aria-hidden="true" />
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="mt-8">
|
||||
<Link href={WEBSITE_URL} className="hover:text-subtle text-emphasis text-base font-medium">
|
||||
{t("or_go_back_home")}
|
||||
<span aria-hidden="true"> →</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
const headersList = headers();
|
||||
const pathname = headersList.get("x-pathname") ?? "";
|
||||
const isInsights = pathname?.startsWith("/insights");
|
||||
|
||||
const metadata = await _generateMetadata(
|
||||
(t) =>
|
||||
isInsights
|
||||
? t("feature_currently_disabled") ?? "Feature is currently disabled"
|
||||
: t("404_page_not_found"),
|
||||
(t) => t("404_page_not_found")
|
||||
);
|
||||
return {
|
||||
...metadata,
|
||||
robots: {
|
||||
index: false,
|
||||
follow: false,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default NotFound;
|
||||
export default WithLayout({
|
||||
ServerPage: NotFound,
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user