Compare commits
48
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67af8b4973 | ||
|
|
7794f2596e | ||
|
|
167cf6e352 | ||
|
|
3d778ba481 | ||
|
|
b8f0000b59 | ||
|
|
618ef63017 | ||
|
|
ae8e633716 | ||
|
|
4a23832388 | ||
|
|
02e486d757 | ||
|
|
af6e24f433 | ||
|
|
f9e24f51a7 | ||
|
|
9e9f815809 | ||
|
|
a91561f3c2 | ||
|
|
ff79064ca7 | ||
|
|
1a573e1619 | ||
|
|
511be7386c | ||
|
|
2a0991c259 | ||
|
|
2e454c9723 | ||
|
|
7e6d94c8e0 | ||
|
|
e18b790ccd | ||
|
|
0274417ad1 | ||
|
|
a8a3a93ace | ||
|
|
88bbab216e | ||
|
|
f74434a282 | ||
|
|
ed6f9d8252 | ||
|
|
a19ccab16a | ||
|
|
77369a13f1 | ||
|
|
8231356d2a | ||
|
|
469a942b5a | ||
|
|
0afe6cf5d1 | ||
|
|
75a161fe35 | ||
|
|
eb1ed107ba | ||
|
|
13e2e340ac | ||
|
|
0af62da16b | ||
|
|
9b323cbcc8 | ||
|
|
18a0e2d75c | ||
|
|
87c89a9968 | ||
|
|
e237749abd | ||
|
|
994a36d039 | ||
|
|
c989817373 | ||
|
|
e3fb7340aa | ||
|
|
e4ef2832cd | ||
|
|
881602cf1c | ||
|
|
4c01f171ae | ||
|
|
d9817fcb07 | ||
|
|
a784af3e47 | ||
|
|
c268925330 | ||
|
|
378e8e0330 |
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@calcom/atoms": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: Availability atom handleFormSubmit callbacks not triggering
|
||||||
Vendored
+2
-2
File diff suppressed because one or more lines are too long
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
undecided:
|
||||||
|
- calcom-monorepo
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
@@ -199,7 +199,7 @@ There's some settings that you'll need to setup.
|
|||||||
Under Vercel > Your API Project > Settings
|
Under Vercel > Your API Project > Settings
|
||||||
|
|
||||||
In General > Build & Development Settings
|
In General > Build & Development Settings
|
||||||
BUILD COMMAND: `yarn turbo run build --scope=@calcom/api --include-dependencies --no-deps`
|
BUILD COMMAND: `yarn turbo run build --filter=@calcom/api --no-deps`
|
||||||
OUTPUT DIRECTORY: `apps/api/.next`
|
OUTPUT DIRECTORY: `apps/api/.next`
|
||||||
|
|
||||||
In Git > Ignored Build Step
|
In Git > Ignored Build Step
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
"@sentry/nextjs": "^9.15.0",
|
"@sentry/nextjs": "^9.15.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"memory-cache": "^0.2.0",
|
"memory-cache": "^0.2.0",
|
||||||
"next": "^15.3.0",
|
"next": "^15.4.5",
|
||||||
"next-api-middleware": "^1.0.1",
|
"next-api-middleware": "^1.0.1",
|
||||||
"next-axiom": "^0.17.0",
|
"next-axiom": "^0.17.0",
|
||||||
"next-swagger-doc": "^0.3.6",
|
"next-swagger-doc": "^0.3.6",
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
"@axiomhq/winston": "^1.2.0",
|
"@axiomhq/winston": "^1.2.0",
|
||||||
"@calcom/platform-constants": "*",
|
"@calcom/platform-constants": "*",
|
||||||
"@calcom/platform-enums": "*",
|
"@calcom/platform-enums": "*",
|
||||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.300",
|
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.318",
|
||||||
"@calcom/platform-types": "*",
|
"@calcom/platform-types": "*",
|
||||||
"@calcom/platform-utils": "*",
|
"@calcom/platform-utils": "*",
|
||||||
"@calcom/prisma": "*",
|
"@calcom/prisma": "*",
|
||||||
|
|||||||
+233
-3
@@ -15,11 +15,11 @@ import { NestExpressApplication } from "@nestjs/platform-express";
|
|||||||
import { Test } from "@nestjs/testing";
|
import { Test } from "@nestjs/testing";
|
||||||
import { PlatformOAuthClient, Team, User } from "@prisma/client";
|
import { PlatformOAuthClient, Team, User } from "@prisma/client";
|
||||||
import * as request from "supertest";
|
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 { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||||
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
|
|
||||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
import { randomString } from "test/utils/randomString";
|
import { randomString } from "test/utils/randomString";
|
||||||
@@ -36,6 +36,7 @@ import {
|
|||||||
CreateBookingInput_2024_08_13,
|
CreateBookingInput_2024_08_13,
|
||||||
CreateEventTypeInput_2024_06_14,
|
CreateEventTypeInput_2024_06_14,
|
||||||
EventTypeOutput_2024_06_14,
|
EventTypeOutput_2024_06_14,
|
||||||
|
GetBookingOutput_2024_08_13,
|
||||||
GetBookingsOutput_2024_08_13,
|
GetBookingsOutput_2024_08_13,
|
||||||
} from "@calcom/platform-types";
|
} from "@calcom/platform-types";
|
||||||
|
|
||||||
@@ -50,9 +51,9 @@ describe("Managed user bookings 2024-08-13", () => {
|
|||||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||||
let schedulesRepositoryFixture: SchedulesRepositoryFixture;
|
|
||||||
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
||||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||||
|
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||||
|
|
||||||
const platformAdminEmail = `managed-users-bookings-2024-08-13-admin-${randomString()}@api.com`;
|
const platformAdminEmail = `managed-users-bookings-2024-08-13-admin-${randomString()}@api.com`;
|
||||||
let platformAdmin: User;
|
let platformAdmin: User;
|
||||||
@@ -67,6 +68,10 @@ describe("Managed user bookings 2024-08-13", () => {
|
|||||||
let thirdManagedUser: CreateManagedUserData;
|
let thirdManagedUser: CreateManagedUserData;
|
||||||
|
|
||||||
let firstManagedUserEventTypeId: number;
|
let firstManagedUserEventTypeId: number;
|
||||||
|
let eventTypeRequiresConfirmationId: number;
|
||||||
|
|
||||||
|
const orgAdminManagedUserEmail = `managed-user-bookings-2024-08-13-org-admin-${randomString()}@api.com`;
|
||||||
|
let orgAdminManagedUser: CreateManagedUserData;
|
||||||
|
|
||||||
let firstManagedUserBookingsCount = 0;
|
let firstManagedUserBookingsCount = 0;
|
||||||
let secondManagedUserBookingsCount = 0;
|
let secondManagedUserBookingsCount = 0;
|
||||||
@@ -85,9 +90,9 @@ describe("Managed user bookings 2024-08-13", () => {
|
|||||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||||
schedulesRepositoryFixture = new SchedulesRepositoryFixture(moduleRef);
|
|
||||||
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||||
|
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||||
|
|
||||||
platformAdmin = await userRepositoryFixture.create({ email: platformAdminEmail });
|
platformAdmin = await userRepositoryFixture.create({ email: platformAdminEmail });
|
||||||
|
|
||||||
@@ -95,6 +100,13 @@ describe("Managed user bookings 2024-08-13", () => {
|
|||||||
name: `oauth-client-users-organization-${randomString()}`,
|
name: `oauth-client-users-organization-${randomString()}`,
|
||||||
isPlatform: true,
|
isPlatform: true,
|
||||||
isOrganization: true,
|
isOrganization: true,
|
||||||
|
platformBilling: {
|
||||||
|
create: {
|
||||||
|
customerId: "cus_999",
|
||||||
|
plan: "SCALE",
|
||||||
|
subscriptionId: "sub_999",
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
oAuthClient = await createOAuthClient(organization.id);
|
oAuthClient = await createOAuthClient(organization.id);
|
||||||
|
|
||||||
@@ -243,6 +255,59 @@ describe("Managed user bookings 2024-08-13", () => {
|
|||||||
thirdManagedUser = responseBody.data;
|
thirdManagedUser = responseBody.data;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it(`should create org admin managed user`, async () => {
|
||||||
|
const requestBody: CreateManagedUserInput = {
|
||||||
|
email: orgAdminManagedUserEmail,
|
||||||
|
timeZone: managedUsersTimeZone,
|
||||||
|
weekStart: "Monday",
|
||||||
|
timeFormat: 24,
|
||||||
|
locale: Locales.FR,
|
||||||
|
name: "Org Admin Smith",
|
||||||
|
avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
|
||||||
|
.set("x-cal-secret-key", oAuthClient.secret)
|
||||||
|
.send(requestBody)
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
const responseBody: CreateManagedUserOutput = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(responseBody.data.user.email).toEqual(
|
||||||
|
OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email)
|
||||||
|
);
|
||||||
|
expect(responseBody.data.accessToken).toBeDefined();
|
||||||
|
expect(responseBody.data.refreshToken).toBeDefined();
|
||||||
|
|
||||||
|
orgAdminManagedUser = responseBody.data;
|
||||||
|
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.post(`/v2/organizations/${organization.id}/memberships`)
|
||||||
|
.set("x-cal-client-id", oAuthClient.id)
|
||||||
|
.set("x-cal-secret-key", oAuthClient.secret)
|
||||||
|
.send({
|
||||||
|
userId: orgAdminManagedUser.user.id,
|
||||||
|
role: "ADMIN",
|
||||||
|
accepted: true,
|
||||||
|
})
|
||||||
|
.expect(201);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create an event type requiring confirmation for first managed user", async () => {
|
||||||
|
const eventTypeRequiresConfirmation = await eventTypesRepositoryFixture.create(
|
||||||
|
{
|
||||||
|
title: `managed-user-bookings-event-type-requires-confirmation-${randomString()}`,
|
||||||
|
slug: `managed-user-bookings-event-type-requires-confirmation-${randomString()}`,
|
||||||
|
length: 60,
|
||||||
|
requiresConfirmation: true,
|
||||||
|
},
|
||||||
|
firstManagedUser.user.id
|
||||||
|
);
|
||||||
|
eventTypeRequiresConfirmationId = eventTypeRequiresConfirmation.id;
|
||||||
|
});
|
||||||
|
|
||||||
describe("bookings using original emails", () => {
|
describe("bookings using original emails", () => {
|
||||||
it("managed user should be booked by managed user attendee and booking shows up in both users' bookings", async () => {
|
it("managed user should be booked by managed user attendee and booking shows up in both users' bookings", async () => {
|
||||||
const body: CreateBookingInput_2024_08_13 = {
|
const body: CreateBookingInput_2024_08_13 = {
|
||||||
@@ -543,10 +608,175 @@ describe("Managed user bookings 2024-08-13", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("booking confirmation by org admin", () => {
|
||||||
|
it("should allow org admin managed user to confirm booking using access token", async () => {
|
||||||
|
const bookingRequiringConfirmation = await bookingsRepositoryFixture.create({
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: firstManagedUser.user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
startTime: new Date(Date.UTC(2030, 0, 9, 13, 0, 0)),
|
||||||
|
endTime: new Date(Date.UTC(2030, 0, 9, 14, 0, 0)),
|
||||||
|
title: "Booking requiring confirmation",
|
||||||
|
uid: `booking-requiring-confirmation-${randomString()}`,
|
||||||
|
eventType: {
|
||||||
|
connect: {
|
||||||
|
id: eventTypeRequiresConfirmationId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
location: "https://meet.google.com/abc-def-ghi",
|
||||||
|
customInputs: {},
|
||||||
|
metadata: {},
|
||||||
|
status: "PENDING",
|
||||||
|
responses: {
|
||||||
|
name: secondManagedUser.user.name,
|
||||||
|
email: secondManagedUserEmail,
|
||||||
|
},
|
||||||
|
attendees: {
|
||||||
|
create: {
|
||||||
|
email: secondManagedUserEmail,
|
||||||
|
name: secondManagedUser.user.name!,
|
||||||
|
locale: secondManagedUser.user.locale,
|
||||||
|
timeZone: secondManagedUser.user.timeZone,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post(`/v2/bookings/${bookingRequiringConfirmation.uid}/confirm`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
|
||||||
|
const bookingData = responseBody.data as BookingOutput_2024_08_13;
|
||||||
|
expect(bookingData.status).toEqual("accepted");
|
||||||
|
expect(bookingData.uid).toEqual(bookingRequiringConfirmation.uid);
|
||||||
|
|
||||||
|
const confirmedBooking = await bookingsRepositoryFixture.getByUid(bookingRequiringConfirmation.uid);
|
||||||
|
expect(confirmedBooking?.status).toEqual("ACCEPTED");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow org admin managed user to reject booking using access token", async () => {
|
||||||
|
const bookingRequiringConfirmation = await bookingsRepositoryFixture.create({
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: firstManagedUser.user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
startTime: new Date(Date.UTC(2030, 0, 9, 15, 0, 0)),
|
||||||
|
endTime: new Date(Date.UTC(2030, 0, 9, 16, 0, 0)),
|
||||||
|
title: "Booking requiring rejection",
|
||||||
|
uid: `booking-requiring-rejection-${randomString()}`,
|
||||||
|
eventType: {
|
||||||
|
connect: {
|
||||||
|
id: eventTypeRequiresConfirmationId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
location: "https://meet.google.com/abc-def-ghi",
|
||||||
|
customInputs: {},
|
||||||
|
metadata: {},
|
||||||
|
status: "PENDING",
|
||||||
|
responses: {
|
||||||
|
name: secondManagedUser.user.name,
|
||||||
|
email: secondManagedUserEmail,
|
||||||
|
},
|
||||||
|
attendees: {
|
||||||
|
create: {
|
||||||
|
email: secondManagedUserEmail,
|
||||||
|
name: secondManagedUser.user.name!,
|
||||||
|
locale: secondManagedUser.user.locale,
|
||||||
|
timeZone: secondManagedUser.user.timeZone,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post(`/v2/bookings/${bookingRequiringConfirmation.uid}/decline`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
|
||||||
|
const bookingData = responseBody.data as BookingOutput_2024_08_13;
|
||||||
|
expect(bookingData.status).toEqual("rejected");
|
||||||
|
expect(bookingData.uid).toEqual(bookingRequiringConfirmation.uid);
|
||||||
|
|
||||||
|
const rejectedBooking = await bookingsRepositoryFixture.getByUid(bookingRequiringConfirmation.uid);
|
||||||
|
expect(rejectedBooking?.status).toEqual("REJECTED");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return unauthorized when org admin tries to confirm regular user's booking", async () => {
|
||||||
|
const regularUser = await userRepositoryFixture.create({
|
||||||
|
email: `managed-user-bookings-2024-08-13-regular-user-${randomString()}@api.com`,
|
||||||
|
name: "Regular User",
|
||||||
|
});
|
||||||
|
|
||||||
|
const regularUserEventType = await eventTypesRepositoryFixture.create(
|
||||||
|
{
|
||||||
|
title: `regular-user-event-type-requires-confirmation-${randomString()}`,
|
||||||
|
slug: `regular-user-event-type-requires-confirmation-${randomString()}`,
|
||||||
|
length: 60,
|
||||||
|
requiresConfirmation: true,
|
||||||
|
},
|
||||||
|
regularUser.id
|
||||||
|
);
|
||||||
|
|
||||||
|
const regularUserBooking = await bookingsRepositoryFixture.create({
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: regularUser.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
startTime: new Date(Date.UTC(2030, 0, 9, 17, 0, 0)),
|
||||||
|
endTime: new Date(Date.UTC(2030, 0, 9, 18, 0, 0)),
|
||||||
|
title: "Regular user booking requiring confirmation",
|
||||||
|
uid: `regular-user-booking-${randomString()}`,
|
||||||
|
eventType: {
|
||||||
|
connect: {
|
||||||
|
id: regularUserEventType.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
location: "https://meet.google.com/abc-def-ghi",
|
||||||
|
customInputs: {},
|
||||||
|
metadata: {},
|
||||||
|
status: "PENDING",
|
||||||
|
responses: {
|
||||||
|
name: "External Attendee",
|
||||||
|
email: "external@example.com",
|
||||||
|
},
|
||||||
|
attendees: {
|
||||||
|
create: {
|
||||||
|
email: "external@example.com",
|
||||||
|
name: "External Attendee",
|
||||||
|
locale: "en",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.post(`/v2/bookings/${regularUserBooking.uid}/confirm`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`)
|
||||||
|
.expect(401);
|
||||||
|
|
||||||
|
await userRepositoryFixture.delete(regularUser.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await userRepositoryFixture.delete(firstManagedUser.user.id);
|
await userRepositoryFixture.delete(firstManagedUser.user.id);
|
||||||
await userRepositoryFixture.delete(secondManagedUser.user.id);
|
await userRepositoryFixture.delete(secondManagedUser.user.id);
|
||||||
await userRepositoryFixture.delete(thirdManagedUser.user.id);
|
await userRepositoryFixture.delete(thirdManagedUser.user.id);
|
||||||
|
await userRepositoryFixture.delete(orgAdminManagedUser.user.id);
|
||||||
await userRepositoryFixture.delete(platformAdmin.id);
|
await userRepositoryFixture.delete(platformAdmin.id);
|
||||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||||
await teamRepositoryFixture.delete(organization.id);
|
await teamRepositoryFixture.delete(organization.id);
|
||||||
|
|||||||
@@ -702,6 +702,7 @@ export class InputBookingsService_2024_08_13 {
|
|||||||
beforeUpdatedDate: queryParams.beforeUpdatedAt,
|
beforeUpdatedDate: queryParams.beforeUpdatedAt,
|
||||||
afterCreatedDate: queryParams.afterCreatedAt,
|
afterCreatedDate: queryParams.afterCreatedAt,
|
||||||
beforeCreatedDate: queryParams.beforeCreatedAt,
|
beforeCreatedDate: queryParams.beforeCreatedAt,
|
||||||
|
bookingUid: queryParams.bookingUid,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+8
-28
@@ -4,37 +4,21 @@ import { Permissions } from "@/modules/auth/decorators/permissions/permissions.d
|
|||||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
import { EventTypeOwnershipGuard } from "@/modules/event-types/guards/event-type-ownership.guard";
|
import { EventTypeOwnershipGuard } from "@/modules/event-types/guards/event-type-ownership.guard";
|
||||||
import {
|
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from "@nestjs/common";
|
||||||
Body,
|
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
Controller,
|
|
||||||
Delete,
|
|
||||||
Get,
|
|
||||||
Param,
|
|
||||||
ParseIntPipe,
|
|
||||||
Patch,
|
|
||||||
Post,
|
|
||||||
UseGuards,
|
|
||||||
} from "@nestjs/common";
|
|
||||||
import { ApiHeader, ApiOperation, ApiTags as DocsTags, OmitType } from "@nestjs/swagger";
|
|
||||||
|
|
||||||
import {
|
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
EVENT_TYPE_READ,
|
|
||||||
EVENT_TYPE_WRITE,
|
|
||||||
SUCCESS_STATUS,
|
|
||||||
} from "@calcom/platform-constants";
|
|
||||||
import {
|
import {
|
||||||
CreatePrivateLinkInput,
|
CreatePrivateLinkInput,
|
||||||
CreatePrivateLinkOutput,
|
CreatePrivateLinkOutput,
|
||||||
DeletePrivateLinkOutput,
|
DeletePrivateLinkOutput,
|
||||||
GetPrivateLinksOutput,
|
GetPrivateLinksOutput,
|
||||||
UpdatePrivateLinkInput,
|
|
||||||
UpdatePrivateLinkOutput,
|
UpdatePrivateLinkOutput,
|
||||||
|
UpdatePrivateLinkBody,
|
||||||
} from "@calcom/platform-types";
|
} from "@calcom/platform-types";
|
||||||
|
|
||||||
import { PrivateLinksService } from "../services/private-links.service";
|
import { PrivateLinksService } from "../services/private-links.service";
|
||||||
|
|
||||||
class UpdatePrivateLinkBody extends OmitType(UpdatePrivateLinkInput, ["linkId"] as const) {}
|
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "/v2/event-types/:eventTypeId/private-links",
|
path: "/v2/event-types/:eventTypeId/private-links",
|
||||||
})
|
})
|
||||||
@@ -85,11 +69,10 @@ export class EventTypesPrivateLinksController {
|
|||||||
async updatePrivateLink(
|
async updatePrivateLink(
|
||||||
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||||
@Param("linkId") linkId: string,
|
@Param("linkId") linkId: string,
|
||||||
@Body() body: UpdatePrivateLinkBody,
|
@Body() body: UpdatePrivateLinkBody
|
||||||
@GetUser("id") userId: number
|
|
||||||
): Promise<UpdatePrivateLinkOutput> {
|
): Promise<UpdatePrivateLinkOutput> {
|
||||||
const updateInput = { ...body, linkId };
|
const updateInput = { ...body, linkId };
|
||||||
const privateLink = await this.privateLinksService.updatePrivateLink(eventTypeId, userId, updateInput);
|
const privateLink = await this.privateLinksService.updatePrivateLink(eventTypeId, updateInput);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
@@ -104,10 +87,9 @@ export class EventTypesPrivateLinksController {
|
|||||||
@ApiOperation({ summary: "Delete a private link for an event type" })
|
@ApiOperation({ summary: "Delete a private link for an event type" })
|
||||||
async deletePrivateLink(
|
async deletePrivateLink(
|
||||||
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||||
@Param("linkId") linkId: string,
|
@Param("linkId") linkId: string
|
||||||
@GetUser("id") userId: number
|
|
||||||
): Promise<DeletePrivateLinkOutput> {
|
): Promise<DeletePrivateLinkOutput> {
|
||||||
await this.privateLinksService.deletePrivateLink(eventTypeId, userId, linkId);
|
await this.privateLinksService.deletePrivateLink(eventTypeId, linkId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
@@ -118,5 +100,3 @@ export class EventTypesPrivateLinksController {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+121
@@ -0,0 +1,121 @@
|
|||||||
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
|
import {
|
||||||
|
OPTIONAL_API_KEY_HEADER,
|
||||||
|
OPTIONAL_X_CAL_CLIENT_ID_HEADER,
|
||||||
|
OPTIONAL_X_CAL_SECRET_KEY_HEADER,
|
||||||
|
} from "@/lib/docs/headers";
|
||||||
|
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 { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
|
||||||
|
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
|
||||||
|
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from "@nestjs/common";
|
||||||
|
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import {
|
||||||
|
CreatePrivateLinkInput,
|
||||||
|
CreatePrivateLinkOutput,
|
||||||
|
DeletePrivateLinkOutput,
|
||||||
|
GetPrivateLinksOutput,
|
||||||
|
UpdatePrivateLinkInput,
|
||||||
|
UpdatePrivateLinkOutput,
|
||||||
|
} from "@calcom/platform-types";
|
||||||
|
|
||||||
|
import { PrivateLinksService } from "../services/private-links.service";
|
||||||
|
|
||||||
|
@Controller({
|
||||||
|
path: "/v2/organizations/:orgId/teams/:teamId/event-types/:eventTypeId/private-links",
|
||||||
|
version: API_VERSIONS_VALUES,
|
||||||
|
})
|
||||||
|
@DocsTags("Orgs / Teams / Event Types / Private Links")
|
||||||
|
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
|
||||||
|
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
|
||||||
|
@ApiHeader(OPTIONAL_API_KEY_HEADER)
|
||||||
|
export class OrganizationsEventTypesPrivateLinksController {
|
||||||
|
constructor(
|
||||||
|
private readonly privateLinksService: PrivateLinksService,
|
||||||
|
private readonly teamsEventTypesService: TeamsEventTypesService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Post("/")
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||||
|
@ApiOperation({ summary: "Create a private link for a team event type" })
|
||||||
|
async createPrivateLink(
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number,
|
||||||
|
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||||
|
@Body() body: CreatePrivateLinkInput
|
||||||
|
): Promise<CreatePrivateLinkOutput> {
|
||||||
|
await this.teamsEventTypesService.validateEventTypeExists(teamId, eventTypeId);
|
||||||
|
// Use teamId as the seed for link generation in org/team context
|
||||||
|
const privateLink = await this.privateLinksService.createPrivateLink(eventTypeId, teamId, body);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: privateLink,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/")
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||||
|
@ApiOperation({ summary: "Get all private links for a team event type" })
|
||||||
|
async getPrivateLinks(
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number,
|
||||||
|
@Param("eventTypeId", ParseIntPipe) eventTypeId: number
|
||||||
|
): Promise<GetPrivateLinksOutput> {
|
||||||
|
await this.teamsEventTypesService.validateEventTypeExists(teamId, eventTypeId);
|
||||||
|
const privateLinks = await this.privateLinksService.getPrivateLinks(eventTypeId);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: privateLinks,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch("/:linkId")
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||||
|
@ApiOperation({ summary: "Update a private link for a team event type" })
|
||||||
|
async updatePrivateLink(
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number,
|
||||||
|
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||||
|
@Param("linkId") linkId: string,
|
||||||
|
@Body() body: Omit<UpdatePrivateLinkInput, "linkId">
|
||||||
|
): Promise<UpdatePrivateLinkOutput> {
|
||||||
|
await this.teamsEventTypesService.validateEventTypeExists(teamId, eventTypeId);
|
||||||
|
const updateInput: UpdatePrivateLinkInput = { ...body, linkId };
|
||||||
|
const privateLink = await this.privateLinksService.updatePrivateLink(eventTypeId, updateInput);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: privateLink,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete("/:linkId")
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||||
|
@ApiOperation({ summary: "Delete a private link for a team event type" })
|
||||||
|
async deletePrivateLink(
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number,
|
||||||
|
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||||
|
@Param("linkId") linkId: string
|
||||||
|
): Promise<DeletePrivateLinkOutput> {
|
||||||
|
await this.teamsEventTypesService.validateEventTypeExists(teamId, eventTypeId);
|
||||||
|
await this.privateLinksService.deletePrivateLink(eventTypeId, linkId);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: {
|
||||||
|
linkId,
|
||||||
|
message: "Private link deleted successfully",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ import { PrivateLinksOutputService } from "./services/private-links-output.servi
|
|||||||
import { PrivateLinksService } from "./services/private-links.service";
|
import { PrivateLinksService } from "./services/private-links.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TokensModule, OAuthClientModule, PrismaModule, EventTypesModule_2024_06_14],
|
imports: [TokensModule, PrismaModule, EventTypesModule_2024_06_14],
|
||||||
controllers: [EventTypesPrivateLinksController],
|
controllers: [EventTypesPrivateLinksController],
|
||||||
providers: [
|
providers: [
|
||||||
PrivateLinksService,
|
PrivateLinksService,
|
||||||
@@ -21,5 +21,6 @@ import { PrivateLinksService } from "./services/private-links.service";
|
|||||||
PrivateLinksRepository,
|
PrivateLinksRepository,
|
||||||
EventTypeOwnershipGuard,
|
EventTypeOwnershipGuard,
|
||||||
],
|
],
|
||||||
|
exports: [PrivateLinksService],
|
||||||
})
|
})
|
||||||
export class EventTypesPrivateLinksModule {}
|
export class EventTypesPrivateLinksModule {}
|
||||||
|
|||||||
@@ -68,11 +68,7 @@ export class PrivateLinksService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async updatePrivateLink(
|
async updatePrivateLink(eventTypeId: number, input: UpdatePrivateLinkInput): Promise<PrivateLinkOutput> {
|
||||||
eventTypeId: number,
|
|
||||||
userId: number,
|
|
||||||
input: UpdatePrivateLinkInput
|
|
||||||
): Promise<PrivateLinkOutput> {
|
|
||||||
try {
|
try {
|
||||||
const transformedInput = this.inputService.transformUpdateInput(input);
|
const transformedInput = this.inputService.transformUpdateInput(input);
|
||||||
const updatedResult = await this.repo.update(eventTypeId, {
|
const updatedResult = await this.repo.update(eventTypeId, {
|
||||||
@@ -106,7 +102,7 @@ export class PrivateLinksService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async deletePrivateLink(eventTypeId: number, userId: number, linkId: string): Promise<void> {
|
async deletePrivateLink(eventTypeId: number, linkId: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const { count } = await this.repo.delete(eventTypeId, linkId);
|
const { count } = await this.repo.delete(eventTypeId, linkId);
|
||||||
if (count === 0) {
|
if (count === 0) {
|
||||||
|
|||||||
+45
@@ -2147,6 +2147,51 @@ describe("Event types Endpoints", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("EventType Hidden Property", () => {
|
||||||
|
let createdEventTypeId: number;
|
||||||
|
|
||||||
|
it("should create an event type with hidden=true", async () => {
|
||||||
|
const createPayload = {
|
||||||
|
title: "Hidden Event",
|
||||||
|
slug: "hidden-event",
|
||||||
|
lengthInMinutes: 30,
|
||||||
|
hidden: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post("/api/v2/event-types")
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
.send(createPayload)
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||||
|
const createdEventType = responseBody.data;
|
||||||
|
expect(createdEventType).toHaveProperty("id");
|
||||||
|
expect(createdEventType.title).toEqual(createPayload.title);
|
||||||
|
expect(createdEventType.slug).toEqual(createPayload.slug);
|
||||||
|
expect(createdEventType.lengthInMinutes).toEqual(createPayload.lengthInMinutes);
|
||||||
|
expect(createdEventType.hidden).toBe(true);
|
||||||
|
|
||||||
|
createdEventTypeId = createdEventType.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update the hidden property to false", async () => {
|
||||||
|
const updatePayload = {
|
||||||
|
hidden: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.patch(`/api/v2/event-types/${createdEventTypeId}`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
.send(updatePayload)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||||
|
const updatedEventType = responseBody.data;
|
||||||
|
+expect(updatedEventType.hidden).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||||
await teamRepositoryFixture.delete(organization.id);
|
await teamRepositoryFixture.delete(organization.id);
|
||||||
|
|||||||
+3
@@ -89,6 +89,7 @@ type Input = Pick<
|
|||||||
| "hideCalendarEventDetails"
|
| "hideCalendarEventDetails"
|
||||||
| "hideOrganizerEmail"
|
| "hideOrganizerEmail"
|
||||||
| "calVideoSettings"
|
| "calVideoSettings"
|
||||||
|
| "hidden"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -127,6 +128,7 @@ export class OutputEventTypesService_2024_06_14 {
|
|||||||
hideCalendarEventDetails,
|
hideCalendarEventDetails,
|
||||||
hideOrganizerEmail,
|
hideOrganizerEmail,
|
||||||
calVideoSettings,
|
calVideoSettings,
|
||||||
|
hidden,
|
||||||
} = databaseEventType;
|
} = databaseEventType;
|
||||||
|
|
||||||
const locations = this.transformLocations(databaseEventType.locations);
|
const locations = this.transformLocations(databaseEventType.locations);
|
||||||
@@ -203,6 +205,7 @@ export class OutputEventTypesService_2024_06_14 {
|
|||||||
hideCalendarEventDetails,
|
hideCalendarEventDetails,
|
||||||
hideOrganizerEmail,
|
hideOrganizerEmail,
|
||||||
calVideoSettings,
|
calVideoSettings,
|
||||||
|
hidden,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -229,4 +229,19 @@ export class SchedulesRepository_2024_06_11 {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getSchedulesByUserIds(userIds: number[], skip: number, take: number) {
|
||||||
|
return this.dbRead.prisma.schedule.findMany({
|
||||||
|
where: {
|
||||||
|
userId: {
|
||||||
|
in: userIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
availability: true,
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
|
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
|
||||||
import { PrismaEventTypeRepository } from "@/lib/repositories/prisma-event-type.repository";
|
import { PrismaEventTypeRepository } from "@/lib/repositories/prisma-event-type.repository";
|
||||||
import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository";
|
import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository";
|
||||||
|
import { PrismaMembershipRepository } from "@/lib/repositories/prisma-membership.repository";
|
||||||
import { PrismaOOORepository } from "@/lib/repositories/prisma-ooo.repository";
|
import { PrismaOOORepository } from "@/lib/repositories/prisma-ooo.repository";
|
||||||
import { PrismaRoutingFormResponseRepository } from "@/lib/repositories/prisma-routing-form-response.repository";
|
import { PrismaRoutingFormResponseRepository } from "@/lib/repositories/prisma-routing-form-response.repository";
|
||||||
import { PrismaScheduleRepository } from "@/lib/repositories/prisma-schedule.repository";
|
import { PrismaScheduleRepository } from "@/lib/repositories/prisma-schedule.repository";
|
||||||
@@ -12,6 +13,7 @@ import { BusyTimesService } from "@/lib/services/busy-times.service";
|
|||||||
import { CacheService } from "@/lib/services/cache.service";
|
import { CacheService } from "@/lib/services/cache.service";
|
||||||
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
|
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
|
||||||
import { FilterHostsService } from "@/lib/services/filter-hosts.service";
|
import { FilterHostsService } from "@/lib/services/filter-hosts.service";
|
||||||
|
import { NoSlotsNotificationService } from "@/lib/services/no-slots-notification.service";
|
||||||
import { QualifiedHostsService } from "@/lib/services/qualified-hosts.service";
|
import { QualifiedHostsService } from "@/lib/services/qualified-hosts.service";
|
||||||
import { UserAvailabilityService } from "@/lib/services/user-availability.service";
|
import { UserAvailabilityService } from "@/lib/services/user-availability.service";
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
@@ -31,6 +33,7 @@ import { Module } from "@nestjs/common";
|
|||||||
PrismaTeamRepository,
|
PrismaTeamRepository,
|
||||||
RedisService,
|
RedisService,
|
||||||
PrismaFeaturesRepository,
|
PrismaFeaturesRepository,
|
||||||
|
PrismaMembershipRepository,
|
||||||
CheckBookingLimitsService,
|
CheckBookingLimitsService,
|
||||||
CacheService,
|
CacheService,
|
||||||
AvailableSlotsService,
|
AvailableSlotsService,
|
||||||
@@ -38,6 +41,7 @@ import { Module } from "@nestjs/common";
|
|||||||
BusyTimesService,
|
BusyTimesService,
|
||||||
FilterHostsService,
|
FilterHostsService,
|
||||||
QualifiedHostsService,
|
QualifiedHostsService,
|
||||||
|
NoSlotsNotificationService,
|
||||||
],
|
],
|
||||||
exports: [AvailableSlotsService],
|
exports: [AvailableSlotsService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { PrismaMembershipRepository as PrismaMembershipRepositoryLib } from "@calcom/platform-libraries/repositories";
|
||||||
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaMembershipRepository extends PrismaMembershipRepositoryLib {
|
||||||
|
constructor(private readonly dbWrite: PrismaWriteService) {
|
||||||
|
super(dbWrite.prisma as unknown as PrismaClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository"
|
|||||||
import { BusyTimesService } from "@/lib/services/busy-times.service";
|
import { BusyTimesService } from "@/lib/services/busy-times.service";
|
||||||
import { CacheService } from "@/lib/services/cache.service";
|
import { CacheService } from "@/lib/services/cache.service";
|
||||||
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
|
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
|
||||||
|
import { NoSlotsNotificationService } from "@/lib/services/no-slots-notification.service";
|
||||||
import { QualifiedHostsService } from "@/lib/services/qualified-hosts.service";
|
import { QualifiedHostsService } from "@/lib/services/qualified-hosts.service";
|
||||||
import { RedisService } from "@/modules/redis/redis.service";
|
import { RedisService } from "@/modules/redis/redis.service";
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
@@ -35,7 +36,8 @@ export class AvailableSlotsService extends BaseAvailableSlotsService {
|
|||||||
checkBookingLimitsService: CheckBookingLimitsService,
|
checkBookingLimitsService: CheckBookingLimitsService,
|
||||||
cacheService: CacheService,
|
cacheService: CacheService,
|
||||||
userAvailabilityService: UserAvailabilityService,
|
userAvailabilityService: UserAvailabilityService,
|
||||||
busyTimesService: BusyTimesService
|
busyTimesService: BusyTimesService,
|
||||||
|
noSlotsNotificationService: NoSlotsNotificationService
|
||||||
) {
|
) {
|
||||||
super({
|
super({
|
||||||
oooRepo: oooRepoDependency,
|
oooRepo: oooRepoDependency,
|
||||||
@@ -53,6 +55,7 @@ export class AvailableSlotsService extends BaseAvailableSlotsService {
|
|||||||
busyTimesService,
|
busyTimesService,
|
||||||
qualifiedHostsService,
|
qualifiedHostsService,
|
||||||
featuresRepo: featuresRepository,
|
featuresRepo: featuresRepository,
|
||||||
|
noSlotsNotificationService,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { PrismaMembershipRepository } from "@/lib/repositories/prisma-membership.repository";
|
||||||
|
import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository";
|
||||||
|
import { RedisService } from "@/modules/redis/redis.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { NoSlotsNotificationService as BaseNoSlotsNotificationService } from "@calcom/platform-libraries/slots";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class NoSlotsNotificationService extends BaseNoSlotsNotificationService {
|
||||||
|
constructor(
|
||||||
|
teamRepository: PrismaTeamRepository,
|
||||||
|
membershipRepository: PrismaMembershipRepository,
|
||||||
|
redisService: RedisService
|
||||||
|
) {
|
||||||
|
super({
|
||||||
|
teamRepo: teamRepository,
|
||||||
|
membershipRepo: membershipRepository,
|
||||||
|
redisClient: redisService,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import { OrganizationsTeamsBookingsModule } from "@/modules/organizations/teams/
|
|||||||
import { OrganizationsUsersBookingsModule } from "@/modules/organizations/users/bookings/organizations-users-bookings.module";
|
import { OrganizationsUsersBookingsModule } from "@/modules/organizations/users/bookings/organizations-users-bookings.module";
|
||||||
import { RouterModule } from "@/modules/router/router.module";
|
import { RouterModule } from "@/modules/router/router.module";
|
||||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||||
|
import { TeamsSchedulesModule } from "@/modules/teams/schedules/teams-schedules.module";
|
||||||
import { TimezoneModule } from "@/modules/timezones/timezones.module";
|
import { TimezoneModule } from "@/modules/timezones/timezones.module";
|
||||||
import { VerifiedResourcesModule } from "@/modules/verified-resources/verified-resources.module";
|
import { VerifiedResourcesModule } from "@/modules/verified-resources/verified-resources.module";
|
||||||
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
|
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
|
||||||
@@ -38,6 +39,7 @@ import { WebhooksModule } from "./webhooks/webhooks.module";
|
|||||||
OrganizationsRoutingFormsModule,
|
OrganizationsRoutingFormsModule,
|
||||||
VerifiedResourcesModule,
|
VerifiedResourcesModule,
|
||||||
RouterModule,
|
RouterModule,
|
||||||
|
TeamsSchedulesModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class EndpointsModule implements NestModule {
|
export class EndpointsModule implements NestModule {
|
||||||
|
|||||||
+551
@@ -0,0 +1,551 @@
|
|||||||
|
import { bootstrap } from "@/app";
|
||||||
|
import { AppModule } from "@/app.module";
|
||||||
|
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||||
|
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||||
|
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||||
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
|
import { OrganizationsTeamsBookingsModule } from "@/modules/organizations/teams/bookings/organizations-teams-bookings.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 { HostsRepositoryFixture } from "test/fixtures/repository/hosts.repository.fixture";
|
||||||
|
import { ManagedOrganizationsRepositoryFixture } from "test/fixtures/repository/managed-organizations.repository.fixture";
|
||||||
|
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||||
|
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.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 { randomString } from "@calcom/lib/random";
|
||||||
|
import {
|
||||||
|
CAL_API_VERSION_HEADER,
|
||||||
|
SUCCESS_STATUS,
|
||||||
|
VERSION_2024_08_13,
|
||||||
|
X_CAL_CLIENT_ID,
|
||||||
|
X_CAL_SECRET_KEY,
|
||||||
|
} from "@calcom/platform-constants";
|
||||||
|
import {
|
||||||
|
BookingOutput_2024_08_13,
|
||||||
|
RecurringBookingOutput_2024_08_13,
|
||||||
|
GetBookingsOutput_2024_08_13,
|
||||||
|
GetSeatedBookingOutput_2024_08_13,
|
||||||
|
} from "@calcom/platform-types";
|
||||||
|
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
||||||
|
|
||||||
|
describe("Organizations Bookings Endpoints 2024-08-13", () => {
|
||||||
|
describe("Manager and managed organizations bookings", () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
let managerOrganization: Team;
|
||||||
|
let managedOrganization: Team;
|
||||||
|
|
||||||
|
let userRepositoryFixture: UserRepositoryFixture;
|
||||||
|
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||||
|
let schedulesService: SchedulesService_2024_04_15;
|
||||||
|
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||||
|
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||||
|
let oAuthClient: PlatformOAuthClient;
|
||||||
|
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||||
|
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||||
|
let hostsRepositoryFixture: HostsRepositoryFixture;
|
||||||
|
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||||
|
let profileRepositoryFixture: ProfileRepositoryFixture;
|
||||||
|
let managedOrganizationsRepositoryFixture: ManagedOrganizationsRepositoryFixture;
|
||||||
|
|
||||||
|
const managerOrgUserEmail = "manager-org-user-1-bookings@api.com";
|
||||||
|
const managerOrgUserEmail2 = "manager-org-user-2-bookings@api.com";
|
||||||
|
const managedOrgUserEmail = "managed-org-user-1-bookings@api.com";
|
||||||
|
const nonOrgUserEmail1 = "non-org-user-1-bookings@api.com";
|
||||||
|
let managerOrgUser1: User;
|
||||||
|
let managerOrgUser2: User;
|
||||||
|
let managedOrgUser: User;
|
||||||
|
let nonOrgUser1: User;
|
||||||
|
let managerOrgTeam1: Team;
|
||||||
|
let managedOrgTeam1: Team;
|
||||||
|
|
||||||
|
let managerOrgEventTypeId: number;
|
||||||
|
let managedOrgEventTypeId: number;
|
||||||
|
let nonOrgEventTypeId: number;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const moduleRef = await withApiAuth(
|
||||||
|
managerOrgUserEmail,
|
||||||
|
Test.createTestingModule({
|
||||||
|
imports: [AppModule, OrganizationsTeamsBookingsModule],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.overrideGuard(PermissionsGuard)
|
||||||
|
.useValue({
|
||||||
|
canActivate: () => true,
|
||||||
|
})
|
||||||
|
.overrideGuard(PlatformPlanGuard)
|
||||||
|
.useValue({
|
||||||
|
canActivate: () => true,
|
||||||
|
})
|
||||||
|
.compile();
|
||||||
|
|
||||||
|
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||||
|
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||||
|
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||||
|
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||||
|
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||||
|
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||||
|
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||||
|
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||||
|
hostsRepositoryFixture = new HostsRepositoryFixture(moduleRef);
|
||||||
|
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||||
|
managedOrganizationsRepositoryFixture = new ManagedOrganizationsRepositoryFixture(moduleRef);
|
||||||
|
|
||||||
|
await setupManagerOrganization();
|
||||||
|
await setupManagedOrganization();
|
||||||
|
|
||||||
|
app = moduleRef.createNestApplication();
|
||||||
|
bootstrap(app as NestExpressApplication);
|
||||||
|
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function setupManagerOrganization() {
|
||||||
|
managerOrganization = await organizationsRepositoryFixture.create({
|
||||||
|
name: "organization bookings",
|
||||||
|
isPlatform: true,
|
||||||
|
});
|
||||||
|
managerOrgTeam1 = await teamRepositoryFixture.create({
|
||||||
|
name: "team orgs booking 1",
|
||||||
|
isOrganization: false,
|
||||||
|
parent: { connect: { id: managerOrganization.id } },
|
||||||
|
});
|
||||||
|
oAuthClient = await createOAuthClient(managerOrganization.id);
|
||||||
|
|
||||||
|
nonOrgUser1 = await userRepositoryFixture.create({
|
||||||
|
email: nonOrgUserEmail1,
|
||||||
|
locale: "it",
|
||||||
|
name: "NonOrgUser1Bookings",
|
||||||
|
platformOAuthClients: {
|
||||||
|
connect: {
|
||||||
|
id: oAuthClient.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
managerOrgUser1 = await userRepositoryFixture.create({
|
||||||
|
email: managerOrgUserEmail,
|
||||||
|
locale: "it",
|
||||||
|
name: "orgUser1Bookings",
|
||||||
|
platformOAuthClients: {
|
||||||
|
connect: {
|
||||||
|
id: oAuthClient.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
managerOrgUser2 = await userRepositoryFixture.create({
|
||||||
|
email: managerOrgUserEmail2,
|
||||||
|
locale: "it",
|
||||||
|
name: "orgUser2Bookings",
|
||||||
|
platformOAuthClients: {
|
||||||
|
connect: {
|
||||||
|
id: oAuthClient.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||||
|
name: "working time",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
isDefault: true,
|
||||||
|
};
|
||||||
|
await schedulesService.createUserSchedule(managerOrgUser1.id, userSchedule);
|
||||||
|
await schedulesService.createUserSchedule(nonOrgUser1.id, userSchedule);
|
||||||
|
await schedulesService.createUserSchedule(managerOrgUser2.id, userSchedule);
|
||||||
|
|
||||||
|
const orgEventType = await eventTypesRepositoryFixture.createTeamEventType({
|
||||||
|
schedulingType: "COLLECTIVE",
|
||||||
|
team: {
|
||||||
|
connect: { id: managerOrgTeam1.id },
|
||||||
|
},
|
||||||
|
title: "Collective Event Type",
|
||||||
|
slug: "manager-org-bookings-collective-event-type",
|
||||||
|
length: 60,
|
||||||
|
assignAllTeamMembers: true,
|
||||||
|
bookingFields: [],
|
||||||
|
locations: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
await profileRepositoryFixture.create({
|
||||||
|
uid: `usr-${managerOrgUser1.id}`,
|
||||||
|
username: managerOrgUserEmail,
|
||||||
|
organization: {
|
||||||
|
connect: {
|
||||||
|
id: managerOrganization.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: managerOrgUser1.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await profileRepositoryFixture.create({
|
||||||
|
uid: `usr-${managerOrgUser2.id}`,
|
||||||
|
username: managerOrgUserEmail2,
|
||||||
|
organization: {
|
||||||
|
connect: {
|
||||||
|
id: managerOrganization.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: managerOrgUser2.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await membershipsRepositoryFixture.create({
|
||||||
|
role: "OWNER",
|
||||||
|
user: { connect: { id: managerOrgUser1.id } },
|
||||||
|
team: { connect: { id: managerOrganization.id } },
|
||||||
|
accepted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await membershipsRepositoryFixture.create({
|
||||||
|
role: "MEMBER",
|
||||||
|
user: { connect: { id: managerOrgUser2.id } },
|
||||||
|
team: { connect: { id: managerOrganization.id } },
|
||||||
|
accepted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const nonOrgEventType = await eventTypesRepositoryFixture.create(
|
||||||
|
{
|
||||||
|
title: "Non Org Event Type",
|
||||||
|
slug: "non-org-event-type",
|
||||||
|
length: 60,
|
||||||
|
bookingFields: [],
|
||||||
|
locations: [],
|
||||||
|
},
|
||||||
|
nonOrgUser1.id
|
||||||
|
);
|
||||||
|
|
||||||
|
managerOrgEventTypeId = orgEventType.id;
|
||||||
|
nonOrgEventTypeId = nonOrgEventType.id;
|
||||||
|
|
||||||
|
await hostsRepositoryFixture.create({
|
||||||
|
isFixed: true,
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: managerOrgUser2.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
eventType: {
|
||||||
|
connect: {
|
||||||
|
id: managerOrgEventTypeId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await bookingsRepositoryFixture.create({
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: managerOrgUser2.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
startTime: new Date(Date.UTC(2030, 0, 9, 13, 0, 0)),
|
||||||
|
endTime: new Date(Date.UTC(2030, 0, 9, 14, 0, 0)),
|
||||||
|
title: "Manager Org Collective Booking",
|
||||||
|
uid: `manager-org-collective-${randomString()}`,
|
||||||
|
eventType: {
|
||||||
|
connect: {
|
||||||
|
id: managerOrgEventTypeId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
location: "https://meet.google.com/abc-def-ghi",
|
||||||
|
customInputs: {},
|
||||||
|
metadata: {},
|
||||||
|
responses: {
|
||||||
|
name: "alice",
|
||||||
|
email: "alice@gmail.com",
|
||||||
|
},
|
||||||
|
attendees: {
|
||||||
|
create: {
|
||||||
|
email: "alice@gmail.com",
|
||||||
|
name: "alice",
|
||||||
|
locale: "es",
|
||||||
|
timeZone: "Europe/Madrid",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await bookingsRepositoryFixture.create({
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: nonOrgUser1.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
startTime: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)),
|
||||||
|
endTime: new Date(Date.UTC(2030, 0, 8, 14, 0, 0)),
|
||||||
|
title: "Non-Org Booking 1",
|
||||||
|
uid: `non-org-booking-1-${randomString()}`,
|
||||||
|
eventType: {
|
||||||
|
connect: {
|
||||||
|
id: nonOrgEventTypeId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
location: "https://meet.google.com/jkl-mno-pqr",
|
||||||
|
customInputs: {},
|
||||||
|
metadata: {},
|
||||||
|
responses: {
|
||||||
|
name: managerOrgUser2.name ?? "",
|
||||||
|
email: managerOrgUserEmail2,
|
||||||
|
},
|
||||||
|
attendees: {
|
||||||
|
create: {
|
||||||
|
email: managerOrgUserEmail2,
|
||||||
|
name: managerOrgUser2.name ?? "",
|
||||||
|
locale: "en",
|
||||||
|
timeZone: managerOrgUser2.timeZone ?? "Europe/Madrid",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setupManagedOrganization() {
|
||||||
|
managedOrganization = await organizationsRepositoryFixture.create({
|
||||||
|
name: "organization bookings",
|
||||||
|
isPlatform: true,
|
||||||
|
});
|
||||||
|
await managedOrganizationsRepositoryFixture.createManagedOrganization(
|
||||||
|
managerOrganization.id,
|
||||||
|
managedOrganization.id
|
||||||
|
);
|
||||||
|
|
||||||
|
managedOrgTeam1 = await teamRepositoryFixture.create({
|
||||||
|
name: "managed org team 1",
|
||||||
|
isOrganization: false,
|
||||||
|
parent: { connect: { id: managedOrganization.id } },
|
||||||
|
});
|
||||||
|
|
||||||
|
managedOrgUser = await userRepositoryFixture.create({
|
||||||
|
email: managedOrgUserEmail,
|
||||||
|
locale: "it",
|
||||||
|
name: "ManagedOrgUser1Bookings",
|
||||||
|
platformOAuthClients: {
|
||||||
|
connect: {
|
||||||
|
id: oAuthClient.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await profileRepositoryFixture.create({
|
||||||
|
uid: `usr-${managedOrgUser.id}`,
|
||||||
|
username: managedOrgUserEmail,
|
||||||
|
organization: {
|
||||||
|
connect: {
|
||||||
|
id: managedOrganization.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: managedOrgUser.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await profileRepositoryFixture.create({
|
||||||
|
uid: `usr-${managerOrgUser1.id}`,
|
||||||
|
username: managerOrgUserEmail,
|
||||||
|
organization: {
|
||||||
|
connect: {
|
||||||
|
id: managedOrganization.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: managerOrgUser1.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await membershipsRepositoryFixture.create({
|
||||||
|
role: "MEMBER",
|
||||||
|
user: { connect: { id: managedOrgUser.id } },
|
||||||
|
team: { connect: { id: managedOrganization.id } },
|
||||||
|
accepted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await membershipsRepositoryFixture.create({
|
||||||
|
role: "OWNER",
|
||||||
|
user: { connect: { id: managerOrgUser1.id } },
|
||||||
|
team: { connect: { id: managedOrganization.id } },
|
||||||
|
accepted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||||
|
name: "working time",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
isDefault: true,
|
||||||
|
};
|
||||||
|
await schedulesService.createUserSchedule(managedOrgUser.id, userSchedule);
|
||||||
|
|
||||||
|
const managedOrgEventType = await eventTypesRepositoryFixture.createTeamEventType({
|
||||||
|
schedulingType: "COLLECTIVE",
|
||||||
|
team: {
|
||||||
|
connect: { id: managedOrgTeam1.id },
|
||||||
|
},
|
||||||
|
title: "Collective Event Type",
|
||||||
|
slug: "managed-org-bookings-collective-event-type",
|
||||||
|
length: 60,
|
||||||
|
assignAllTeamMembers: true,
|
||||||
|
bookingFields: [],
|
||||||
|
locations: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
managedOrgEventTypeId = managedOrgEventType.id;
|
||||||
|
|
||||||
|
await hostsRepositoryFixture.create({
|
||||||
|
isFixed: true,
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: managedOrgUser.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
eventType: {
|
||||||
|
connect: {
|
||||||
|
id: managedOrgEventType.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await bookingsRepositoryFixture.create({
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: managedOrgUser.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
startTime: new Date(Date.UTC(2030, 0, 11, 13, 0, 0)),
|
||||||
|
endTime: new Date(Date.UTC(2030, 0, 11, 14, 0, 0)),
|
||||||
|
title: "Managed Org Booking",
|
||||||
|
uid: `managed-org-booking-${randomString()}`,
|
||||||
|
eventType: {
|
||||||
|
connect: {
|
||||||
|
id: managedOrgEventTypeId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
location: "https://meet.google.com/ghi-jkl-mno",
|
||||||
|
customInputs: {},
|
||||||
|
metadata: {},
|
||||||
|
responses: {
|
||||||
|
name: "charlie",
|
||||||
|
email: "charlie@gmail.com",
|
||||||
|
},
|
||||||
|
attendees: {
|
||||||
|
create: {
|
||||||
|
email: "charlie@gmail.com",
|
||||||
|
name: "charlie",
|
||||||
|
locale: "en",
|
||||||
|
timeZone: "Europe/Madrid",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("get manager organization bookings", () => {
|
||||||
|
it("should get bookings by organizationId for manager organization", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/organizations/${managerOrganization.id}/bookings`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.set(X_CAL_CLIENT_ID, oAuthClient.id)
|
||||||
|
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
|
||||||
|
.expect(200)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
const data: (
|
||||||
|
| BookingOutput_2024_08_13
|
||||||
|
| RecurringBookingOutput_2024_08_13
|
||||||
|
| GetSeatedBookingOutput_2024_08_13
|
||||||
|
)[] = responseBody.data;
|
||||||
|
expect(data.length).toEqual(2);
|
||||||
|
expect(data.map((booking) => booking.eventTypeId).sort()).toEqual(
|
||||||
|
[managerOrgEventTypeId, nonOrgEventTypeId].sort()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should get bookings by organizationId and userId", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/organizations/${managerOrganization.id}/bookings?userIds=${managerOrgUser2.id}`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.expect(200)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
const data: (
|
||||||
|
| BookingOutput_2024_08_13
|
||||||
|
| RecurringBookingOutput_2024_08_13
|
||||||
|
| GetSeatedBookingOutput_2024_08_13
|
||||||
|
)[] = responseBody.data;
|
||||||
|
expect(data.length).toEqual(2);
|
||||||
|
expect(data.map((booking) => booking.eventTypeId).sort()).toEqual(
|
||||||
|
[managerOrgEventTypeId, nonOrgEventTypeId].sort()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("get managed organization bookings", () => {
|
||||||
|
it("should get bookings by organizationId for managed organization", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/organizations/${managedOrganization.id}/bookings`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.set(X_CAL_CLIENT_ID, oAuthClient.id)
|
||||||
|
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
|
||||||
|
.expect(200)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
const data: (
|
||||||
|
| BookingOutput_2024_08_13
|
||||||
|
| RecurringBookingOutput_2024_08_13
|
||||||
|
| GetSeatedBookingOutput_2024_08_13
|
||||||
|
)[] = responseBody.data;
|
||||||
|
expect(data.length).toEqual(1);
|
||||||
|
expect(data[0].eventTypeId).toEqual(managedOrgEventTypeId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function createOAuthClient(organizationId: number) {
|
||||||
|
const data = {
|
||||||
|
logo: "logo-url",
|
||||||
|
name: "name",
|
||||||
|
redirectUris: ["http://localhost:5555"],
|
||||||
|
permissions: 32,
|
||||||
|
areCalendarEventsEnabled: false,
|
||||||
|
areEmailsEnabled: false,
|
||||||
|
};
|
||||||
|
const secret = "secret";
|
||||||
|
|
||||||
|
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||||
|
await teamRepositoryFixture.delete(managerOrganization.id);
|
||||||
|
await userRepositoryFixture.deleteByEmail(managerOrgUser1.email);
|
||||||
|
await userRepositoryFixture.deleteByEmail(nonOrgUser1.email);
|
||||||
|
await bookingsRepositoryFixture.deleteAllBookings(managerOrgUser1.id, managerOrgUser1.email);
|
||||||
|
await bookingsRepositoryFixture.deleteAllBookings(nonOrgUser1.id, nonOrgUser1.email);
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
+128
-1
@@ -66,6 +66,8 @@ describe("Organizations Bookings Endpoints 2024-08-13", () => {
|
|||||||
let orgEventTypeId2: number;
|
let orgEventTypeId2: number;
|
||||||
let nonOrgEventTypeId: number;
|
let nonOrgEventTypeId: number;
|
||||||
|
|
||||||
|
let collectiveOrgBookingUid: string;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const moduleRef = await withApiAuth(
|
const moduleRef = await withApiAuth(
|
||||||
orgUserEmail2,
|
orgUserEmail2,
|
||||||
@@ -91,12 +93,17 @@ describe("Organizations Bookings Endpoints 2024-08-13", () => {
|
|||||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||||
|
|
||||||
organization = await organizationsRepositoryFixture.create({ name: "organization bookings" });
|
organization = await organizationsRepositoryFixture.create({ name: "organization bookings" });
|
||||||
|
oAuthClient = await createOAuthClient(organization.id);
|
||||||
team1 = await teamRepositoryFixture.create({
|
team1 = await teamRepositoryFixture.create({
|
||||||
name: "team orgs booking 1",
|
name: "team orgs booking 1",
|
||||||
isOrganization: false,
|
isOrganization: false,
|
||||||
parent: { connect: { id: organization.id } },
|
parent: { connect: { id: organization.id } },
|
||||||
|
createdByOAuthClient: {
|
||||||
|
connect: {
|
||||||
|
id: oAuthClient.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
oAuthClient = await createOAuthClient(organization.id);
|
|
||||||
|
|
||||||
nonOrgUser1 = await userRepositoryFixture.create({
|
nonOrgUser1 = await userRepositoryFixture.create({
|
||||||
email: nonOrgUserEmail1,
|
email: nonOrgUserEmail1,
|
||||||
@@ -315,6 +322,7 @@ describe("Organizations Bookings Endpoints 2024-08-13", () => {
|
|||||||
|
|
||||||
if (responseDataIsBooking(responseBody.data)) {
|
if (responseDataIsBooking(responseBody.data)) {
|
||||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||||
|
collectiveOrgBookingUid = data.uid;
|
||||||
expect(data.id).toBeDefined();
|
expect(data.id).toBeDefined();
|
||||||
expect(data.uid).toBeDefined();
|
expect(data.uid).toBeDefined();
|
||||||
expect(data.hosts.length).toEqual(1);
|
expect(data.hosts.length).toEqual(1);
|
||||||
@@ -669,6 +677,125 @@ describe("Organizations Bookings Endpoints 2024-08-13", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("get by bookingUid param", () => {
|
||||||
|
it("should get a specific booking by bookingUid query param", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/organizations/${organization.id}/bookings?bookingUid=${collectiveOrgBookingUid}`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.set(X_CAL_CLIENT_ID, oAuthClient.id)
|
||||||
|
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
|
||||||
|
.expect(200)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
|
||||||
|
const data: (
|
||||||
|
| BookingOutput_2024_08_13
|
||||||
|
| RecurringBookingOutput_2024_08_13
|
||||||
|
| GetSeatedBookingOutput_2024_08_13
|
||||||
|
)[] = responseBody.data;
|
||||||
|
expect(data.length).toEqual(1);
|
||||||
|
expect(responseDataIsBooking(data[0])).toBe(true);
|
||||||
|
|
||||||
|
if (responseDataIsBooking(data[0])) {
|
||||||
|
const booking: BookingOutput_2024_08_13 = data[0];
|
||||||
|
expect(booking.uid).toEqual(collectiveOrgBookingUid);
|
||||||
|
expect(booking.eventTypeId).toEqual(orgEventTypeId);
|
||||||
|
expect(booking.status).toEqual("accepted");
|
||||||
|
expect(booking.attendees.length).toEqual(2);
|
||||||
|
expect(booking.attendees[0].name).toEqual("alice");
|
||||||
|
expect(booking.attendees[0].email).toEqual("alice@gmail.com");
|
||||||
|
} else {
|
||||||
|
throw new Error("Expected single booking but received different response type");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty array for non-existent booking UID query param", async () => {
|
||||||
|
const nonExistentUid = "non-existent-booking-uid";
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/organizations/${organization.id}/bookings?bookingUid=${nonExistentUid}`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.set(X_CAL_CLIENT_ID, oAuthClient.id)
|
||||||
|
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
|
||||||
|
.expect(200)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(responseBody.data.length).toEqual(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty array for booking UID not belonging to the organization", async () => {
|
||||||
|
const regularUser = await userRepositoryFixture.create({
|
||||||
|
email: `org-bookings-regular-user-${Math.floor(Math.random() * 10000)}@api.com`,
|
||||||
|
name: "Regular User",
|
||||||
|
});
|
||||||
|
|
||||||
|
const regularUserEventType = await eventTypesRepositoryFixture.create(
|
||||||
|
{
|
||||||
|
title: `regular-user-event-type-${Math.floor(Math.random() * 10000)}`,
|
||||||
|
slug: `regular-user-event-type-${Math.floor(Math.random() * 10000)}`,
|
||||||
|
length: 60,
|
||||||
|
bookingFields: [],
|
||||||
|
locations: [],
|
||||||
|
},
|
||||||
|
regularUser.id
|
||||||
|
);
|
||||||
|
|
||||||
|
const regularUserBooking = await bookingsRepositoryFixture.create({
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: regularUser.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
startTime: new Date(Date.UTC(2030, 0, 15, 13, 0, 0)),
|
||||||
|
endTime: new Date(Date.UTC(2030, 0, 15, 14, 0, 0)),
|
||||||
|
title: "Regular user booking",
|
||||||
|
uid: `regular-user-booking-${Math.floor(Math.random() * 10000)}`,
|
||||||
|
eventType: {
|
||||||
|
connect: {
|
||||||
|
id: regularUserEventType.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
location: "https://meet.google.com/regular-user",
|
||||||
|
customInputs: {},
|
||||||
|
metadata: {},
|
||||||
|
status: "ACCEPTED",
|
||||||
|
responses: {
|
||||||
|
name: "Regular Attendee",
|
||||||
|
email: "regular@example.com",
|
||||||
|
},
|
||||||
|
attendees: {
|
||||||
|
create: {
|
||||||
|
email: "regular@example.com",
|
||||||
|
name: "Regular Attendee",
|
||||||
|
locale: "en",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/organizations/${organization.id}/bookings?bookingUid=${regularUserBooking.uid}`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.set(X_CAL_CLIENT_ID, oAuthClient.id)
|
||||||
|
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
|
||||||
|
.expect(200)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(responseBody.data.length).toEqual(0);
|
||||||
|
|
||||||
|
await userRepositoryFixture.delete(regularUser.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
async function createOAuthClient(organizationId: number) {
|
async function createOAuthClient(organizationId: number) {
|
||||||
|
|||||||
+183
@@ -0,0 +1,183 @@
|
|||||||
|
import { bootstrap } from "@/app";
|
||||||
|
import { AppModule } from "@/app.module";
|
||||||
|
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
||||||
|
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
||||||
|
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 { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||||
|
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 * as request from "supertest";
|
||||||
|
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||||
|
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||||
|
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||||
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
|
import { randomString } from "test/utils/randomString";
|
||||||
|
import { withApiAuth } from "test/utils/withApiAuth";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import { CreatePrivateLinkInput } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
describe("Organizations / Teams / Event Types / Private Links Endpoints", () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
|
||||||
|
let orgFixture: OrganizationRepositoryFixture;
|
||||||
|
let teamFixture: TeamRepositoryFixture;
|
||||||
|
let userFixture: UserRepositoryFixture;
|
||||||
|
let eventTypesFixture: EventTypesRepositoryFixture;
|
||||||
|
|
||||||
|
let org: any;
|
||||||
|
let team: any;
|
||||||
|
let user: any;
|
||||||
|
let eventType: any;
|
||||||
|
|
||||||
|
const userEmail = `org-private-links-user-${randomString()}@api.com`;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const testingModuleBuilder = withApiAuth(
|
||||||
|
userEmail,
|
||||||
|
Test.createTestingModule({
|
||||||
|
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||||
|
imports: [AppModule, UsersModule, TokensModule],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
// Bypass org admin plan and admin API checks and roles in this e2e
|
||||||
|
.overrideGuard(PlatformPlanGuard)
|
||||||
|
.useValue({ canActivate: () => true })
|
||||||
|
.overrideGuard(IsAdminAPIEnabledGuard)
|
||||||
|
.useValue({ canActivate: () => true })
|
||||||
|
.overrideGuard(RolesGuard)
|
||||||
|
.useValue({ canActivate: () => true })
|
||||||
|
// Keep IsOrgGuard and IsTeamInOrg to validate org/team path integrity
|
||||||
|
.overrideGuard(ApiAuthGuard)
|
||||||
|
.useValue({ canActivate: () => true });
|
||||||
|
|
||||||
|
const moduleRef = await testingModuleBuilder.compile();
|
||||||
|
|
||||||
|
app = moduleRef.createNestApplication();
|
||||||
|
bootstrap(app as NestExpressApplication);
|
||||||
|
|
||||||
|
orgFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||||
|
teamFixture = new TeamRepositoryFixture(moduleRef);
|
||||||
|
userFixture = new UserRepositoryFixture(moduleRef);
|
||||||
|
eventTypesFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||||
|
|
||||||
|
user = await userFixture.create({
|
||||||
|
email: userEmail,
|
||||||
|
username: `org-private-links-user-${randomString()}`,
|
||||||
|
name: "Test User",
|
||||||
|
});
|
||||||
|
|
||||||
|
org = await orgFixture.create({
|
||||||
|
name: `org-private-links-org-${randomString()}`,
|
||||||
|
slug: `org-private-links-org-${randomString()}`,
|
||||||
|
isOrganization: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
team = await teamFixture.create({
|
||||||
|
name: `org-private-links-team-${randomString()}`,
|
||||||
|
isOrganization: false,
|
||||||
|
parent: { connect: { id: org.id } },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a team-owned event type
|
||||||
|
eventType = await eventTypesFixture.createTeamEventType({
|
||||||
|
title: `org-private-links-event-type-${randomString()}`,
|
||||||
|
slug: `org-private-links-event-type-${randomString()}`,
|
||||||
|
length: 30,
|
||||||
|
locations: [],
|
||||||
|
team: { connect: { id: team.id } },
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("POST /v2/organizations/:orgId/teams/:teamId/event-types/:eventTypeId/private-links - create", async () => {
|
||||||
|
const body: CreatePrivateLinkInput = { maxUsageCount: 5 };
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${eventType.id}/private-links`)
|
||||||
|
.set("Authorization", "Bearer test")
|
||||||
|
.send(body)
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
expect(response.body.status).toBe(SUCCESS_STATUS);
|
||||||
|
expect(response.body.data.linkId).toBeDefined();
|
||||||
|
expect(response.body.data.maxUsageCount).toBe(5);
|
||||||
|
expect(response.body.data.usageCount).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("GET /v2/organizations/:orgId/teams/:teamId/event-types/:eventTypeId/private-links - list", async () => {
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.get(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${eventType.id}/private-links`)
|
||||||
|
.set("Authorization", "Bearer test")
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body.status).toBe(SUCCESS_STATUS);
|
||||||
|
expect(Array.isArray(response.body.data)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("PATCH /v2/organizations/:orgId/teams/:teamId/event-types/:eventTypeId/private-links/:linkId - update", async () => {
|
||||||
|
// create first
|
||||||
|
const createResp = await request(app.getHttpServer())
|
||||||
|
.post(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${eventType.id}/private-links`)
|
||||||
|
.set("Authorization", "Bearer test")
|
||||||
|
.send({ maxUsageCount: 3 })
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
const linkId = createResp.body.data.linkId as string;
|
||||||
|
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.patch(
|
||||||
|
`/v2/organizations/${org.id}/teams/${team.id}/event-types/${eventType.id}/private-links/${linkId}`
|
||||||
|
)
|
||||||
|
.set("Authorization", "Bearer test")
|
||||||
|
.send({ maxUsageCount: 10 })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body.status).toBe(SUCCESS_STATUS);
|
||||||
|
expect(response.body.data.maxUsageCount).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("DELETE /v2/organizations/:orgId/teams/:teamId/event-types/:eventTypeId/private-links/:linkId - delete", async () => {
|
||||||
|
// create first
|
||||||
|
const createResp = await request(app.getHttpServer())
|
||||||
|
.post(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${eventType.id}/private-links`)
|
||||||
|
.set("Authorization", "Bearer test")
|
||||||
|
.send({ maxUsageCount: 2 })
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
const linkId = createResp.body.data.linkId as string;
|
||||||
|
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.delete(
|
||||||
|
`/v2/organizations/${org.id}/teams/${team.id}/event-types/${eventType.id}/private-links/${linkId}`
|
||||||
|
)
|
||||||
|
.set("Authorization", "Bearer test")
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body.status).toBe(SUCCESS_STATUS);
|
||||||
|
expect(response.body.data.linkId).toBe(linkId);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
try {
|
||||||
|
if (eventType?.id) {
|
||||||
|
await eventTypesFixture.delete(eventType.id);
|
||||||
|
}
|
||||||
|
if (team?.id) {
|
||||||
|
await teamFixture.delete(team.id);
|
||||||
|
}
|
||||||
|
if (org?.id) {
|
||||||
|
await orgFixture.delete(org.id);
|
||||||
|
}
|
||||||
|
if (user?.email) {
|
||||||
|
await userFixture.deleteByEmail(user.email);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -74,6 +74,7 @@ type Input = Pick<
|
|||||||
| "hideOrganizerEmail"
|
| "hideOrganizerEmail"
|
||||||
| "team"
|
| "team"
|
||||||
| "calVideoSettings"
|
| "calVideoSettings"
|
||||||
|
| "hidden"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|||||||
+17
-1
@@ -2,6 +2,8 @@ import { CreateOrgMembershipDto } from "@/modules/organizations/memberships/inpu
|
|||||||
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
|
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
|
||||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { TeamService } from "@calcom/platform-libraries";
|
||||||
|
|
||||||
import { UpdateOrgMembershipDto } from "../inputs/update-organization-membership.input";
|
import { UpdateOrgMembershipDto } from "../inputs/update-organization-membership.input";
|
||||||
import { OrganizationsMembershipOutputService } from "./organizations-membership-output.service";
|
import { OrganizationsMembershipOutputService } from "./organizations-membership-output.service";
|
||||||
|
|
||||||
@@ -62,10 +64,24 @@ export class OrganizationsMembershipService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteOrgMembership(organizationId: number, membershipId: number) {
|
async deleteOrgMembership(organizationId: number, membershipId: number) {
|
||||||
const membership = await this.organizationsMembershipRepository.deleteOrgMembership(
|
// Get the membership first to get the userId
|
||||||
|
const membership = await this.organizationsMembershipRepository.findOrgMembership(
|
||||||
organizationId,
|
organizationId,
|
||||||
membershipId
|
membershipId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!membership) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
`Membership with id ${membershipId} within organization id ${organizationId} not found`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await TeamService.removeMembers({
|
||||||
|
teamIds: [organizationId],
|
||||||
|
userIds: [membership.userId],
|
||||||
|
isOrg: true,
|
||||||
|
});
|
||||||
|
|
||||||
return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership);
|
return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
|
import { OrganizationsEventTypesPrivateLinksController } from "@/ee/event-types-private-links/controllers/organizations-event-types-private-links.controller";
|
||||||
|
import { EventTypesPrivateLinksModule } from "@/ee/event-types-private-links/event-types-private-links.module";
|
||||||
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||||
import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.module";
|
import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.module";
|
||||||
|
import { InputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/input-schedules.service";
|
||||||
|
import { SchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/schedules.service";
|
||||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||||
import { ConferencingRepository } from "@/modules/conferencing/repositories/conferencing.repository";
|
import { ConferencingRepository } from "@/modules/conferencing/repositories/conferencing.repository";
|
||||||
import { ConferencingService } from "@/modules/conferencing/services/conferencing.service";
|
import { ConferencingService } from "@/modules/conferencing/services/conferencing.service";
|
||||||
@@ -18,7 +22,6 @@ import { OrganizationAttributesService } from "@/modules/organizations/attribute
|
|||||||
import { OrganizationAttributeOptionRepository } from "@/modules/organizations/attributes/options/organization-attribute-options.repository";
|
import { OrganizationAttributeOptionRepository } from "@/modules/organizations/attributes/options/organization-attribute-options.repository";
|
||||||
import { OrganizationsAttributesOptionsController } from "@/modules/organizations/attributes/options/organizations-attributes-options.controller";
|
import { OrganizationsAttributesOptionsController } from "@/modules/organizations/attributes/options/organizations-attributes-options.controller";
|
||||||
import { OrganizationAttributeOptionService } from "@/modules/organizations/attributes/options/services/organization-attributes-option.service";
|
import { OrganizationAttributeOptionService } from "@/modules/organizations/attributes/options/services/organization-attributes-option.service";
|
||||||
import { OrganizationsConferencingController } from "@/modules/organizations/conferencing/organizations-conferencing.controller";
|
|
||||||
import { OrganizationsConferencingModule } from "@/modules/organizations/conferencing/organizations-conferencing.module";
|
import { OrganizationsConferencingModule } from "@/modules/organizations/conferencing/organizations-conferencing.module";
|
||||||
import { OrganizationsConferencingService } from "@/modules/organizations/conferencing/services/organizations-conferencing.service";
|
import { OrganizationsConferencingService } from "@/modules/organizations/conferencing/services/organizations-conferencing.service";
|
||||||
import { OrganizationsDelegationCredentialModule } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.module";
|
import { OrganizationsDelegationCredentialModule } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.module";
|
||||||
@@ -36,7 +39,6 @@ import { OrganizationsMembershipOutputService } from "@/modules/organizations/me
|
|||||||
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
|
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
|
||||||
import { OrganizationsOrganizationsModule } from "@/modules/organizations/organizations/organizations-organizations.module";
|
import { OrganizationsOrganizationsModule } from "@/modules/organizations/organizations/organizations-organizations.module";
|
||||||
import { OrganizationsSchedulesController } from "@/modules/organizations/schedules/organizations-schedules.controller";
|
import { OrganizationsSchedulesController } from "@/modules/organizations/schedules/organizations-schedules.controller";
|
||||||
import { OrganizationSchedulesRepository } from "@/modules/organizations/schedules/organizations-schedules.repository";
|
|
||||||
import { OrganizationsSchedulesService } from "@/modules/organizations/schedules/services/organizations-schedules.service";
|
import { OrganizationsSchedulesService } from "@/modules/organizations/schedules/services/organizations-schedules.service";
|
||||||
import { OrganizationsStripeModule } from "@/modules/organizations/stripe/organizations-stripe.module";
|
import { OrganizationsStripeModule } from "@/modules/organizations/stripe/organizations-stripe.module";
|
||||||
import { OrganizationsStripeService } from "@/modules/organizations/stripe/services/organizations-stripe.service";
|
import { OrganizationsStripeService } from "@/modules/organizations/stripe/services/organizations-stripe.service";
|
||||||
@@ -63,6 +65,7 @@ import { RedisModule } from "@/modules/redis/redis.module";
|
|||||||
import { RedisService } from "@/modules/redis/redis.service";
|
import { RedisService } from "@/modules/redis/redis.service";
|
||||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||||
import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module";
|
import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module";
|
||||||
|
import { TeamsSchedulesService } from "@/modules/teams/schedules/services/teams-schedules.service";
|
||||||
import { TeamsModule } from "@/modules/teams/teams/teams.module";
|
import { TeamsModule } from "@/modules/teams/teams/teams.module";
|
||||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||||
import { UsersModule } from "@/modules/users/users.module";
|
import { UsersModule } from "@/modules/users/users.module";
|
||||||
@@ -91,6 +94,7 @@ import { Module } from "@nestjs/common";
|
|||||||
OrganizationsStripeModule,
|
OrganizationsStripeModule,
|
||||||
OrganizationsTeamsRoutingFormsModule,
|
OrganizationsTeamsRoutingFormsModule,
|
||||||
OrganizationsConferencingModule,
|
OrganizationsConferencingModule,
|
||||||
|
EventTypesPrivateLinksModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
OrganizationsRepository,
|
OrganizationsRepository,
|
||||||
@@ -99,7 +103,6 @@ import { Module } from "@nestjs/common";
|
|||||||
OrganizationsTeamsService,
|
OrganizationsTeamsService,
|
||||||
MembershipsRepository,
|
MembershipsRepository,
|
||||||
OrganizationsSchedulesService,
|
OrganizationsSchedulesService,
|
||||||
OrganizationSchedulesRepository,
|
|
||||||
OrganizationsUsersRepository,
|
OrganizationsUsersRepository,
|
||||||
OrganizationsUsersService,
|
OrganizationsUsersService,
|
||||||
EmailService,
|
EmailService,
|
||||||
@@ -141,6 +144,9 @@ import { Module } from "@nestjs/common";
|
|||||||
TeamWorkflowsService,
|
TeamWorkflowsService,
|
||||||
WorkflowsInputService,
|
WorkflowsInputService,
|
||||||
WorkflowsOutputService,
|
WorkflowsOutputService,
|
||||||
|
TeamsSchedulesService,
|
||||||
|
SchedulesService_2024_06_11,
|
||||||
|
InputSchedulesService_2024_06_11,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
OrganizationsService,
|
OrganizationsService,
|
||||||
@@ -177,6 +183,7 @@ import { Module } from "@nestjs/common";
|
|||||||
OrganizationsTeamsSchedulesController,
|
OrganizationsTeamsSchedulesController,
|
||||||
OrganizationsUsersOOOController,
|
OrganizationsUsersOOOController,
|
||||||
OrganizationTeamWorkflowsController,
|
OrganizationTeamWorkflowsController,
|
||||||
|
OrganizationsEventTypesPrivateLinksController,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class OrganizationsModule {}
|
export class OrganizationsModule {}
|
||||||
|
|||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
|
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
|
||||||
import { OutputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/output-schedules.service";
|
import { OutputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/output-schedules.service";
|
||||||
import { OrganizationSchedulesRepository } from "@/modules/organizations/schedules/organizations-schedules.repository";
|
|
||||||
import { UsersRepository } from "@/modules/users/users.repository";
|
import { UsersRepository } from "@/modules/users/users.repository";
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
@@ -8,8 +8,8 @@ import { ScheduleOutput_2024_06_11 } from "@calcom/platform-types";
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class OrganizationsSchedulesService {
|
export class OrganizationsSchedulesService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly organizationSchedulesService: OrganizationSchedulesRepository,
|
|
||||||
private readonly outputSchedulesService: OutputSchedulesService_2024_06_11,
|
private readonly outputSchedulesService: OutputSchedulesService_2024_06_11,
|
||||||
|
private readonly schedulesRepository: SchedulesRepository_2024_06_11,
|
||||||
private readonly usersRepository: UsersRepository
|
private readonly usersRepository: UsersRepository
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ export class OrganizationsSchedulesService {
|
|||||||
const users = await this.usersRepository.getOrganizationUsers(organizationId);
|
const users = await this.usersRepository.getOrganizationUsers(organizationId);
|
||||||
const usersIds = users.map((user) => user.id);
|
const usersIds = users.map((user) => user.id);
|
||||||
|
|
||||||
const schedules = await this.organizationSchedulesService.getSchedulesByUserIds(usersIds, skip, take);
|
const schedules = await this.schedulesRepository.getSchedulesByUserIds(usersIds, skip, take);
|
||||||
|
|
||||||
const responseSchedules: ScheduleOutput_2024_06_11[] = [];
|
const responseSchedules: ScheduleOutput_2024_06_11[] = [];
|
||||||
|
|
||||||
|
|||||||
-4
@@ -20,7 +20,6 @@ import {
|
|||||||
OrgTeamMembershipOutputResponseDto,
|
OrgTeamMembershipOutputResponseDto,
|
||||||
} from "@/modules/organizations/teams/memberships/outputs/organization-teams-memberships.output";
|
} from "@/modules/organizations/teams/memberships/outputs/organization-teams-memberships.output";
|
||||||
import { OrganizationsTeamsMembershipsService } from "@/modules/organizations/teams/memberships/services/organizations-teams-memberships.service";
|
import { OrganizationsTeamsMembershipsService } from "@/modules/organizations/teams/memberships/services/organizations-teams-memberships.service";
|
||||||
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
|
|
||||||
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
|
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
@@ -59,7 +58,6 @@ export class OrganizationsTeamsMembershipsController {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private organizationsTeamsMembershipsService: OrganizationsTeamsMembershipsService,
|
private organizationsTeamsMembershipsService: OrganizationsTeamsMembershipsService,
|
||||||
private teamsEventTypesService: TeamsEventTypesService,
|
|
||||||
private readonly organizationsRepository: OrganizationsRepository
|
private readonly organizationsRepository: OrganizationsRepository
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -127,8 +125,6 @@ export class OrganizationsTeamsMembershipsController {
|
|||||||
membershipId
|
membershipId
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.teamsEventTypesService.deleteUserTeamEventTypesAndHosts(membership.userId, teamId);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
|
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
|
||||||
|
|||||||
+13
-1
@@ -3,6 +3,8 @@ import { UpdateOrgTeamMembershipDto } from "@/modules/organizations/teams/member
|
|||||||
import { OrganizationsTeamsMembershipsRepository } from "@/modules/organizations/teams/memberships/organizations-teams-memberships.repository";
|
import { OrganizationsTeamsMembershipsRepository } from "@/modules/organizations/teams/memberships/organizations-teams-memberships.repository";
|
||||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { TeamService } from "@calcom/platform-libraries";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OrganizationsTeamsMembershipsService {
|
export class OrganizationsTeamsMembershipsService {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -58,11 +60,21 @@ export class OrganizationsTeamsMembershipsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteOrgTeamMembership(organizationId: number, teamId: number, membershipId: number) {
|
async deleteOrgTeamMembership(organizationId: number, teamId: number, membershipId: number) {
|
||||||
const teamMembership = await this.organizationsTeamsMembershipsRepository.deleteOrgTeamMembershipById(
|
// First get the membership to get the userId
|
||||||
|
const teamMembership = await this.organizationsTeamsMembershipsRepository.findOrgTeamMembership(
|
||||||
organizationId,
|
organizationId,
|
||||||
teamId,
|
teamId,
|
||||||
membershipId
|
membershipId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!teamMembership) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
`Membership with id ${membershipId} not found in team ${teamId} of organization ${organizationId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await TeamService.removeMembers({ teamIds: [teamId], userIds: [teamMembership.userId], isOrg: false });
|
||||||
|
|
||||||
return teamMembership;
|
return teamMembership;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-3
@@ -14,11 +14,12 @@ import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
|||||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||||
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
|
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
|
||||||
import { IsUserInOrgTeam } from "@/modules/auth/guards/users/is-user-in-org-team.guard";
|
import { IsUserInOrgTeam } from "@/modules/auth/guards/users/is-user-in-org-team.guard";
|
||||||
import { Controller, UseGuards, Get, Param, ParseIntPipe } from "@nestjs/common";
|
import { TeamsSchedulesService } from "@/modules/teams/schedules/services/teams-schedules.service";
|
||||||
|
import { Controller, UseGuards, Get, Param, ParseIntPipe, Query } from "@nestjs/common";
|
||||||
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
|
|
||||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
import { GetSchedulesOutput_2024_06_11 } from "@calcom/platform-types";
|
import { GetSchedulesOutput_2024_06_11, SkipTakePagination } from "@calcom/platform-types";
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "/v2/organizations/:orgId/teams/:teamId",
|
path: "/v2/organizations/:orgId/teams/:teamId",
|
||||||
@@ -29,7 +30,31 @@ import { GetSchedulesOutput_2024_06_11 } from "@calcom/platform-types";
|
|||||||
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
|
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
|
||||||
@ApiHeader(OPTIONAL_API_KEY_HEADER)
|
@ApiHeader(OPTIONAL_API_KEY_HEADER)
|
||||||
export class OrganizationsTeamsSchedulesController {
|
export class OrganizationsTeamsSchedulesController {
|
||||||
constructor(private schedulesService: SchedulesService_2024_06_11) {}
|
constructor(
|
||||||
|
private schedulesService: SchedulesService_2024_06_11,
|
||||||
|
|
||||||
|
private teamsSchedulesService: TeamsSchedulesService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@UseGuards(IsTeamInOrg)
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@Get("/schedules")
|
||||||
|
@DocsTags("Orgs / Teams / Schedules")
|
||||||
|
@ApiOperation({ summary: "Get all team member schedules" })
|
||||||
|
async getTeamSchedules(
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number,
|
||||||
|
@Query() queryParams: SkipTakePagination
|
||||||
|
): Promise<GetSchedulesOutput_2024_06_11> {
|
||||||
|
const { skip, take } = queryParams;
|
||||||
|
|
||||||
|
const schedules = await this.teamsSchedulesService.getTeamSchedules(teamId, skip, take);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: schedules,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@Roles("TEAM_ADMIN")
|
@Roles("TEAM_ADMIN")
|
||||||
@PlatformPlan("ESSENTIALS")
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
|||||||
+39
@@ -18,6 +18,7 @@ import { randomString } from "test/utils/randomString";
|
|||||||
import { withApiAuth } from "test/utils/withApiAuth";
|
import { withApiAuth } from "test/utils/withApiAuth";
|
||||||
|
|
||||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import { GetSchedulesOutput_2024_06_11 } from "@calcom/platform-types";
|
||||||
import { ApiSuccessResponse, ScheduleOutput_2024_06_11 } from "@calcom/platform-types";
|
import { ApiSuccessResponse, ScheduleOutput_2024_06_11 } from "@calcom/platform-types";
|
||||||
import { Team, Schedule } from "@calcom/prisma/client";
|
import { Team, Schedule } from "@calcom/prisma/client";
|
||||||
|
|
||||||
@@ -36,6 +37,8 @@ describe("Organizations Teams Schedules Endpoints", () => {
|
|||||||
let org: Team;
|
let org: Team;
|
||||||
let orgTeam: Team;
|
let orgTeam: Team;
|
||||||
let nonOrgTeam: Team;
|
let nonOrgTeam: Team;
|
||||||
|
|
||||||
|
let userSchedule: Schedule;
|
||||||
let user2Schedule: Schedule;
|
let user2Schedule: Schedule;
|
||||||
|
|
||||||
const userEmail = `organizations-teams-schedules-admin-${randomString()}@api.com`;
|
const userEmail = `organizations-teams-schedules-admin-${randomString()}@api.com`;
|
||||||
@@ -73,6 +76,16 @@ describe("Organizations Teams Schedules Endpoints", () => {
|
|||||||
username: userEmail2,
|
username: userEmail2,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
userSchedule = await scheduleRepositoryFixture.create({
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: `organizations-teams-schedules-user-schedule-${randomString()}`,
|
||||||
|
timeZone: "America/New_York",
|
||||||
|
});
|
||||||
|
|
||||||
user2Schedule = await scheduleRepositoryFixture.create({
|
user2Schedule = await scheduleRepositoryFixture.create({
|
||||||
user: {
|
user: {
|
||||||
connect: {
|
connect: {
|
||||||
@@ -214,6 +227,32 @@ describe("Organizations Teams Schedules Endpoints", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should get all the schedules of members in a team", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/schedules`)
|
||||||
|
.expect(200)
|
||||||
|
.then((response) => {
|
||||||
|
const responseBody: GetSchedulesOutput_2024_06_11 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(Array.isArray(responseBody.data)).toBe(true);
|
||||||
|
expect(responseBody.data.length).toEqual(2);
|
||||||
|
|
||||||
|
const userOneSchedule = responseBody.data.find((schedule) => schedule.id === userSchedule.id);
|
||||||
|
const userTwoSchedule = responseBody.data.find((schedule) => schedule.id === user2Schedule.id);
|
||||||
|
|
||||||
|
expect(userOneSchedule).toBeDefined();
|
||||||
|
expect(userTwoSchedule).toBeDefined();
|
||||||
|
|
||||||
|
expect(userOneSchedule?.id).toEqual(userSchedule.id);
|
||||||
|
expect(userOneSchedule?.name).toEqual(userSchedule.name);
|
||||||
|
expect(userOneSchedule?.timeZone).toEqual(userSchedule.timeZone);
|
||||||
|
|
||||||
|
expect(userTwoSchedule?.id).toEqual(user2Schedule.id);
|
||||||
|
expect(userTwoSchedule?.name).toEqual(user2Schedule.name);
|
||||||
|
expect(userOneSchedule?.timeZone).toEqual(user2Schedule.timeZone);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await userRepositoryFixture.deleteByEmail(user.email);
|
await userRepositoryFixture.deleteByEmail(user.email);
|
||||||
await userRepositoryFixture.deleteByEmail(userToInviteViaApi.email);
|
await userRepositoryFixture.deleteByEmail(userToInviteViaApi.email);
|
||||||
|
|||||||
+21
-1
@@ -12,7 +12,12 @@ import {
|
|||||||
EMAIL,
|
EMAIL,
|
||||||
WorkflowEmailAttendeeStepDto,
|
WorkflowEmailAttendeeStepDto,
|
||||||
} from "@/modules/workflows/inputs/workflow-step.input";
|
} from "@/modules/workflows/inputs/workflow-step.input";
|
||||||
import { BEFORE_EVENT, DAY } from "@/modules/workflows/inputs/workflow-trigger.input";
|
import {
|
||||||
|
BEFORE_EVENT,
|
||||||
|
DAY,
|
||||||
|
OnAfterEventTriggerDto,
|
||||||
|
OnBeforeEventTriggerDto,
|
||||||
|
} from "@/modules/workflows/inputs/workflow-trigger.input";
|
||||||
// Adjust path if needed
|
// Adjust path if needed
|
||||||
import { GetWorkflowOutput, GetWorkflowsOutput } from "@/modules/workflows/outputs/workflow.output";
|
import { GetWorkflowOutput, GetWorkflowsOutput } from "@/modules/workflows/outputs/workflow.output";
|
||||||
import { INestApplication } from "@nestjs/common";
|
import { INestApplication } from "@nestjs/common";
|
||||||
@@ -255,6 +260,10 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => {
|
|||||||
"+37255555555"
|
"+37255555555"
|
||||||
);
|
);
|
||||||
expect(responseBody.data.steps.find((step) => step.stepNumber === 3)?.email).toEqual(authEmail);
|
expect(responseBody.data.steps.find((step) => step.stepNumber === 3)?.email).toEqual(authEmail);
|
||||||
|
const trigger = sampleCreateWorkflowDto.trigger as OnBeforeEventTriggerDto;
|
||||||
|
expect(responseBody.data.trigger?.offset?.value).toEqual(trigger.offset.value);
|
||||||
|
expect(responseBody.data.trigger?.offset?.unit).toEqual(trigger.offset.unit);
|
||||||
|
|
||||||
createdWorkflowId = responseBody.data.id;
|
createdWorkflowId = responseBody.data.id;
|
||||||
createdWorkflow = responseBody.data;
|
createdWorkflow = responseBody.data;
|
||||||
});
|
});
|
||||||
@@ -330,6 +339,13 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => {
|
|||||||
expect(step1).toBeDefined();
|
expect(step1).toBeDefined();
|
||||||
const partialUpdateDto: Partial<CreateWorkflowDto> = {
|
const partialUpdateDto: Partial<CreateWorkflowDto> = {
|
||||||
name: updatedName,
|
name: updatedName,
|
||||||
|
trigger: {
|
||||||
|
type: "afterEvent",
|
||||||
|
offset: {
|
||||||
|
unit: "minute",
|
||||||
|
value: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
steps: step1 ? [{ ...step1, sender: "updatedSender" } as WorkflowEmailAttendeeStepDto] : [],
|
steps: step1 ? [{ ...step1, sender: "updatedSender" } as WorkflowEmailAttendeeStepDto] : [],
|
||||||
};
|
};
|
||||||
expect(createdWorkflowId).toBeDefined();
|
expect(createdWorkflowId).toBeDefined();
|
||||||
@@ -348,6 +364,10 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => {
|
|||||||
step1 && expect(responseBody.data.steps[0].id).toEqual(step1.id);
|
step1 && expect(responseBody.data.steps[0].id).toEqual(step1.id);
|
||||||
expect(responseBody.data.steps[0].sender).toEqual("updatedSender");
|
expect(responseBody.data.steps[0].sender).toEqual("updatedSender");
|
||||||
expect(responseBody.data.steps[1]?.id).toBeUndefined();
|
expect(responseBody.data.steps[1]?.id).toBeUndefined();
|
||||||
|
const trigger = partialUpdateDto.trigger as OnAfterEventTriggerDto;
|
||||||
|
expect(responseBody.data.trigger?.type).toEqual(trigger.type);
|
||||||
|
expect(responseBody.data.trigger?.offset?.value).toEqual(trigger.offset.value);
|
||||||
|
expect(responseBody.data.trigger?.offset?.unit).toEqual(trigger.offset.unit);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -156,17 +156,4 @@ export class TeamsEventTypesService {
|
|||||||
|
|
||||||
return this.eventTypesRepository.deleteEventType(eventTypeId);
|
return this.eventTypesRepository.deleteEventType(eventTypeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteUserTeamEventTypesAndHosts(userId: number, teamId: number) {
|
|
||||||
try {
|
|
||||||
await this.teamsEventTypesRepository.deleteUserManagedTeamEventTypes(userId, teamId);
|
|
||||||
await this.teamsEventTypesRepository.removeUserFromTeamEventTypesHosts(userId, teamId);
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.error("Could not remove user from all team event-types.", {
|
|
||||||
error: err,
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-8
@@ -3,7 +3,6 @@ import { API_KEY_HEADER } from "@/lib/docs/headers";
|
|||||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||||
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
|
|
||||||
import { CreateTeamMembershipInput } from "@/modules/teams/memberships/inputs/create-team-membership.input";
|
import { CreateTeamMembershipInput } from "@/modules/teams/memberships/inputs/create-team-membership.input";
|
||||||
import { UpdateTeamMembershipInput } from "@/modules/teams/memberships/inputs/update-team-membership.input";
|
import { UpdateTeamMembershipInput } from "@/modules/teams/memberships/inputs/update-team-membership.input";
|
||||||
import { CreateTeamMembershipOutput } from "@/modules/teams/memberships/outputs/create-team-membership.output";
|
import { CreateTeamMembershipOutput } from "@/modules/teams/memberships/outputs/create-team-membership.output";
|
||||||
@@ -45,10 +44,7 @@ import { SkipTakePagination } from "@calcom/platform-types";
|
|||||||
export class TeamsMembershipsController {
|
export class TeamsMembershipsController {
|
||||||
private logger = new Logger("TeamsMembershipsController");
|
private logger = new Logger("TeamsMembershipsController");
|
||||||
|
|
||||||
constructor(
|
constructor(private teamsMembershipsService: TeamsMembershipsService) {}
|
||||||
private teamsMembershipsService: TeamsMembershipsService,
|
|
||||||
private teamsEventTypesService: TeamsEventTypesService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@Roles("TEAM_ADMIN")
|
@Roles("TEAM_ADMIN")
|
||||||
@Post("/")
|
@Post("/")
|
||||||
@@ -127,7 +123,6 @@ export class TeamsMembershipsController {
|
|||||||
membershipId,
|
membershipId,
|
||||||
body
|
body
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!currentMembership.accepted && updatedMembership.accepted) {
|
if (!currentMembership.accepted && updatedMembership.accepted) {
|
||||||
try {
|
try {
|
||||||
await updateNewTeamMemberEventTypes(updatedMembership.userId, teamId);
|
await updateNewTeamMemberEventTypes(updatedMembership.userId, teamId);
|
||||||
@@ -151,8 +146,6 @@ export class TeamsMembershipsController {
|
|||||||
): Promise<DeleteTeamMembershipOutput> {
|
): Promise<DeleteTeamMembershipOutput> {
|
||||||
const membership = await this.teamsMembershipsService.deleteTeamMembership(teamId, membershipId);
|
const membership = await this.teamsMembershipsService.deleteTeamMembership(teamId, membershipId);
|
||||||
|
|
||||||
await this.teamsEventTypesService.deleteUserTeamEventTypesAndHosts(membership.userId, teamId);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
|
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { UpdateTeamMembershipInput } from "@/modules/teams/memberships/inputs/up
|
|||||||
import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository";
|
import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository";
|
||||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { TeamService } from "@calcom/platform-libraries";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TeamsMembershipsService {
|
export class TeamsMembershipsService {
|
||||||
constructor(private readonly teamsMembershipsRepository: TeamsMembershipsRepository) {}
|
constructor(private readonly teamsMembershipsRepository: TeamsMembershipsRepository) {}
|
||||||
@@ -41,10 +43,15 @@ export class TeamsMembershipsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteTeamMembership(teamId: number, membershipId: number) {
|
async deleteTeamMembership(teamId: number, membershipId: number) {
|
||||||
const teamMembership = await this.teamsMembershipsRepository.deleteTeamMembershipById(
|
// First get the membership to get the userId
|
||||||
teamId,
|
const teamMembership = await this.teamsMembershipsRepository.findTeamMembership(teamId, membershipId);
|
||||||
membershipId
|
|
||||||
);
|
if (!teamMembership) {
|
||||||
|
throw new NotFoundException(`Membership with id ${membershipId} not found in team ${teamId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await TeamService.removeMembers({ teamIds: [teamId], userIds: [teamMembership.userId], isOrg: false });
|
||||||
|
|
||||||
return teamMembership;
|
return teamMembership;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
|
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
||||||
|
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||||
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
|
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||||
|
import { TeamsSchedulesService } from "@/modules/teams/schedules/services/teams-schedules.service";
|
||||||
|
import { Controller, UseGuards, Get, Param, ParseIntPipe, Query } from "@nestjs/common";
|
||||||
|
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import { GetSchedulesOutput_2024_06_11, SkipTakePagination } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
@Controller({
|
||||||
|
path: "/v2/teams/:teamId/schedules",
|
||||||
|
version: API_VERSIONS_VALUES,
|
||||||
|
})
|
||||||
|
@UseGuards(ApiAuthGuard, RolesGuard)
|
||||||
|
@DocsTags("Teams / Schedules")
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
export class TeamsSchedulesController {
|
||||||
|
constructor(private teamsSchedulesService: TeamsSchedulesService) {}
|
||||||
|
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@Get("/")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get all team member schedules",
|
||||||
|
})
|
||||||
|
async getTeamSchedules(
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number,
|
||||||
|
@Query() queryParams: SkipTakePagination
|
||||||
|
): Promise<GetSchedulesOutput_2024_06_11> {
|
||||||
|
const { skip, take } = queryParams;
|
||||||
|
|
||||||
|
const schedules = await this.teamsSchedulesService.getTeamSchedules(teamId, skip, take);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: schedules,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
import { bootstrap } from "@/app";
|
||||||
|
import { AppModule } from "@/app.module";
|
||||||
|
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 { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
|
||||||
|
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||||
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
|
import { randomString } from "test/utils/randomString";
|
||||||
|
import { withApiAuth } from "test/utils/withApiAuth";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import { GetSchedulesOutput_2024_06_11 } from "@calcom/platform-types";
|
||||||
|
import { Team, Schedule } from "@calcom/prisma/client";
|
||||||
|
|
||||||
|
describe("Teams Schedules Endpoints", () => {
|
||||||
|
describe("User Authentication - User is Team Admin", () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
|
||||||
|
let userRepositoryFixture: UserRepositoryFixture;
|
||||||
|
|
||||||
|
let teamsRepositoryFixture: TeamRepositoryFixture;
|
||||||
|
let scheduleRepositoryFixture: SchedulesRepositoryFixture;
|
||||||
|
|
||||||
|
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||||
|
|
||||||
|
let nonOrgTeam: Team;
|
||||||
|
|
||||||
|
let userSchedule: Schedule;
|
||||||
|
let user2Schedule: Schedule;
|
||||||
|
|
||||||
|
const userEmail = `teams-schedules-admin-${randomString()}@api.com`;
|
||||||
|
const userEmail2 = `teams-schedules-member-${randomString()}@api.com`;
|
||||||
|
|
||||||
|
let user: User;
|
||||||
|
let user2: User;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const moduleRef = await withApiAuth(
|
||||||
|
userEmail,
|
||||||
|
Test.createTestingModule({
|
||||||
|
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
||||||
|
})
|
||||||
|
).compile();
|
||||||
|
|
||||||
|
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||||
|
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||||
|
|
||||||
|
scheduleRepositoryFixture = new SchedulesRepositoryFixture(moduleRef);
|
||||||
|
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||||
|
|
||||||
|
user = await userRepositoryFixture.create({
|
||||||
|
email: userEmail,
|
||||||
|
username: userEmail,
|
||||||
|
});
|
||||||
|
user2 = await userRepositoryFixture.create({
|
||||||
|
email: userEmail2,
|
||||||
|
username: userEmail2,
|
||||||
|
});
|
||||||
|
|
||||||
|
userSchedule = await scheduleRepositoryFixture.create({
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: `teams-schedules-user-schedule-${randomString()}`,
|
||||||
|
timeZone: "America/New_York",
|
||||||
|
});
|
||||||
|
|
||||||
|
user2Schedule = await scheduleRepositoryFixture.create({
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: user2.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: `teams-schedules-user2-schedule-${randomString()}`,
|
||||||
|
timeZone: "America/New_York",
|
||||||
|
});
|
||||||
|
|
||||||
|
nonOrgTeam = await teamsRepositoryFixture.create({
|
||||||
|
name: `teams-schedules-non-org-team-${randomString()}`,
|
||||||
|
isOrganization: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await membershipsRepositoryFixture.create({
|
||||||
|
role: "ADMIN",
|
||||||
|
user: { connect: { id: user.id } },
|
||||||
|
team: { connect: { id: nonOrgTeam.id } },
|
||||||
|
});
|
||||||
|
|
||||||
|
await membershipsRepositoryFixture.create({
|
||||||
|
role: "MEMBER",
|
||||||
|
user: { connect: { id: user2.id } },
|
||||||
|
team: { connect: { id: nonOrgTeam.id } },
|
||||||
|
});
|
||||||
|
|
||||||
|
app = moduleRef.createNestApplication();
|
||||||
|
bootstrap(app as NestExpressApplication);
|
||||||
|
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should get all the schedules of members in a team", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/teams/${nonOrgTeam.id}/schedules`)
|
||||||
|
.expect(200)
|
||||||
|
.then((response) => {
|
||||||
|
const responseBody: GetSchedulesOutput_2024_06_11 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(Array.isArray(responseBody.data)).toBe(true);
|
||||||
|
expect(responseBody.data.length).toEqual(2);
|
||||||
|
|
||||||
|
const userOneSchedule = responseBody.data.find((schedule) => schedule.id === userSchedule.id);
|
||||||
|
const userTwoSchedule = responseBody.data.find((schedule) => schedule.id === user2Schedule.id);
|
||||||
|
|
||||||
|
expect(userOneSchedule).toBeDefined();
|
||||||
|
expect(userTwoSchedule).toBeDefined();
|
||||||
|
|
||||||
|
expect(userOneSchedule?.id).toEqual(userSchedule.id);
|
||||||
|
expect(userOneSchedule?.name).toEqual(userSchedule.name);
|
||||||
|
expect(userOneSchedule?.timeZone).toEqual(userSchedule.timeZone);
|
||||||
|
|
||||||
|
expect(userTwoSchedule?.id).toEqual(user2Schedule.id);
|
||||||
|
expect(userTwoSchedule?.name).toEqual(user2Schedule.name);
|
||||||
|
expect(userOneSchedule?.timeZone).toEqual(user2Schedule.timeZone);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await userRepositoryFixture.deleteByEmail(user.email);
|
||||||
|
await userRepositoryFixture.deleteByEmail(user2.email);
|
||||||
|
await teamsRepositoryFixture.delete(nonOrgTeam.id);
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
|
||||||
|
import { OutputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/output-schedules.service";
|
||||||
|
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { ScheduleOutput_2024_06_11 } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TeamsSchedulesService {
|
||||||
|
constructor(
|
||||||
|
private readonly teamsRepository: TeamsRepository,
|
||||||
|
private readonly schedulesRepository: SchedulesRepository_2024_06_11,
|
||||||
|
private readonly outputSchedulesService: OutputSchedulesService_2024_06_11
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getTeamSchedules(teamId: number, skip = 0, take = 250) {
|
||||||
|
const userIds = await this.teamsRepository.getTeamUsersIds(teamId);
|
||||||
|
const schedules = await this.schedulesRepository.getSchedulesByUserIds(userIds, skip, take);
|
||||||
|
|
||||||
|
const responseSchedules: ScheduleOutput_2024_06_11[] = [];
|
||||||
|
|
||||||
|
for (const schedule of schedules) {
|
||||||
|
responseSchedules.push(await this.outputSchedulesService.getResponseSchedule(schedule));
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseSchedules;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
|
||||||
|
import { OutputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/output-schedules.service";
|
||||||
|
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||||
|
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||||
|
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||||
|
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||||
|
import { OrganizationSchedulesRepository } from "@/modules/organizations/schedules/organizations-schedules.repository";
|
||||||
|
import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository";
|
||||||
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
|
import { RedisModule } from "@/modules/redis/redis.module";
|
||||||
|
import { StripeService } from "@/modules/stripe/stripe.service";
|
||||||
|
import { TeamsMembershipsService } from "@/modules/teams/memberships/services/teams-memberships.service";
|
||||||
|
import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository";
|
||||||
|
import { TeamsSchedulesController } from "@/modules/teams/schedules/controllers/teams-schedules.controller";
|
||||||
|
import { TeamsSchedulesService } from "@/modules/teams/schedules/services/teams-schedules.service";
|
||||||
|
import { TeamsController } from "@/modules/teams/teams/controllers/teams.controller";
|
||||||
|
import { TeamsService } from "@/modules/teams/teams/services/teams.service";
|
||||||
|
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
|
||||||
|
import { UsersRepository } from "@/modules/users/users.repository";
|
||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PrismaModule, MembershipsModule, RedisModule],
|
||||||
|
providers: [
|
||||||
|
TeamsRepository,
|
||||||
|
TeamsService,
|
||||||
|
TeamsMembershipsRepository,
|
||||||
|
TeamsMembershipsService,
|
||||||
|
OutputSchedulesService_2024_06_11,
|
||||||
|
OrganizationSchedulesRepository,
|
||||||
|
StripeService,
|
||||||
|
UsersRepository,
|
||||||
|
AppsRepository,
|
||||||
|
CredentialsRepository,
|
||||||
|
OrganizationsRepository,
|
||||||
|
TeamsSchedulesService,
|
||||||
|
OrganizationsTeamsRepository,
|
||||||
|
SchedulesRepository_2024_06_11,
|
||||||
|
],
|
||||||
|
controllers: [TeamsController, TeamsSchedulesController],
|
||||||
|
exports: [TeamsRepository],
|
||||||
|
})
|
||||||
|
export class TeamsSchedulesModule {}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { TimeUnit, WorkflowTriggerEvents } from "@prisma/client";
|
import { TimeUnit, WorkflowTriggerEvents } from "@prisma/client";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { IsNumber, ValidateNested } from "class-validator";
|
import { IsIn, IsNumber, IsString, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
export const BEFORE_EVENT = "beforeEvent";
|
export const BEFORE_EVENT = "beforeEvent";
|
||||||
export const EVENT_CANCELLED = "eventCancelled";
|
export const EVENT_CANCELLED = "eventCancelled";
|
||||||
@@ -68,6 +68,8 @@ export class WorkflowTriggerOffsetDto {
|
|||||||
value!: number;
|
value!: number;
|
||||||
|
|
||||||
@ApiProperty({ description: "Unit for the offset time", example: HOUR })
|
@ApiProperty({ description: "Unit for the offset time", example: HOUR })
|
||||||
|
@IsString()
|
||||||
|
@IsIn(TIME_UNITS)
|
||||||
unit!: TimeUnitType;
|
unit!: TimeUnitType;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +77,8 @@ export class BaseWorkflowTriggerDto {
|
|||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: "Trigger type for the workflow",
|
description: "Trigger type for the workflow",
|
||||||
})
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsIn([WORKFLOW_TRIGGER_TYPES])
|
||||||
type!: WorkflowTriggerType;
|
type!: WorkflowTriggerType;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,6 +86,8 @@ export class OnCreationTriggerDto {
|
|||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: "Trigger type for the workflow",
|
description: "Trigger type for the workflow",
|
||||||
})
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsIn([NEW_EVENT])
|
||||||
type: typeof NEW_EVENT = NEW_EVENT;
|
type: typeof NEW_EVENT = NEW_EVENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,12 +95,16 @@ export class OnRescheduleTriggerDto {
|
|||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: "Trigger type for the workflow",
|
description: "Trigger type for the workflow",
|
||||||
})
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsIn([RESCHEDULE_EVENT])
|
||||||
type: typeof RESCHEDULE_EVENT = RESCHEDULE_EVENT;
|
type: typeof RESCHEDULE_EVENT = RESCHEDULE_EVENT;
|
||||||
}
|
}
|
||||||
export class OnCancelTriggerDto {
|
export class OnCancelTriggerDto {
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: "Trigger type for the workflow",
|
description: "Trigger type for the workflow",
|
||||||
})
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsIn([EVENT_CANCELLED])
|
||||||
type: typeof EVENT_CANCELLED = EVENT_CANCELLED;
|
type: typeof EVENT_CANCELLED = EVENT_CANCELLED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +123,8 @@ export class OnBeforeEventTriggerDto extends TriggerOffsetDTO {
|
|||||||
description: "Trigger type for the workflow",
|
description: "Trigger type for the workflow",
|
||||||
example: BEFORE_EVENT,
|
example: BEFORE_EVENT,
|
||||||
})
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsIn([BEFORE_EVENT])
|
||||||
type: typeof BEFORE_EVENT = BEFORE_EVENT;
|
type: typeof BEFORE_EVENT = BEFORE_EVENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +133,8 @@ export class OnAfterEventTriggerDto extends TriggerOffsetDTO {
|
|||||||
description: "Trigger type for the workflow",
|
description: "Trigger type for the workflow",
|
||||||
example: AFTER_EVENT,
|
example: AFTER_EVENT,
|
||||||
})
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsIn([AFTER_EVENT])
|
||||||
type: typeof AFTER_EVENT = AFTER_EVENT;
|
type: typeof AFTER_EVENT = AFTER_EVENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,6 +143,8 @@ export class OnAfterCalVideoGuestsNoShowTriggerDto extends TriggerOffsetDTO {
|
|||||||
description: "Trigger type for the workflow",
|
description: "Trigger type for the workflow",
|
||||||
example: AFTER_GUESTS_CAL_VIDEO_NO_SHOW,
|
example: AFTER_GUESTS_CAL_VIDEO_NO_SHOW,
|
||||||
})
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsIn([AFTER_GUESTS_CAL_VIDEO_NO_SHOW])
|
||||||
type: typeof AFTER_GUESTS_CAL_VIDEO_NO_SHOW = AFTER_GUESTS_CAL_VIDEO_NO_SHOW;
|
type: typeof AFTER_GUESTS_CAL_VIDEO_NO_SHOW = AFTER_GUESTS_CAL_VIDEO_NO_SHOW;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,5 +153,7 @@ export class OnAfterCalVideoHostsNoShowTriggerDto extends TriggerOffsetDTO {
|
|||||||
description: "Trigger type for the workflow",
|
description: "Trigger type for the workflow",
|
||||||
example: AFTER_HOSTS_CAL_VIDEO_NO_SHOW,
|
example: AFTER_HOSTS_CAL_VIDEO_NO_SHOW,
|
||||||
})
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsIn([AFTER_HOSTS_CAL_VIDEO_NO_SHOW])
|
||||||
type: typeof AFTER_HOSTS_CAL_VIDEO_NO_SHOW = AFTER_HOSTS_CAL_VIDEO_NO_SHOW;
|
type: typeof AFTER_HOSTS_CAL_VIDEO_NO_SHOW = AFTER_HOSTS_CAL_VIDEO_NO_SHOW;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4384,6 +4384,292 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/private-links": {
|
||||||
|
"post": {
|
||||||
|
"operationId": "OrganizationsEventTypesPrivateLinksController_createPrivateLink",
|
||||||
|
"summary": "Create a private link for a team event type",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "Authorization",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For non-platform customers - value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x-cal-secret-key",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For platform customers - OAuth client secret key",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x-cal-client-id",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For platform customers - OAuth client ID",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "teamId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "eventTypeId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CreatePrivateLinkInput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CreatePrivateLinkOutput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Orgs / Teams / Event Types / Private Links"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"get": {
|
||||||
|
"operationId": "OrganizationsEventTypesPrivateLinksController_getPrivateLinks",
|
||||||
|
"summary": "Get all private links for a team event type",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "Authorization",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For non-platform customers - value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x-cal-secret-key",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For platform customers - OAuth client secret key",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x-cal-client-id",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For platform customers - OAuth client ID",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "teamId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "eventTypeId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/GetPrivateLinksOutput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Orgs / Teams / Event Types / Private Links"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/private-links/{linkId}": {
|
||||||
|
"patch": {
|
||||||
|
"operationId": "OrganizationsEventTypesPrivateLinksController_updatePrivateLink",
|
||||||
|
"summary": "Update a private link for a team event type",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "Authorization",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For non-platform customers - value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x-cal-secret-key",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For platform customers - OAuth client secret key",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x-cal-client-id",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For platform customers - OAuth client ID",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "teamId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "eventTypeId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "linkId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UpdatePrivateLinkOutput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Orgs / Teams / Event Types / Private Links"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"operationId": "OrganizationsEventTypesPrivateLinksController_deletePrivateLink",
|
||||||
|
"summary": "Delete a private link for a team event type",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "Authorization",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For non-platform customers - value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x-cal-secret-key",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For platform customers - OAuth client secret key",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x-cal-client-id",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For platform customers - OAuth client ID",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "teamId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "eventTypeId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "linkId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/DeletePrivateLinkOutput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Orgs / Teams / Event Types / Private Links"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/v2/organizations/{orgId}/teams/{teamId}/memberships": {
|
"/v2/organizations/{orgId}/teams/{teamId}/memberships": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "OrganizationsTeamsMembershipsController_getAllOrgTeamMemberships",
|
"operationId": "OrganizationsTeamsMembershipsController_getAllOrgTeamMemberships",
|
||||||
@@ -23789,6 +24075,213 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"CreatePrivateLinkInput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"expiresAt": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Expiration date for time-based links",
|
||||||
|
"format": "date-time",
|
||||||
|
"example": "2024-12-31T23:59:59.000Z"
|
||||||
|
},
|
||||||
|
"maxUsageCount": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Maximum number of times the link can be used. If omitted and expiresAt is not provided, defaults to 1 (one time use).",
|
||||||
|
"example": 10,
|
||||||
|
"minimum": 1,
|
||||||
|
"default": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"TimeBasedPrivateLinkOutput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"linkId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The private link ID",
|
||||||
|
"example": "abc123def456"
|
||||||
|
},
|
||||||
|
"eventTypeId": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Event type ID this link belongs to",
|
||||||
|
"example": 123
|
||||||
|
},
|
||||||
|
"isExpired": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether the link is currently expired",
|
||||||
|
"example": false
|
||||||
|
},
|
||||||
|
"bookingUrl": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Full booking URL for this private link",
|
||||||
|
"format": "uri",
|
||||||
|
"example": "https://cal.com/d/abc123def456/30min"
|
||||||
|
},
|
||||||
|
"expiresAt": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Expiration date for this time-based link",
|
||||||
|
"format": "date-time",
|
||||||
|
"example": "2025-12-31T23:59:59.000Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"linkId",
|
||||||
|
"eventTypeId",
|
||||||
|
"isExpired",
|
||||||
|
"bookingUrl",
|
||||||
|
"expiresAt"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"UsageBasedPrivateLinkOutput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"linkId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The private link ID",
|
||||||
|
"example": "abc123def456"
|
||||||
|
},
|
||||||
|
"eventTypeId": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Event type ID this link belongs to",
|
||||||
|
"example": 123
|
||||||
|
},
|
||||||
|
"isExpired": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether the link is currently expired",
|
||||||
|
"example": false
|
||||||
|
},
|
||||||
|
"bookingUrl": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Full booking URL for this private link",
|
||||||
|
"format": "uri",
|
||||||
|
"example": "https://cal.com/d/abc123def456/30min"
|
||||||
|
},
|
||||||
|
"maxUsageCount": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Maximum number of times this link can be used",
|
||||||
|
"example": 10
|
||||||
|
},
|
||||||
|
"usageCount": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Current usage count for this link",
|
||||||
|
"example": 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"linkId",
|
||||||
|
"eventTypeId",
|
||||||
|
"isExpired",
|
||||||
|
"bookingUrl",
|
||||||
|
"maxUsageCount",
|
||||||
|
"usageCount"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"CreatePrivateLinkOutput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Response status",
|
||||||
|
"example": "success"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"description": "Created private link data (either time-based or usage-based)",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"status",
|
||||||
|
"data"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"GetPrivateLinksOutput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Response status",
|
||||||
|
"example": "success"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Array of private links for the event type (mix of time-based and usage-based)",
|
||||||
|
"items": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"status",
|
||||||
|
"data"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"UpdatePrivateLinkOutput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Response status",
|
||||||
|
"example": "success"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"description": "Updated private link data (either time-based or usage-based)",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"status",
|
||||||
|
"data"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"DeletePrivateLinkOutput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Response status",
|
||||||
|
"example": "success"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Deleted link information",
|
||||||
|
"properties": {
|
||||||
|
"linkId": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "abc123def456"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Private link deleted successfully"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"status",
|
||||||
|
"data"
|
||||||
|
]
|
||||||
|
},
|
||||||
"StripConnectOutputDto": {
|
"StripConnectOutputDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -25637,6 +26130,23 @@
|
|||||||
"data"
|
"data"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"UpdatePrivateLinkBody": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"expiresAt": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string",
|
||||||
|
"description": "New expiration date for time-based links",
|
||||||
|
"example": "2024-12-31T23:59:59.000Z"
|
||||||
|
},
|
||||||
|
"maxUsageCount": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "New maximum number of times the link can be used",
|
||||||
|
"example": 10,
|
||||||
|
"minimum": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"MeOrgOutput": {
|
"MeOrgOutput": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -28282,230 +28792,6 @@
|
|||||||
"data"
|
"data"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"CreatePrivateLinkInput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"expiresAt": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Expiration date for time-based links",
|
|
||||||
"format": "date-time",
|
|
||||||
"example": "2024-12-31T23:59:59.000Z"
|
|
||||||
},
|
|
||||||
"maxUsageCount": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "Maximum number of times the link can be used. If omitted and expiresAt is not provided, defaults to 1 (one time use).",
|
|
||||||
"example": 10,
|
|
||||||
"minimum": 1,
|
|
||||||
"default": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"TimeBasedPrivateLinkOutput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"linkId": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The private link ID",
|
|
||||||
"example": "abc123def456"
|
|
||||||
},
|
|
||||||
"eventTypeId": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "Event type ID this link belongs to",
|
|
||||||
"example": 123
|
|
||||||
},
|
|
||||||
"isExpired": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Whether the link is currently expired",
|
|
||||||
"example": false
|
|
||||||
},
|
|
||||||
"bookingUrl": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Full booking URL for this private link",
|
|
||||||
"format": "uri",
|
|
||||||
"example": "https://cal.com/d/abc123def456/30min"
|
|
||||||
},
|
|
||||||
"expiresAt": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Expiration date for this time-based link",
|
|
||||||
"format": "date-time",
|
|
||||||
"example": "2025-12-31T23:59:59.000Z"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"linkId",
|
|
||||||
"eventTypeId",
|
|
||||||
"isExpired",
|
|
||||||
"bookingUrl",
|
|
||||||
"expiresAt"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"UsageBasedPrivateLinkOutput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"linkId": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The private link ID",
|
|
||||||
"example": "abc123def456"
|
|
||||||
},
|
|
||||||
"eventTypeId": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "Event type ID this link belongs to",
|
|
||||||
"example": 123
|
|
||||||
},
|
|
||||||
"isExpired": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Whether the link is currently expired",
|
|
||||||
"example": false
|
|
||||||
},
|
|
||||||
"bookingUrl": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Full booking URL for this private link",
|
|
||||||
"format": "uri",
|
|
||||||
"example": "https://cal.com/d/abc123def456/30min"
|
|
||||||
},
|
|
||||||
"maxUsageCount": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "Maximum number of times this link can be used",
|
|
||||||
"example": 10
|
|
||||||
},
|
|
||||||
"usageCount": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "Current usage count for this link",
|
|
||||||
"example": 3
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"linkId",
|
|
||||||
"eventTypeId",
|
|
||||||
"isExpired",
|
|
||||||
"bookingUrl",
|
|
||||||
"maxUsageCount",
|
|
||||||
"usageCount"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"CreatePrivateLinkOutput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"status": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Response status",
|
|
||||||
"example": "success"
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"description": "Created private link data (either time-based or usage-based)",
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"status",
|
|
||||||
"data"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"GetPrivateLinksOutput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"status": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Response status",
|
|
||||||
"example": "success"
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"type": "array",
|
|
||||||
"description": "Array of private links for the event type (mix of time-based and usage-based)",
|
|
||||||
"items": {
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"status",
|
|
||||||
"data"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"UpdatePrivateLinkBody": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"expiresAt": {
|
|
||||||
"format": "date-time",
|
|
||||||
"type": "string",
|
|
||||||
"description": "New expiration date for time-based links",
|
|
||||||
"example": "2024-12-31T23:59:59.000Z"
|
|
||||||
},
|
|
||||||
"maxUsageCount": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "New maximum number of times the link can be used",
|
|
||||||
"example": 10,
|
|
||||||
"minimum": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"UpdatePrivateLinkOutput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"status": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Response status",
|
|
||||||
"example": "success"
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"description": "Updated private link data (either time-based or usage-based)",
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"status",
|
|
||||||
"data"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"DeletePrivateLinkOutput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"status": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Response status",
|
|
||||||
"example": "success"
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"type": "object",
|
|
||||||
"description": "Deleted link information",
|
|
||||||
"properties": {
|
|
||||||
"linkId": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "abc123def456"
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "Private link deleted successfully"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"status",
|
|
||||||
"data"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"UserWebhookOutputDto": {
|
"UserWebhookOutputDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -59,4 +59,8 @@ export class EventTypesRepositoryFixture {
|
|||||||
async delete(eventTypeId: EventType["id"]) {
|
async delete(eventTypeId: EventType["id"]) {
|
||||||
return this.prismaWriteClient.eventType.delete({ where: { id: eventTypeId } });
|
return this.prismaWriteClient.eventType.delete({ where: { id: eventTypeId } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findById(eventTypeId: EventType["id"]) {
|
||||||
|
return this.prismaReadClient.eventType.findUnique({ where: { id: eventTypeId } });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
@@ -34,4 +34,13 @@ export class ManagedOrganizationsRepositoryFixture {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createManagedOrganization(managerOrganizationId: number, managedOrganizationId: number) {
|
||||||
|
return this.prismaWriteClient.managedOrganization.create({
|
||||||
|
data: {
|
||||||
|
managerOrganizationId,
|
||||||
|
managedOrganizationId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,4 +35,8 @@ export class MembershipRepositoryFixture {
|
|||||||
await this.prismaWriteClient.user.update({ where: { id: user.id }, data: { organizationId: org.id } });
|
await this.prismaWriteClient.user.update({ where: { id: user.id }, data: { organizationId: org.id } });
|
||||||
return membership;
|
return membership;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findById(membershipId: Membership["id"]) {
|
||||||
|
return this.prismaReadClient.membership.findUnique({ where: { id: membershipId } });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import { createRouterCaller } from "app/_trpc/context";
|
import { createRouterCaller } from "app/_trpc/context";
|
||||||
import type { PageProps } from "app/_types";
|
import type { PageProps } from "app/_types";
|
||||||
import { _generateMetadata } from "app/_utils";
|
import { _generateMetadata } from "app/_utils";
|
||||||
|
import { cookies, headers } from "next/headers";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
import { AppCategories } from "@calcom/prisma/enums";
|
import { AppCategories } from "@calcom/prisma/enums";
|
||||||
import { appsRouter } from "@calcom/trpc/server/routers/viewer/apps/_router";
|
import { appsRouter } from "@calcom/trpc/server/routers/viewer/apps/_router";
|
||||||
import { calendarsRouter } from "@calcom/trpc/server/routers/viewer/calendars/_router";
|
import { calendarsRouter } from "@calcom/trpc/server/routers/viewer/calendars/_router";
|
||||||
|
|
||||||
|
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||||
|
|
||||||
import InstalledApps from "~/apps/installed/[category]/installed-category-view";
|
import InstalledApps from "~/apps/installed/[category]/installed-category-view";
|
||||||
|
|
||||||
const querySchema = z.object({
|
const querySchema = z.object({
|
||||||
@@ -31,16 +35,23 @@ const InstalledAppsWrapper = async ({ params }: PageProps) => {
|
|||||||
redirect("/apps/installed/calendar");
|
redirect("/apps/installed/calendar");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return redirect("/auth/login");
|
||||||
|
}
|
||||||
|
|
||||||
const [calendarsCaller, appsCaller] = await Promise.all([
|
const [calendarsCaller, appsCaller] = await Promise.all([
|
||||||
createRouterCaller(calendarsRouter),
|
createRouterCaller(calendarsRouter),
|
||||||
createRouterCaller(appsRouter),
|
createRouterCaller(appsRouter),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const connectedCalendars = await calendarsCaller.connectedCalendars();
|
const [connectedCalendars, installedCalendars] = await Promise.all([
|
||||||
const installedCalendars = await appsCaller.integrations({
|
calendarsCaller.connectedCalendars(),
|
||||||
|
appsCaller.integrations({
|
||||||
variant: "calendar",
|
variant: "calendar",
|
||||||
onlyInstalled: true,
|
onlyInstalled: true,
|
||||||
});
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InstalledApps
|
<InstalledApps
|
||||||
|
|||||||
+4
-1
@@ -5,6 +5,7 @@ import { unstable_cache } from "next/cache";
|
|||||||
import { RoleManagementFactory } from "@calcom/features/pbac/services/role-management.factory";
|
import { RoleManagementFactory } from "@calcom/features/pbac/services/role-management.factory";
|
||||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||||
import { PrismaAttributeRepository } from "@calcom/lib/server/repository/PrismaAttributeRepository";
|
import { PrismaAttributeRepository } from "@calcom/lib/server/repository/PrismaAttributeRepository";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
import { viewerTeamsRouter } from "@calcom/trpc/server/routers/viewer/teams/_router";
|
import { viewerTeamsRouter } from "@calcom/trpc/server/routers/viewer/teams/_router";
|
||||||
|
|
||||||
import { TeamMembersView } from "~/teams/team-members-view";
|
import { TeamMembersView } from "~/teams/team-members-view";
|
||||||
@@ -36,8 +37,10 @@ const getCachedTeamRoles = unstable_cache(
|
|||||||
const getCachedTeamAttributes = unstable_cache(
|
const getCachedTeamAttributes = unstable_cache(
|
||||||
async (organizationId?: number) => {
|
async (organizationId?: number) => {
|
||||||
if (!organizationId) return [];
|
if (!organizationId) return [];
|
||||||
|
const attributeRepo = new PrismaAttributeRepository(prisma);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await PrismaAttributeRepository.findAllByOrgIdWithOptions({ orgId: organizationId });
|
return await attributeRepo.findAllByOrgIdWithOptions({ orgId: organizationId });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -4,6 +4,7 @@ import { unstable_cache } from "next/cache";
|
|||||||
|
|
||||||
import { RoleManagementFactory } from "@calcom/features/pbac/services/role-management.factory";
|
import { RoleManagementFactory } from "@calcom/features/pbac/services/role-management.factory";
|
||||||
import { PrismaAttributeRepository } from "@calcom/lib/server/repository/PrismaAttributeRepository";
|
import { PrismaAttributeRepository } from "@calcom/lib/server/repository/PrismaAttributeRepository";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
import { viewerOrganizationsRouter } from "@calcom/trpc/server/routers/viewer/organizations/_router";
|
import { viewerOrganizationsRouter } from "@calcom/trpc/server/routers/viewer/organizations/_router";
|
||||||
|
|
||||||
import { MembersView } from "~/members/members-view";
|
import { MembersView } from "~/members/members-view";
|
||||||
@@ -19,7 +20,9 @@ export const generateMetadata = async () =>
|
|||||||
|
|
||||||
const getCachedAttributes = unstable_cache(
|
const getCachedAttributes = unstable_cache(
|
||||||
async (orgId: number) => {
|
async (orgId: number) => {
|
||||||
return await PrismaAttributeRepository.findAllByOrgIdWithOptions({ orgId });
|
const attributeRepo = new PrismaAttributeRepository(prisma);
|
||||||
|
|
||||||
|
return await attributeRepo.findAllByOrgIdWithOptions({ orgId });
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
{ revalidate: 3600, tags: ["viewer.attributes.list"] } // Cache for 1 hour
|
{ revalidate: 3600, tags: ["viewer.attributes.list"] } // Cache for 1 hour
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
|
||||||
import type { PageProps } from "app/_types";
|
|
||||||
import { _generateMetadata } from "app/_utils";
|
import { _generateMetadata } from "app/_utils";
|
||||||
import { cookies, headers } from "next/headers";
|
|
||||||
|
|
||||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
|
||||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
|
||||||
|
|
||||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/about-view";
|
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/about-view";
|
||||||
|
|
||||||
@@ -17,10 +11,7 @@ export const generateMetadata = async () =>
|
|||||||
"/settings/organizations/new/about"
|
"/settings/organizations/new/about"
|
||||||
);
|
);
|
||||||
|
|
||||||
const getData = withAppDirSsr(getServerSideProps);
|
const ServerPage = async () => {
|
||||||
|
|
||||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
|
||||||
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
|
|
||||||
return (
|
return (
|
||||||
<LayoutWrapper>
|
<LayoutWrapper>
|
||||||
<LegacyPage />
|
<LegacyPage />
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
|
||||||
import type { PageProps } from "app/_types";
|
|
||||||
import { _generateMetadata } from "app/_utils";
|
import { _generateMetadata } from "app/_utils";
|
||||||
import { cookies, headers } from "next/headers";
|
|
||||||
|
|
||||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
|
||||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
|
||||||
|
|
||||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/add-teams-view";
|
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/add-teams-view";
|
||||||
|
|
||||||
@@ -17,10 +11,7 @@ export const generateMetadata = async () =>
|
|||||||
"/settings/organizations/new/add-teams"
|
"/settings/organizations/new/add-teams"
|
||||||
);
|
);
|
||||||
|
|
||||||
const getData = withAppDirSsr(getServerSideProps);
|
const ServerPage = async () => {
|
||||||
|
|
||||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
|
||||||
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
|
|
||||||
return (
|
return (
|
||||||
<LayoutWrapper>
|
<LayoutWrapper>
|
||||||
<LegacyPage />
|
<LegacyPage />
|
||||||
|
|||||||
@@ -1,11 +1,4 @@
|
|||||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
|
||||||
import type { PageProps } from "app/_types";
|
|
||||||
import { _generateMetadata } from "app/_utils";
|
import { _generateMetadata } from "app/_utils";
|
||||||
import { cookies } from "next/headers";
|
|
||||||
import { headers } from "next/headers";
|
|
||||||
|
|
||||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
|
||||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
|
||||||
|
|
||||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/onboarding-handover";
|
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/onboarding-handover";
|
||||||
|
|
||||||
@@ -18,10 +11,7 @@ export const generateMetadata = async () =>
|
|||||||
"/settings/organizations/new/handover"
|
"/settings/organizations/new/handover"
|
||||||
);
|
);
|
||||||
|
|
||||||
const getData = withAppDirSsr(getServerSideProps);
|
const ServerPage = async () => {
|
||||||
|
|
||||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
|
||||||
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
|
|
||||||
return (
|
return (
|
||||||
<LayoutWrapper>
|
<LayoutWrapper>
|
||||||
<LegacyPage />
|
<LegacyPage />
|
||||||
|
|||||||
+6
-12
@@ -1,21 +1,15 @@
|
|||||||
import type { GetServerSidePropsResult } from "next";
|
import { notFound } from "next/navigation";
|
||||||
|
|
||||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||||
import { prisma } from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
|
|
||||||
export const getServerSideProps = async (): Promise<GetServerSidePropsResult<{ isOrg: boolean }>> => {
|
export default async function Layout({ children }: { children: React.ReactNode }) {
|
||||||
const featuresRepository = new FeaturesRepository(prisma);
|
const featuresRepository = new FeaturesRepository(prisma);
|
||||||
const organizations = await featuresRepository.checkIfFeatureIsEnabledGlobally("organizations");
|
const organizations = await featuresRepository.checkIfFeatureIsEnabledGlobally("organizations");
|
||||||
// Check if organizations are enabled
|
|
||||||
if (!organizations) {
|
if (!organizations) {
|
||||||
return {
|
return notFound();
|
||||||
notFound: true,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return children;
|
||||||
props: {
|
}
|
||||||
isOrg: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
+1
-10
@@ -1,10 +1,4 @@
|
|||||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
|
||||||
import type { PageProps } from "app/_types";
|
|
||||||
import { _generateMetadata } from "app/_utils";
|
import { _generateMetadata } from "app/_utils";
|
||||||
import { cookies, headers } from "next/headers";
|
|
||||||
|
|
||||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
|
||||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
|
||||||
|
|
||||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/onboard-members-view";
|
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/onboard-members-view";
|
||||||
|
|
||||||
@@ -17,10 +11,7 @@ export const generateMetadata = async () =>
|
|||||||
"/settings/organizations/new/onboard-members"
|
"/settings/organizations/new/onboard-members"
|
||||||
);
|
);
|
||||||
|
|
||||||
const getData = withAppDirSsr(getServerSideProps);
|
const ServerPage = async () => {
|
||||||
|
|
||||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
|
||||||
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
|
|
||||||
return (
|
return (
|
||||||
<LayoutWrapper>
|
<LayoutWrapper>
|
||||||
<LegacyPage />
|
<LegacyPage />
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
|
||||||
import type { PageProps } from "app/_types";
|
|
||||||
import { _generateMetadata } from "app/_utils";
|
import { _generateMetadata } from "app/_utils";
|
||||||
import { cookies, headers } from "next/headers";
|
|
||||||
|
|
||||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||||
|
|
||||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
|
||||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
|
||||||
|
|
||||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/create-new-view";
|
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/create-new-view";
|
||||||
|
|
||||||
export const generateMetadata = async () =>
|
export const generateMetadata = async () =>
|
||||||
@@ -19,14 +13,7 @@ export const generateMetadata = async () =>
|
|||||||
"/settings/organizations/new"
|
"/settings/organizations/new"
|
||||||
);
|
);
|
||||||
|
|
||||||
type Props = {
|
const ServerPage = async () => {
|
||||||
isOrg: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getData = withAppDirSsr<Props>(getServerSideProps);
|
|
||||||
|
|
||||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
|
||||||
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
|
|
||||||
return (
|
return (
|
||||||
<LayoutWrapper>
|
<LayoutWrapper>
|
||||||
<LicenseRequired>
|
<LicenseRequired>
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
|
||||||
import type { PageProps } from "app/_types";
|
|
||||||
import { _generateMetadata } from "app/_utils";
|
import { _generateMetadata } from "app/_utils";
|
||||||
import { cookies, headers } from "next/headers";
|
|
||||||
|
|
||||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
|
||||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
|
||||||
|
|
||||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/payment-status-view";
|
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/payment-status-view";
|
||||||
|
|
||||||
@@ -17,10 +11,7 @@ export const generateMetadata = async () =>
|
|||||||
"/settings/organizations/new/status"
|
"/settings/organizations/new/status"
|
||||||
);
|
);
|
||||||
|
|
||||||
const getData = withAppDirSsr(getServerSideProps);
|
const ServerPage = async () => {
|
||||||
|
|
||||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
|
||||||
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
|
|
||||||
return (
|
return (
|
||||||
<LayoutWrapper>
|
<LayoutWrapper>
|
||||||
<LegacyPage />
|
<LegacyPage />
|
||||||
|
|||||||
@@ -13,5 +13,5 @@ export type PageProps = {
|
|||||||
|
|
||||||
export type LayoutProps = { params: Promise<Params>; children: React.ReactElement };
|
export type LayoutProps = { params: Promise<Params>; children: React.ReactElement };
|
||||||
|
|
||||||
export { type ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers";
|
export type ReadonlyHeaders = Awaited<ReturnType<typeof import("next/headers").headers>>;
|
||||||
export { type ReadonlyRequestCookies } from "next/dist/server/web/spec-extension/adapters/request-cookies";
|
export type ReadonlyRequestCookies = Awaited<ReturnType<typeof import("next/headers").cookies>>;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { getLocale } from "@calcom/features/auth/lib/getLocale";
|
|||||||
import type { AppImageProps, MeetingImageProps } from "@calcom/lib/OgImages";
|
import type { AppImageProps, MeetingImageProps } from "@calcom/lib/OgImages";
|
||||||
import { constructAppImage, constructGenericImage, constructMeetingImage } from "@calcom/lib/OgImages";
|
import { constructAppImage, constructGenericImage, constructMeetingImage } from "@calcom/lib/OgImages";
|
||||||
import { IS_CALCOM, WEBAPP_URL, APP_NAME, SEO_IMG_OGIMG, CAL_URL } from "@calcom/lib/constants";
|
import { IS_CALCOM, WEBAPP_URL, APP_NAME, SEO_IMG_OGIMG, CAL_URL } from "@calcom/lib/constants";
|
||||||
|
import { getCalcomUrl } from "@calcom/lib/getCalcomUrl";
|
||||||
import { buildCanonical } from "@calcom/lib/next-seo.config";
|
import { buildCanonical } from "@calcom/lib/next-seo.config";
|
||||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||||
import { truncateOnWord } from "@calcom/lib/text";
|
import { truncateOnWord } from "@calcom/lib/text";
|
||||||
@@ -32,7 +33,7 @@ const _generateMetadataWithoutImage = async (
|
|||||||
const description = getDescription(t);
|
const description = getDescription(t);
|
||||||
const titleSuffix = `| ${APP_NAME}`;
|
const titleSuffix = `| ${APP_NAME}`;
|
||||||
const displayedTitle = title.includes(titleSuffix) || hideBranding ? title : `${title} ${titleSuffix}`;
|
const displayedTitle = title.includes(titleSuffix) || hideBranding ? title : `${title} ${titleSuffix}`;
|
||||||
const metadataBase = new URL(IS_CALCOM ? "https://cal.com" : WEBAPP_URL);
|
const metadataBase = new URL(IS_CALCOM ? getCalcomUrl() : WEBAPP_URL);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: title.length === 0 ? APP_NAME : displayedTitle,
|
title: title.length === 0 ? APP_NAME : displayedTitle,
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ async function getHandler(request: NextRequest) {
|
|||||||
|
|
||||||
buffer = await optimizeImage({
|
buffer = await optimizeImage({
|
||||||
buffer,
|
buffer,
|
||||||
contentType: detectContentType(buffer) ?? "image/jpeg",
|
contentType: (await detectContentType(buffer)) ?? "image/jpeg",
|
||||||
quality: 100,
|
quality: 100,
|
||||||
width: logoDefinition.w,
|
width: logoDefinition.w,
|
||||||
height: logoDefinition.h, // optional
|
height: logoDefinition.h, // optional
|
||||||
|
|||||||
@@ -87,7 +87,10 @@ async function handler(req: NextRequest) {
|
|||||||
|
|
||||||
return new Response(img.body, {
|
return new Response(img.body, {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: { "Content-Type": "image/png", "cache-control": "max-age=0" },
|
headers: {
|
||||||
|
"Content-Type": "image/png",
|
||||||
|
"Cache-Control": "public, max-age=3600, stale-while-revalidate=86400",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ZodError) {
|
if (error instanceof ZodError) {
|
||||||
@@ -116,7 +119,13 @@ async function handler(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
const img = new ImageResponse(<App name={name} description={description} slug={slug} />, ogConfig);
|
const img = new ImageResponse(<App name={name} description={description} slug={slug} />, ogConfig);
|
||||||
|
|
||||||
return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
|
return new Response(img.body, {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "image/png",
|
||||||
|
"Cache-Control": "public, max-age=3600, stale-while-revalidate=86400",
|
||||||
|
},
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ZodError) {
|
if (error instanceof ZodError) {
|
||||||
return new Response(
|
return new Response(
|
||||||
@@ -144,7 +153,13 @@ async function handler(req: NextRequest) {
|
|||||||
|
|
||||||
const img = new ImageResponse(<Generic title={title} description={description} />, ogConfig);
|
const img = new ImageResponse(<Generic title={title} description={description} />, ogConfig);
|
||||||
|
|
||||||
return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
|
return new Response(img.body, {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "image/png",
|
||||||
|
"Cache-Control": "public, max-age=3600, stale-while-revalidate=86400",
|
||||||
|
},
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ZodError) {
|
if (error instanceof ZodError) {
|
||||||
return new Response(
|
return new Response(
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import Script from "next/script";
|
|||||||
import "@calcom/embed-core/src/embed-iframe";
|
import "@calcom/embed-core/src/embed-iframe";
|
||||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||||
import { IS_CALCOM, WEBAPP_URL } from "@calcom/lib/constants";
|
import { IS_CALCOM, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
|
import { getCalcomUrl } from "@calcom/lib/getCalcomUrl";
|
||||||
import { buildCanonical } from "@calcom/lib/next-seo.config";
|
import { buildCanonical } from "@calcom/lib/next-seo.config";
|
||||||
import { IconSprites } from "@calcom/ui/components/icon";
|
import { IconSprites } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
@@ -64,7 +65,7 @@ function PageWrapper(props: AppProps) {
|
|||||||
// Set canonical to https://cal.com or self-hosted URL
|
// Set canonical to https://cal.com or self-hosted URL
|
||||||
canonical={
|
canonical={
|
||||||
IS_CALCOM
|
IS_CALCOM
|
||||||
? buildCanonical({ path, origin: "https://cal.com" }) // cal.com & .dev
|
? buildCanonical({ path, origin: getCalcomUrl() }) // cal.com & .dev
|
||||||
: buildCanonical({ path, origin: WEBAPP_URL }) // self-hosted
|
: buildCanonical({ path, origin: WEBAPP_URL }) // self-hosted
|
||||||
}
|
}
|
||||||
{...seoConfig.defaultNextSeo}
|
{...seoConfig.defaultNextSeo}
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import { useMemo } from "react";
|
|||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
import type { UseFormGetValues, UseFormSetValue, Control, FormState } from "react-hook-form";
|
import type { UseFormGetValues, UseFormSetValue, Control, FormState } from "react-hook-form";
|
||||||
|
|
||||||
import type { TLocationOptions } from "@calcom/features/eventtypes/components/Locations";
|
import type {
|
||||||
import type { TEventTypeLocation } from "@calcom/features/eventtypes/components/Locations";
|
TLocationOptions,
|
||||||
import Locations from "@calcom/features/eventtypes/components/Locations";
|
TEventTypeLocation,
|
||||||
|
} from "@calcom/features/eventtypes/components/locations/Locations";
|
||||||
|
import Locations from "@calcom/features/eventtypes/components/locations/Locations";
|
||||||
import type { LocationFormValues } from "@calcom/features/eventtypes/lib/types";
|
import type { LocationFormValues } from "@calcom/features/eventtypes/lib/types";
|
||||||
import type { SingleValueLocationOption } from "@calcom/features/form/components/LocationSelect";
|
import type { SingleValueLocationOption } from "@calcom/features/form/components/LocationSelect";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import type { SearchParams } from "app/_types";
|
import type { SearchParams } from "app/_types";
|
||||||
import { type Params } from "app/_types";
|
import { type Params } from "app/_types";
|
||||||
|
import type { ReadonlyHeaders, ReadonlyRequestCookies } from "app/_types";
|
||||||
import type { GetServerSidePropsContext, NextApiRequest } from "next";
|
import type { GetServerSidePropsContext, NextApiRequest } from "next";
|
||||||
import { type ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers";
|
|
||||||
import { type ReadonlyRequestCookies } from "next/dist/server/web/spec-extension/adapters/request-cookies";
|
|
||||||
|
|
||||||
const createProxifiedObject = (object: Record<string, string>) =>
|
const createProxifiedObject = (object: Record<string, string>) =>
|
||||||
new Proxy(object, {
|
new Proxy(object, {
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
import { getSession } from "next-auth/react";
|
|
||||||
|
|
||||||
import { UserPermissionRole } from "@calcom/prisma/enums";
|
|
||||||
|
|
||||||
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
|
||||||
const session = await getSession(ctx);
|
|
||||||
if (!session || !session.user) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/login",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAdmin = session.user.role === UserPermissionRole.ADMIN;
|
|
||||||
if (!isAdmin) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
error: null,
|
|
||||||
migrated: null,
|
|
||||||
userId: session.user.id,
|
|
||||||
username: session.user.username,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
import { getSession } from "next-auth/react";
|
|
||||||
|
|
||||||
import { UserPermissionRole } from "@calcom/prisma/enums";
|
|
||||||
|
|
||||||
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
|
||||||
const session = await getSession(ctx);
|
|
||||||
if (!session || !session.user) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/login",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAdmin = session.user.role === UserPermissionRole.ADMIN;
|
|
||||||
if (!isAdmin) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
props: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
import { getSession } from "next-auth/react";
|
|
||||||
|
|
||||||
import { UserPermissionRole } from "@calcom/prisma/enums";
|
|
||||||
|
|
||||||
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
|
||||||
const session = await getSession(ctx);
|
|
||||||
if (!session || !session.user) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/login",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAdmin = session.user.role === UserPermissionRole.ADMIN;
|
|
||||||
if (!isAdmin) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
props: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
import { getSession } from "next-auth/react";
|
|
||||||
|
|
||||||
import { UserPermissionRole } from "@calcom/prisma/enums";
|
|
||||||
|
|
||||||
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
|
||||||
const session = await getSession(ctx);
|
|
||||||
if (!session || !session.user) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/login",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAdmin = session.user.role === UserPermissionRole.ADMIN;
|
|
||||||
if (!isAdmin) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
props: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -156,7 +156,7 @@ export function AvailabilityList({ availabilities }: AvailabilityListProps) {
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-default mb-16 mt-4 hidden text-center text-sm md:block">
|
<div className="text-default mb-16 mt-4 block text-center text-sm">
|
||||||
{t("temporarily_out_of_office")}{" "}
|
{t("temporarily_out_of_office")}{" "}
|
||||||
<Link href="settings/my-account/out-of-office" className="underline">
|
<Link href="settings/my-account/out-of-office" className="underline">
|
||||||
{t("add_a_redirect")}
|
{t("add_a_redirect")}
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ function BookingsContent({ status }: BookingsProps) {
|
|||||||
columnHelper.accessor((row) => row.type === "data" && row.booking.user?.id, {
|
columnHelper.accessor((row) => row.type === "data" && row.booking.user?.id, {
|
||||||
id: "userId",
|
id: "userId",
|
||||||
header: t("member"),
|
header: t("member"),
|
||||||
enableColumnFilter: true,
|
enableColumnFilter: user?.isTeamAdminOrOwner ?? false,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
cell: () => null,
|
cell: () => null,
|
||||||
meta: {
|
meta: {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "5.5.17",
|
"version": "5.6.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
@@ -106,7 +106,7 @@
|
|||||||
"memory-cache": "^0.2.0",
|
"memory-cache": "^0.2.0",
|
||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"next": "^15.3.0",
|
"next": "^15.4.5",
|
||||||
"next-auth": "^4.22.1",
|
"next-auth": "^4.22.1",
|
||||||
"next-axiom": "^0.17.0",
|
"next-axiom": "^0.17.0",
|
||||||
"next-collect": "^0.2.1",
|
"next-collect": "^0.2.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { IncomingMessage } from "http";
|
import type { IncomingMessage } from "http";
|
||||||
|
import type { NextPageContext } from "next";
|
||||||
import { SessionProvider } from "next-auth/react";
|
import { SessionProvider } from "next-auth/react";
|
||||||
import type { AppContextType } from "next/dist/shared/lib/utils";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import CacheProvider from "react-inlinesvg/provider";
|
import CacheProvider from "react-inlinesvg/provider";
|
||||||
|
|
||||||
@@ -32,8 +32,8 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MyApp.getInitialProps = async (ctx: AppContextType) => {
|
MyApp.getInitialProps = async ({ ctx }: { ctx: NextPageContext }) => {
|
||||||
const { req } = ctx.ctx;
|
const { req } = ctx;
|
||||||
|
|
||||||
let newLocale = "en";
|
let newLocale = "en";
|
||||||
|
|
||||||
|
|||||||
@@ -479,7 +479,6 @@ test.describe("Bookings", () => {
|
|||||||
const filterItemsConfig = [
|
const filterItemsConfig = [
|
||||||
{ key: "eventTypeId", name: "Event Type", testId: "add-filter-item-eventTypeId" },
|
{ key: "eventTypeId", name: "Event Type", testId: "add-filter-item-eventTypeId" },
|
||||||
{ key: "teamId", name: "Team", testId: "add-filter-item-teamId" },
|
{ key: "teamId", name: "Team", testId: "add-filter-item-teamId" },
|
||||||
{ key: "userId", name: "Member", testId: "add-filter-item-userId" },
|
|
||||||
{ key: "attendeeName", name: "Attendees Name", testId: "add-filter-item-attendeeName" },
|
{ key: "attendeeName", name: "Attendees Name", testId: "add-filter-item-attendeeName" },
|
||||||
{ key: "attendeeEmail", name: "Attendee Email", testId: "add-filter-item-attendeeEmail" },
|
{ key: "attendeeEmail", name: "Attendee Email", testId: "add-filter-item-attendeeEmail" },
|
||||||
{ key: "dateRange", name: "Date Range", testId: "add-filter-item-dateRange" },
|
{ key: "dateRange", name: "Date Range", testId: "add-filter-item-dateRange" },
|
||||||
@@ -488,8 +487,16 @@ test.describe("Bookings", () => {
|
|||||||
|
|
||||||
const getFilterItemLocator = (page: Page, testId: string) => page.locator(`[data-testid="${testId}"]`);
|
const getFilterItemLocator = (page: Page, testId: string) => page.locator(`[data-testid="${testId}"]`);
|
||||||
|
|
||||||
test.beforeEach(async ({ page, users }) => {
|
const setup = async ({
|
||||||
const user = await users.create();
|
isAdmin,
|
||||||
|
page,
|
||||||
|
users,
|
||||||
|
}: {
|
||||||
|
isAdmin: boolean;
|
||||||
|
page: Page;
|
||||||
|
users: Fixtures["users"];
|
||||||
|
}) => {
|
||||||
|
const user = isAdmin ? await users.create(undefined, { hasTeam: true }) : await users.create();
|
||||||
await user.apiLogin();
|
await user.apiLogin();
|
||||||
const bookingsGetResponse = page.waitForResponse((response) =>
|
const bookingsGetResponse = page.waitForResponse((response) =>
|
||||||
/\/api\/trpc\/bookings\/get.*/.test(response.url())
|
/\/api\/trpc\/bookings\/get.*/.test(response.url())
|
||||||
@@ -498,9 +505,10 @@ test.describe("Bookings", () => {
|
|||||||
await bookingsGetResponse;
|
await bookingsGetResponse;
|
||||||
await page.locator('[data-testid="add-filter-button"]').click();
|
await page.locator('[data-testid="add-filter-button"]').click();
|
||||||
await expect(page.locator(searchInputSelector)).toBeVisible();
|
await expect(page.locator(searchInputSelector)).toBeVisible();
|
||||||
});
|
};
|
||||||
|
|
||||||
test("should show all filter items initially and after clearing search", async ({ page }) => {
|
test("should show all filter items initially and after clearing search", async ({ page, users }) => {
|
||||||
|
await setup({ isAdmin: false, page, users });
|
||||||
const searchInput = page.locator(searchInputSelector);
|
const searchInput = page.locator(searchInputSelector);
|
||||||
|
|
||||||
// Initial check: all defined filter items should be visible
|
// Initial check: all defined filter items should be visible
|
||||||
@@ -524,7 +532,18 @@ test.describe("Bookings", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("search should be case-insensitive", async ({ page }) => {
|
test("should show admin-only filter", async ({ page, users }) => {
|
||||||
|
await setup({ isAdmin: true, page, users });
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
getFilterItemLocator(page, "add-filter-item-userId"),
|
||||||
|
`Item "Member" should be visible initially`
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("search should be case-insensitive", async ({ page, users }) => {
|
||||||
|
await setup({ isAdmin: true, page, users });
|
||||||
|
|
||||||
const searchInput = page.locator(searchInputSelector);
|
const searchInput = page.locator(searchInputSelector);
|
||||||
|
|
||||||
// Search for "member" (lowercase)
|
// Search for "member" (lowercase)
|
||||||
@@ -534,7 +553,8 @@ test.describe("Bookings", () => {
|
|||||||
await expect(getFilterItemLocator(page, "add-filter-item-teamId")).toBeHidden();
|
await expect(getFilterItemLocator(page, "add-filter-item-teamId")).toBeHidden();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should individually find each filter item by its full name", async ({ page }) => {
|
test("should individually find each filter item by its full name", async ({ page, users }) => {
|
||||||
|
await setup({ isAdmin: false, page, users });
|
||||||
const searchInput = page.locator(searchInputSelector);
|
const searchInput = page.locator(searchInputSelector);
|
||||||
|
|
||||||
for (const targetItem of filterItemsConfig) {
|
for (const targetItem of filterItemsConfig) {
|
||||||
@@ -550,7 +570,8 @@ test.describe("Bookings", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should show no items for a non-matching search term", async ({ page }) => {
|
test("should show no items for a non-matching search term", async ({ page, users }) => {
|
||||||
|
await setup({ isAdmin: false, page, users });
|
||||||
const searchInput = page.locator(searchInputSelector);
|
const searchInput = page.locator(searchInputSelector);
|
||||||
const nonExistentTerm = "NonExistentFilterXYZ123";
|
const nonExistentTerm = "NonExistentFilterXYZ123";
|
||||||
await searchInput.fill(nonExistentTerm);
|
await searchInput.fill(nonExistentTerm);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { test } from "./lib/fixtures";
|
|||||||
import {
|
import {
|
||||||
bookTimeSlot,
|
bookTimeSlot,
|
||||||
confirmReschedule,
|
confirmReschedule,
|
||||||
|
confirmBooking,
|
||||||
doOnOrgDomain,
|
doOnOrgDomain,
|
||||||
selectFirstAvailableTimeSlotNextMonth,
|
selectFirstAvailableTimeSlotNextMonth,
|
||||||
selectSecondAvailableTimeSlotNextMonth,
|
selectSecondAvailableTimeSlotNextMonth,
|
||||||
@@ -98,6 +99,58 @@ test("dynamic booking info prefilled by query params", async ({ page, users }) =
|
|||||||
activeState = await listItemLocator.getAttribute("data-active");
|
activeState = await listItemLocator.getAttribute("data-active");
|
||||||
expect(activeState).toEqual("false");
|
expect(activeState).toEqual("false");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("multiple duration selection updates event length correctly", async ({ page, users }) => {
|
||||||
|
const user = await users.create();
|
||||||
|
await user.apiLogin();
|
||||||
|
|
||||||
|
await test.step("update event title to include duration placeholder", async () => {
|
||||||
|
await page.goto("/event-types");
|
||||||
|
await page.waitForSelector('[data-testid="event-types"]');
|
||||||
|
await page.click(`text=Multiple duration`);
|
||||||
|
await page.waitForSelector('[data-testid="event-title"]');
|
||||||
|
await expect(page.getByTestId("vertical-tab-event_setup_tab_title")).toHaveAttribute(
|
||||||
|
"aria-current",
|
||||||
|
"page"
|
||||||
|
);
|
||||||
|
await page.getByTestId("vertical-tab-event_advanced_tab_title").click();
|
||||||
|
await page.fill('[name="eventName"]', "{Event duration} event btwn {Organiser} {Scheduler}");
|
||||||
|
await page.locator('[data-testid="update-eventtype"]').click();
|
||||||
|
await page.waitForResponse("/api/trpc/eventTypes/update?batch=1");
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(`/${user.username}/multiple-duration`);
|
||||||
|
|
||||||
|
await page.locator('[data-testid="multiple-choice-30mins"]').waitFor({ state: "visible" });
|
||||||
|
|
||||||
|
await test.step("verify default 30min duration is selected", async () => {
|
||||||
|
const duration30 = page.getByTestId("multiple-choice-30mins");
|
||||||
|
const activeState = await duration30.getAttribute("data-active");
|
||||||
|
expect(activeState).toEqual("true");
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step("book with 90min duration and verify title", async () => {
|
||||||
|
await page.getByTestId("multiple-choice-90mins").click();
|
||||||
|
|
||||||
|
const duration90 = page.getByTestId("multiple-choice-90mins");
|
||||||
|
const activeState = await duration90.getAttribute("data-active");
|
||||||
|
expect(activeState).toEqual("true");
|
||||||
|
|
||||||
|
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||||
|
|
||||||
|
await page.fill('[name="name"]', "Test User");
|
||||||
|
await page.fill('[name="email"]', "test@example.com");
|
||||||
|
await confirmBooking(page);
|
||||||
|
|
||||||
|
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||||
|
|
||||||
|
const bookingTitle = page.locator('[data-testid="booking-title"]');
|
||||||
|
await expect(bookingTitle).toBeVisible();
|
||||||
|
const titleText = await bookingTitle.textContent();
|
||||||
|
expect(titleText).toContain("90");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line playwright/no-skipped-test
|
// eslint-disable-next-line playwright/no-skipped-test
|
||||||
test.skip("it contains the right event details", async ({ page }) => {
|
test.skip("it contains the right event details", async ({ page }) => {
|
||||||
const response = await page.goto(`http://acme.cal.local:3000/owner1+member1`);
|
const response = await page.goto(`http://acme.cal.local:3000/owner1+member1`);
|
||||||
|
|||||||
+477
-199
@@ -4193,6 +4193,284 @@
|
|||||||
"tags": ["Orgs / Teams / Event Types"]
|
"tags": ["Orgs / Teams / Event Types"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/private-links": {
|
||||||
|
"post": {
|
||||||
|
"operationId": "OrganizationsEventTypesPrivateLinksController_createPrivateLink",
|
||||||
|
"summary": "Create a private link for a team event type",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "Authorization",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For non-platform customers - value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x-cal-secret-key",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For platform customers - OAuth client secret key",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x-cal-client-id",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For platform customers - OAuth client ID",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "teamId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "eventTypeId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CreatePrivateLinkInput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CreatePrivateLinkOutput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": ["Orgs / Teams / Event Types / Private Links"]
|
||||||
|
},
|
||||||
|
"get": {
|
||||||
|
"operationId": "OrganizationsEventTypesPrivateLinksController_getPrivateLinks",
|
||||||
|
"summary": "Get all private links for a team event type",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "Authorization",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For non-platform customers - value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x-cal-secret-key",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For platform customers - OAuth client secret key",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x-cal-client-id",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For platform customers - OAuth client ID",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "teamId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "eventTypeId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/GetPrivateLinksOutput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": ["Orgs / Teams / Event Types / Private Links"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/private-links/{linkId}": {
|
||||||
|
"patch": {
|
||||||
|
"operationId": "OrganizationsEventTypesPrivateLinksController_updatePrivateLink",
|
||||||
|
"summary": "Update a private link for a team event type",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "Authorization",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For non-platform customers - value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x-cal-secret-key",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For platform customers - OAuth client secret key",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x-cal-client-id",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For platform customers - OAuth client ID",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "teamId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "eventTypeId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "linkId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UpdatePrivateLinkOutput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": ["Orgs / Teams / Event Types / Private Links"]
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"operationId": "OrganizationsEventTypesPrivateLinksController_deletePrivateLink",
|
||||||
|
"summary": "Delete a private link for a team event type",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "Authorization",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For non-platform customers - value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x-cal-secret-key",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For platform customers - OAuth client secret key",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x-cal-client-id",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For platform customers - OAuth client ID",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "teamId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "eventTypeId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "linkId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/DeletePrivateLinkOutput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": ["Orgs / Teams / Event Types / Private Links"]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/v2/organizations/{orgId}/teams/{teamId}/memberships": {
|
"/v2/organizations/{orgId}/teams/{teamId}/memberships": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "OrganizationsTeamsMembershipsController_getAllOrgTeamMemberships",
|
"operationId": "OrganizationsTeamsMembershipsController_getAllOrgTeamMemberships",
|
||||||
@@ -21786,6 +22064,188 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"CreatePrivateLinkInput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"expiresAt": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Expiration date for time-based links",
|
||||||
|
"format": "date-time",
|
||||||
|
"example": "2024-12-31T23:59:59.000Z"
|
||||||
|
},
|
||||||
|
"maxUsageCount": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Maximum number of times the link can be used. If omitted and expiresAt is not provided, defaults to 1 (one time use).",
|
||||||
|
"example": 10,
|
||||||
|
"minimum": 1,
|
||||||
|
"default": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"TimeBasedPrivateLinkOutput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"linkId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The private link ID",
|
||||||
|
"example": "abc123def456"
|
||||||
|
},
|
||||||
|
"eventTypeId": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Event type ID this link belongs to",
|
||||||
|
"example": 123
|
||||||
|
},
|
||||||
|
"isExpired": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether the link is currently expired",
|
||||||
|
"example": false
|
||||||
|
},
|
||||||
|
"bookingUrl": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Full booking URL for this private link",
|
||||||
|
"format": "uri",
|
||||||
|
"example": "https://cal.com/d/abc123def456/30min"
|
||||||
|
},
|
||||||
|
"expiresAt": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Expiration date for this time-based link",
|
||||||
|
"format": "date-time",
|
||||||
|
"example": "2025-12-31T23:59:59.000Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["linkId", "eventTypeId", "isExpired", "bookingUrl", "expiresAt"]
|
||||||
|
},
|
||||||
|
"UsageBasedPrivateLinkOutput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"linkId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The private link ID",
|
||||||
|
"example": "abc123def456"
|
||||||
|
},
|
||||||
|
"eventTypeId": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Event type ID this link belongs to",
|
||||||
|
"example": 123
|
||||||
|
},
|
||||||
|
"isExpired": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether the link is currently expired",
|
||||||
|
"example": false
|
||||||
|
},
|
||||||
|
"bookingUrl": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Full booking URL for this private link",
|
||||||
|
"format": "uri",
|
||||||
|
"example": "https://cal.com/d/abc123def456/30min"
|
||||||
|
},
|
||||||
|
"maxUsageCount": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Maximum number of times this link can be used",
|
||||||
|
"example": 10
|
||||||
|
},
|
||||||
|
"usageCount": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Current usage count for this link",
|
||||||
|
"example": 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["linkId", "eventTypeId", "isExpired", "bookingUrl", "maxUsageCount", "usageCount"]
|
||||||
|
},
|
||||||
|
"CreatePrivateLinkOutput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Response status",
|
||||||
|
"example": "success"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"description": "Created private link data (either time-based or usage-based)",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["status", "data"]
|
||||||
|
},
|
||||||
|
"GetPrivateLinksOutput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Response status",
|
||||||
|
"example": "success"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Array of private links for the event type (mix of time-based and usage-based)",
|
||||||
|
"items": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["status", "data"]
|
||||||
|
},
|
||||||
|
"UpdatePrivateLinkOutput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Response status",
|
||||||
|
"example": "success"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"description": "Updated private link data (either time-based or usage-based)",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["status", "data"]
|
||||||
|
},
|
||||||
|
"DeletePrivateLinkOutput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Response status",
|
||||||
|
"example": "success"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Deleted link information",
|
||||||
|
"properties": {
|
||||||
|
"linkId": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "abc123def456"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Private link deleted successfully"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["status", "data"]
|
||||||
|
},
|
||||||
"StripConnectOutputDto": {
|
"StripConnectOutputDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -23368,6 +23828,23 @@
|
|||||||
},
|
},
|
||||||
"required": ["status", "data"]
|
"required": ["status", "data"]
|
||||||
},
|
},
|
||||||
|
"UpdatePrivateLinkBody": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"expiresAt": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string",
|
||||||
|
"description": "New expiration date for time-based links",
|
||||||
|
"example": "2024-12-31T23:59:59.000Z"
|
||||||
|
},
|
||||||
|
"maxUsageCount": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "New maximum number of times the link can be used",
|
||||||
|
"example": 10,
|
||||||
|
"minimum": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"MeOrgOutput": {
|
"MeOrgOutput": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -25757,205 +26234,6 @@
|
|||||||
},
|
},
|
||||||
"required": ["status", "data"]
|
"required": ["status", "data"]
|
||||||
},
|
},
|
||||||
"CreatePrivateLinkInput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"expiresAt": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Expiration date for time-based links",
|
|
||||||
"format": "date-time",
|
|
||||||
"example": "2024-12-31T23:59:59.000Z"
|
|
||||||
},
|
|
||||||
"maxUsageCount": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "Maximum number of times the link can be used. If omitted and expiresAt is not provided, defaults to 1 (one time use).",
|
|
||||||
"example": 10,
|
|
||||||
"minimum": 1,
|
|
||||||
"default": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"TimeBasedPrivateLinkOutput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"linkId": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The private link ID",
|
|
||||||
"example": "abc123def456"
|
|
||||||
},
|
|
||||||
"eventTypeId": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "Event type ID this link belongs to",
|
|
||||||
"example": 123
|
|
||||||
},
|
|
||||||
"isExpired": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Whether the link is currently expired",
|
|
||||||
"example": false
|
|
||||||
},
|
|
||||||
"bookingUrl": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Full booking URL for this private link",
|
|
||||||
"format": "uri",
|
|
||||||
"example": "https://cal.com/d/abc123def456/30min"
|
|
||||||
},
|
|
||||||
"expiresAt": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Expiration date for this time-based link",
|
|
||||||
"format": "date-time",
|
|
||||||
"example": "2025-12-31T23:59:59.000Z"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["linkId", "eventTypeId", "isExpired", "bookingUrl", "expiresAt"]
|
|
||||||
},
|
|
||||||
"UsageBasedPrivateLinkOutput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"linkId": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The private link ID",
|
|
||||||
"example": "abc123def456"
|
|
||||||
},
|
|
||||||
"eventTypeId": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "Event type ID this link belongs to",
|
|
||||||
"example": 123
|
|
||||||
},
|
|
||||||
"isExpired": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Whether the link is currently expired",
|
|
||||||
"example": false
|
|
||||||
},
|
|
||||||
"bookingUrl": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Full booking URL for this private link",
|
|
||||||
"format": "uri",
|
|
||||||
"example": "https://cal.com/d/abc123def456/30min"
|
|
||||||
},
|
|
||||||
"maxUsageCount": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "Maximum number of times this link can be used",
|
|
||||||
"example": 10
|
|
||||||
},
|
|
||||||
"usageCount": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "Current usage count for this link",
|
|
||||||
"example": 3
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["linkId", "eventTypeId", "isExpired", "bookingUrl", "maxUsageCount", "usageCount"]
|
|
||||||
},
|
|
||||||
"CreatePrivateLinkOutput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"status": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Response status",
|
|
||||||
"example": "success"
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"description": "Created private link data (either time-based or usage-based)",
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["status", "data"]
|
|
||||||
},
|
|
||||||
"GetPrivateLinksOutput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"status": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Response status",
|
|
||||||
"example": "success"
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"type": "array",
|
|
||||||
"description": "Array of private links for the event type (mix of time-based and usage-based)",
|
|
||||||
"items": {
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["status", "data"]
|
|
||||||
},
|
|
||||||
"UpdatePrivateLinkBody": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"expiresAt": {
|
|
||||||
"format": "date-time",
|
|
||||||
"type": "string",
|
|
||||||
"description": "New expiration date for time-based links",
|
|
||||||
"example": "2024-12-31T23:59:59.000Z"
|
|
||||||
},
|
|
||||||
"maxUsageCount": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "New maximum number of times the link can be used",
|
|
||||||
"example": 10,
|
|
||||||
"minimum": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"UpdatePrivateLinkOutput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"status": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Response status",
|
|
||||||
"example": "success"
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"description": "Updated private link data (either time-based or usage-based)",
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["status", "data"]
|
|
||||||
},
|
|
||||||
"DeletePrivateLinkOutput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"status": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Response status",
|
|
||||||
"example": "success"
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"type": "object",
|
|
||||||
"description": "Deleted link information",
|
|
||||||
"properties": {
|
|
||||||
"linkId": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "abc123def456"
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "Private link deleted successfully"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["status", "data"]
|
|
||||||
},
|
|
||||||
"UserWebhookOutputDto": {
|
"UserWebhookOutputDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ Below is a list of props that can be passed to the booker atom.
|
|||||||
| onTimeslotsLoaded | No | Callback function triggered once the available timeslots have been fetched. |
|
| onTimeslotsLoaded | No | Callback function triggered once the available timeslots have been fetched. |
|
||||||
| roundRobinHideOrgAndTeam | No | Boolean indicating if the organization and team should be hidden in the booker atom sidebar for round robin scheduling type, defaults to false. |
|
| roundRobinHideOrgAndTeam | No | Boolean indicating if the organization and team should be hidden in the booker atom sidebar for round robin scheduling type, defaults to false. |
|
||||||
| showNoAvailabilityDialog | No | Boolean indicating if the no availability dialog should be shown, defaults to true. |
|
| showNoAvailabilityDialog | No | Boolean indicating if the no availability dialog should be shown, defaults to true. |
|
||||||
|
| silentlyHandleCalendarFailures | No | Boolean when true the booker still displays slots when the third party calendars credentials are invalid or expired, Booker may show stale availability when enabled |
|
||||||
|
|
||||||
## Styling
|
## Styling
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ Sometimes, yarn install might fail during deployment on Vercel, in which case, y
|
|||||||
<Step title="Override the build command">
|
<Step title="Override the build command">
|
||||||
Override to:
|
Override to:
|
||||||
```
|
```
|
||||||
cd ../.. && yarn build --include-dependencies --no-deps
|
cd ../.. && yarn build --no-deps
|
||||||
```
|
```
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Hit Deploy">
|
<Step title="Hit Deploy">
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ RUN set -eux; \
|
|||||||
apt-get clean && \
|
apt-get clean && \
|
||||||
rm -rf /var/lib/apt/lists /var/cache/apt/archives && \
|
rm -rf /var/lib/apt/lists /var/cache/apt/archives && \
|
||||||
yarn config set httpTimeout 1200000 && \
|
yarn config set httpTimeout 1200000 && \
|
||||||
npx turbo prune --scope=@calcom/web --docker && \
|
npx turbo prune --filter=@calcom/web --docker && \
|
||||||
npx turbo prune --scope=@calcom/api --docker && \
|
npx turbo prune --filter=@calcom/api --docker && \
|
||||||
yarn install && \
|
yarn install && \
|
||||||
yarn turbo run build --filter=@calcom/api
|
yarn turbo run build --filter=@calcom/api
|
||||||
|
|
||||||
|
|||||||
+13
-13
@@ -28,23 +28,23 @@
|
|||||||
"edit-app-template": "yarn app-store edit-template",
|
"edit-app-template": "yarn app-store edit-template",
|
||||||
"delete-app-template": "yarn app-store delete-template",
|
"delete-app-template": "yarn app-store delete-template",
|
||||||
"build": "turbo run build --filter=@calcom/web...",
|
"build": "turbo run build --filter=@calcom/web...",
|
||||||
"build:ai": "turbo run build --scope=\"@calcom/ai\"",
|
"build:ai": "turbo run build --filter=\"@calcom/ai\"",
|
||||||
"clean": "find . -name node_modules -o -name .next -o -name .turbo -o -name dist -type d -prune | xargs rm -rf",
|
"clean": "find . -name node_modules -o -name .next -o -name .turbo -o -name dist -type d -prune | xargs rm -rf",
|
||||||
"db-deploy": "turbo run db-deploy",
|
"db-deploy": "turbo run db-deploy",
|
||||||
"db-seed": "turbo run db-seed",
|
"db-seed": "turbo run db-seed",
|
||||||
"db-studio": "yarn prisma studio",
|
"db-studio": "yarn prisma studio",
|
||||||
"deploy": "turbo run deploy",
|
"deploy": "turbo run deploy",
|
||||||
"dev:all": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/website\" --scope=\"@calcom/console\"",
|
"dev:all": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/website\" --filter=\"@calcom/console\"",
|
||||||
"dev:ai": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/api-proxy\" --scope=\"@calcom/api\" --scope=\"@calcom/ai\"",
|
"dev:ai": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/api-proxy\" --filter=\"@calcom/api\" --filter=\"@calcom/ai\"",
|
||||||
"dev:api": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/api-proxy\" --scope=\"@calcom/api\"",
|
"dev:api": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/api-proxy\" --filter=\"@calcom/api\"",
|
||||||
"dev:api:console": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/api-proxy\" --scope=\"@calcom/api\" --scope=\"@calcom/console\"",
|
"dev:api:console": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/api-proxy\" --filter=\"@calcom/api\" --filter=\"@calcom/console\"",
|
||||||
"dev:console": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/console\"",
|
"dev:console": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/console\"",
|
||||||
"dev:swagger": "turbo run dev --scope=\"@calcom/api-proxy\" --scope=\"@calcom/api\" --scope=\"@calcom/swagger\"",
|
"dev:swagger": "turbo run dev --filter=\"@calcom/api-proxy\" --filter=\"@calcom/api\" --filter=\"@calcom/swagger\"",
|
||||||
"dev:website": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/website\"",
|
"dev:website": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/website\"",
|
||||||
"dev": "turbo run dev --scope=\"@calcom/web\"",
|
"dev": "turbo run dev --filter=\"@calcom/web\"",
|
||||||
"build-storybook": "turbo run build --scope=\"@calcom/storybook\"",
|
"build-storybook": "turbo run build --filter=\"@calcom/storybook\"",
|
||||||
"dx": "turbo run dx",
|
"dx": "turbo run dx",
|
||||||
"i-dev": "infisical run -- turbo run dev --scope=\"@calcom/web\"",
|
"i-dev": "infisical run -- turbo run dev --filter=\"@calcom/web\"",
|
||||||
"i-dx": "infisical run -- turbo run dx",
|
"i-dx": "infisical run -- turbo run dx",
|
||||||
"i-gen-web-example-env": "infisical secrets generate-example-env --tags=web > .env.example",
|
"i-gen-web-example-env": "infisical secrets generate-example-env --tags=web > .env.example",
|
||||||
"i-gen-app-store-example-env": "infisical secrets generate-example-env --tags=appstore > .env.appStore.example",
|
"i-gen-app-store-example-env": "infisical secrets generate-example-env --tags=appstore > .env.appStore.example",
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
"pre-commit": "lint-staged",
|
"pre-commit": "lint-staged",
|
||||||
"predev": "echo 'Checking env files'",
|
"predev": "echo 'Checking env files'",
|
||||||
"prisma": "yarn workspace @calcom/prisma prisma",
|
"prisma": "yarn workspace @calcom/prisma prisma",
|
||||||
"start": "turbo run start --scope=\"@calcom/web\"",
|
"start": "turbo run start --filter=\"@calcom/web\"",
|
||||||
"tdd": "vitest watch",
|
"tdd": "vitest watch",
|
||||||
"e2e": "NEXT_PUBLIC_IS_E2E=1 yarn playwright test --project=@calcom/web",
|
"e2e": "NEXT_PUBLIC_IS_E2E=1 yarn playwright test --project=@calcom/web",
|
||||||
"e2e:app-store": "QUICK=true yarn playwright test --project=@calcom/app-store",
|
"e2e:app-store": "QUICK=true yarn playwright test --project=@calcom/app-store",
|
||||||
@@ -124,7 +124,7 @@
|
|||||||
"date-fns-tz": "^3.2.0",
|
"date-fns-tz": "^3.2.0",
|
||||||
"eslint": "^8.34.0",
|
"eslint": "^8.34.0",
|
||||||
"p-limit": "^6.2.0",
|
"p-limit": "^6.2.0",
|
||||||
"turbo": "^1.10.1"
|
"turbo": "^2.5.5"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"types-ramda": "0.29.4",
|
"types-ramda": "0.29.4",
|
||||||
|
|||||||
@@ -661,22 +661,30 @@ export default class Office365CalendarService implements Calendar {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
private handleErrorJsonOffice365Calendar = <Type>(response: Response): Promise<Type | string> => {
|
private async handleErrorJsonOffice365Calendar<Type>(response: Response): Promise<Type | string> {
|
||||||
if (response.headers.get("content-encoding") === "gzip") {
|
if (response.headers.get("content-encoding") === "gzip") {
|
||||||
return response.text();
|
return response.text();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status === 204) {
|
if (response.status === 204) {
|
||||||
return new Promise((resolve) => resolve({} as Type));
|
return {} as Type;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response.ok && response.status < 200 && response.status >= 300) {
|
if (!response.ok) {
|
||||||
response.json().then(console.log);
|
let errorBody: string | object;
|
||||||
throw Error(response.statusText);
|
try {
|
||||||
|
errorBody = await response.json();
|
||||||
|
} catch (e) {
|
||||||
|
errorBody = await response.text();
|
||||||
|
}
|
||||||
|
this.log.error(
|
||||||
|
`handleErrorJsonOffice365Calendar: Office365 API request failed with status ${response.status}`,
|
||||||
|
errorBody
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json();
|
return response.json();
|
||||||
};
|
}
|
||||||
|
|
||||||
async getMainTimeZone(): Promise<string> {
|
async getMainTimeZone(): Promise<string> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -9,6 +9,21 @@ function _fireEvent(fullName: string, detail: CustomEventDetail) {
|
|||||||
window.dispatchEvent(event);
|
window.dispatchEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BaseBookingEventPayload = {
|
||||||
|
title: string | undefined;
|
||||||
|
startTime: string | undefined;
|
||||||
|
endTime: string | undefined;
|
||||||
|
eventTypeId: number | null | undefined;
|
||||||
|
status: string | undefined;
|
||||||
|
paymentRequired: boolean;
|
||||||
|
isRecurring: boolean;
|
||||||
|
/**
|
||||||
|
* This is only used for recurring bookings
|
||||||
|
*/
|
||||||
|
allBookings?: { startTime: string; endTime: string }[];
|
||||||
|
videoCallUrl?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type EventDataMap = {
|
export type EventDataMap = {
|
||||||
eventTypeSelected: {
|
eventTypeSelected: {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@@ -24,20 +39,8 @@ export type EventDataMap = {
|
|||||||
linkReady: Record<string, never>;
|
linkReady: Record<string, never>;
|
||||||
__connectInitiated: Record<string, never>;
|
__connectInitiated: Record<string, never>;
|
||||||
__connectCompleted: Record<string, never>;
|
__connectCompleted: Record<string, never>;
|
||||||
bookingSuccessfulV2: {
|
bookingSuccessfulV2: BaseBookingEventPayload & {
|
||||||
uid: string | undefined;
|
uid: string | undefined;
|
||||||
title: string | undefined;
|
|
||||||
startTime: string | undefined;
|
|
||||||
endTime: string | undefined;
|
|
||||||
eventTypeId: number | null | undefined;
|
|
||||||
status: string | undefined;
|
|
||||||
paymentRequired: boolean;
|
|
||||||
isRecurring: boolean;
|
|
||||||
/**
|
|
||||||
* This is only used for recurring bookings
|
|
||||||
*/
|
|
||||||
allBookings?: { startTime: string; endTime: string }[];
|
|
||||||
videoCallUrl?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -56,20 +59,11 @@ export type EventDataMap = {
|
|||||||
};
|
};
|
||||||
confirmed: boolean;
|
confirmed: boolean;
|
||||||
};
|
};
|
||||||
rescheduleBookingSuccessfulV2: {
|
rescheduleBookingSuccessfulV2: BaseBookingEventPayload & {
|
||||||
uid: string | undefined;
|
uid: string | undefined;
|
||||||
title: string | undefined;
|
|
||||||
startTime: string | undefined;
|
|
||||||
endTime: string | undefined;
|
|
||||||
eventTypeId: number | null | undefined;
|
|
||||||
status: string | undefined;
|
|
||||||
paymentRequired: boolean;
|
|
||||||
isRecurring: boolean;
|
|
||||||
/**
|
|
||||||
* This is only used for recurring bookings
|
|
||||||
*/
|
|
||||||
allBookings?: { startTime: string; endTime: string }[];
|
|
||||||
};
|
};
|
||||||
|
dryRunBookingSuccessfulV2: BaseBookingEventPayload;
|
||||||
|
dryRunRescheduleBookingSuccessfulV2: BaseBookingEventPayload;
|
||||||
/**
|
/**
|
||||||
* @deprecated Use `rescheduleBookingSuccessfulV2` instead. We restrict the data heavily there, only sending what is absolutely needed and keeping it light as well. Plus, more importantly that can be documented well.
|
* @deprecated Use `rescheduleBookingSuccessfulV2` instead. We restrict the data heavily there, only sending what is absolutely needed and keeping it light as well. Plus, more importantly that can be documented well.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import { parse } from "accept-language-parser";
|
|||||||
import { lookup } from "bcp-47-match";
|
import { lookup } from "bcp-47-match";
|
||||||
import type { GetTokenParams } from "next-auth/jwt";
|
import type { GetTokenParams } from "next-auth/jwt";
|
||||||
import { getToken } from "next-auth/jwt";
|
import { getToken } from "next-auth/jwt";
|
||||||
import { type ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers";
|
|
||||||
import { type ReadonlyRequestCookies } from "next/dist/server/web/spec-extension/adapters/request-cookies";
|
|
||||||
|
|
||||||
import { i18n } from "@calcom/config/next-i18next.config";
|
import { i18n } from "@calcom/config/next-i18next.config";
|
||||||
|
|
||||||
|
type ReadonlyHeaders = Awaited<ReturnType<typeof import("next/headers").headers>>;
|
||||||
|
type ReadonlyRequestCookies = Awaited<ReturnType<typeof import("next/headers").cookies>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is a slimmed down version of the `getServerSession` function from
|
* This is a slimmed down version of the `getServerSession` function from
|
||||||
* `next-auth`.
|
* `next-auth`.
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import dayjs from "@calcom/dayjs";
|
|||||||
import PoweredBy from "@calcom/ee/components/PoweredBy";
|
import PoweredBy from "@calcom/ee/components/PoweredBy";
|
||||||
import { updateEmbedBookerState } from "@calcom/embed-core/src/embed-iframe";
|
import { updateEmbedBookerState } from "@calcom/embed-core/src/embed-iframe";
|
||||||
import TurnstileCaptcha from "@calcom/features/auth/Turnstile";
|
import TurnstileCaptcha from "@calcom/features/auth/Turnstile";
|
||||||
|
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||||
import useSkipConfirmStep from "@calcom/features/bookings/Booker/components/hooks/useSkipConfirmStep";
|
import useSkipConfirmStep from "@calcom/features/bookings/Booker/components/hooks/useSkipConfirmStep";
|
||||||
import { getQueryParam } from "@calcom/features/bookings/Booker/utils/query-param";
|
import { getQueryParam } from "@calcom/features/bookings/Booker/utils/query-param";
|
||||||
import { useNonEmptyScheduleDays } from "@calcom/features/schedules/lib/use-schedule/useNonEmptyScheduleDays";
|
import { useNonEmptyScheduleDays } from "@calcom/features/schedules/lib/use-schedule/useNonEmptyScheduleDays";
|
||||||
@@ -39,7 +40,6 @@ import { NotFound } from "./components/Unavailable";
|
|||||||
import { useIsQuickAvailabilityCheckFeatureEnabled } from "./components/hooks/useIsQuickAvailabilityCheckFeatureEnabled";
|
import { useIsQuickAvailabilityCheckFeatureEnabled } from "./components/hooks/useIsQuickAvailabilityCheckFeatureEnabled";
|
||||||
import { fadeInLeft, getBookerSizeClassNames, useBookerResizeAnimation } from "./config";
|
import { fadeInLeft, getBookerSizeClassNames, useBookerResizeAnimation } from "./config";
|
||||||
import framerFeatures from "./framer-features";
|
import framerFeatures from "./framer-features";
|
||||||
import { useBookerStore } from "./store";
|
|
||||||
import type { BookerProps, WrappedBookerProps } from "./types";
|
import type { BookerProps, WrappedBookerProps } from "./types";
|
||||||
import { isBookingDryRun } from "./utils/isBookingDryRun";
|
import { isBookingDryRun } from "./utils/isBookingDryRun";
|
||||||
import { isTimeSlotAvailable } from "./utils/isTimeslotAvailable";
|
import { isTimeSlotAvailable } from "./utils/isTimeslotAvailable";
|
||||||
@@ -85,9 +85,14 @@ const BookerComponent = ({
|
|||||||
}: BookerProps & WrappedBookerProps) => {
|
}: BookerProps & WrappedBookerProps) => {
|
||||||
const searchParams = useCompatSearchParams();
|
const searchParams = useCompatSearchParams();
|
||||||
const isPlatformBookerEmbed = useIsPlatformBookerEmbed();
|
const isPlatformBookerEmbed = useIsPlatformBookerEmbed();
|
||||||
const [bookerState, setBookerState] = useBookerStore((state) => [state.state, state.setState], shallow);
|
const [bookerState, setBookerState] = useBookerStoreContext(
|
||||||
|
(state) => [state.state, state.setState],
|
||||||
|
shallow
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedDate = useBookerStoreContext((state) => state.selectedDate);
|
||||||
|
const setSelectedDate = useBookerStoreContext((state) => state.setSelectedDate);
|
||||||
|
|
||||||
const selectedDate = useBookerStore((state) => state.selectedDate);
|
|
||||||
const {
|
const {
|
||||||
shouldShowFormInDialog,
|
shouldShowFormInDialog,
|
||||||
hasDarkBackground,
|
hasDarkBackground,
|
||||||
@@ -100,12 +105,15 @@ const BookerComponent = ({
|
|||||||
bookerLayouts,
|
bookerLayouts,
|
||||||
} = bookerLayout;
|
} = bookerLayout;
|
||||||
|
|
||||||
const [seatedEventData, setSeatedEventData] = useBookerStore(
|
const [seatedEventData, setSeatedEventData] = useBookerStoreContext(
|
||||||
(state) => [state.seatedEventData, state.setSeatedEventData],
|
(state) => [state.seatedEventData, state.setSeatedEventData],
|
||||||
shallow
|
shallow
|
||||||
);
|
);
|
||||||
const { selectedTimeslot, setSelectedTimeslot, allSelectedTimeslots } = slots;
|
const { selectedTimeslot, setSelectedTimeslot, allSelectedTimeslots } = slots;
|
||||||
const [dayCount, setDayCount] = useBookerStore((state) => [state.dayCount, state.setDayCount], shallow);
|
const [dayCount, setDayCount] = useBookerStoreContext(
|
||||||
|
(state) => [state.dayCount, state.setDayCount],
|
||||||
|
shallow
|
||||||
|
);
|
||||||
|
|
||||||
const nonEmptyScheduleDays = useNonEmptyScheduleDays(schedule?.data?.slots).filter(
|
const nonEmptyScheduleDays = useNonEmptyScheduleDays(schedule?.data?.slots).filter(
|
||||||
(slot) => dayjs(selectedDate).diff(slot, "day") <= 0
|
(slot) => dayjs(selectedDate).diff(slot, "day") <= 0
|
||||||
@@ -184,6 +192,10 @@ const BookerComponent = ({
|
|||||||
(bookerState === "booking" || (bookerState === "selecting_time" && skipConfirmStep))
|
(bookerState === "booking" || (bookerState === "selecting_time" && skipConfirmStep))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onAvailableTimeSlotSelect = (time: string) => {
|
||||||
|
setSelectedTimeslot(time);
|
||||||
|
};
|
||||||
|
|
||||||
updateEmbedBookerState({ bookerState, slotsQuery: schedule });
|
updateEmbedBookerState({ bookerState, slotsQuery: schedule });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -220,6 +232,7 @@ const BookerComponent = ({
|
|||||||
return bookerState === "booking" ? (
|
return bookerState === "booking" ? (
|
||||||
<BookEventForm
|
<BookEventForm
|
||||||
key={key}
|
key={key}
|
||||||
|
timeslot={selectedTimeslot}
|
||||||
shouldRenderCaptcha={shouldRenderCaptcha}
|
shouldRenderCaptcha={shouldRenderCaptcha}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
setSelectedTimeslot(null);
|
setSelectedTimeslot(null);
|
||||||
@@ -312,9 +325,7 @@ const BookerComponent = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{event.data && !isPlatform ? <BookingPageTagManager eventType={event.data} /> : <></>}
|
{event.data && !isPlatform ? <BookingPageTagManager eventType={event.data} /> : <></>}
|
||||||
|
|
||||||
{(isBookingDryRunProp || isBookingDryRun(searchParams)) && <DryRunMessage isEmbed={isEmbed} />}
|
{(isBookingDryRunProp || isBookingDryRun(searchParams)) && <DryRunMessage isEmbed={isEmbed} />}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
// In a popup embed, if someone clicks outside the main(having main class or main tag), it closes the embed
|
// In a popup embed, if someone clicks outside the main(having main class or main tag), it closes the embed
|
||||||
@@ -394,6 +405,7 @@ const BookerComponent = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<EventMeta
|
<EventMeta
|
||||||
|
selectedTimeslot={selectedTimeslot}
|
||||||
classNames={{
|
classNames={{
|
||||||
eventMetaContainer: customClassNames?.eventMetaCustomClassNames?.eventMetaContainer,
|
eventMetaContainer: customClassNames?.eventMetaCustomClassNames?.eventMetaContainer,
|
||||||
eventMetaTitle: customClassNames?.eventMetaCustomClassNames?.eventMetaTitle,
|
eventMetaTitle: customClassNames?.eventMetaCustomClassNames?.eventMetaTitle,
|
||||||
@@ -480,6 +492,7 @@ const BookerComponent = ({
|
|||||||
ref={timeslotsRef}
|
ref={timeslotsRef}
|
||||||
{...fadeInLeft}>
|
{...fadeInLeft}>
|
||||||
<AvailableTimeSlots
|
<AvailableTimeSlots
|
||||||
|
onAvailableTimeSlotSelect={onAvailableTimeSlotSelect}
|
||||||
customClassNames={customClassNames?.availableTimeSlotsCustomClassNames}
|
customClassNames={customClassNames?.availableTimeSlotsCustomClassNames}
|
||||||
extraDays={extraDays}
|
extraDays={extraDays}
|
||||||
limitHeight={layout === BookerLayouts.MONTH_VIEW}
|
limitHeight={layout === BookerLayouts.MONTH_VIEW}
|
||||||
@@ -552,7 +565,6 @@ const BookerComponent = ({
|
|||||||
</m.span>
|
</m.span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<>
|
<>
|
||||||
{verifyCode && formEmail ? (
|
{verifyCode && formEmail ? (
|
||||||
<VerifyCodeDialog
|
<VerifyCodeDialog
|
||||||
@@ -571,7 +583,6 @@ const BookerComponent = ({
|
|||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
||||||
<BookFormAsModal
|
<BookFormAsModal
|
||||||
onCancel={() => setSelectedTimeslot(null)}
|
onCancel={() => setSelectedTimeslot(null)}
|
||||||
visible={bookerState === "booking" && shouldShowFormInDialog}>
|
visible={bookerState === "booking" && shouldShowFormInDialog}>
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { createContext, useContext, useRef, type ReactNode, useEffect } from "react";
|
||||||
|
import { useStore } from "zustand";
|
||||||
|
import type { StoreApi } from "zustand";
|
||||||
|
|
||||||
|
import { createBookerStore, type BookerStore, type StoreInitializeType } from "./store";
|
||||||
|
|
||||||
|
export const BookerStoreContext = createContext<StoreApi<BookerStore> | null>(null);
|
||||||
|
|
||||||
|
export interface BookerStoreProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BookerStoreProvider = ({ children }: BookerStoreProviderProps) => {
|
||||||
|
const storeRef = useRef<StoreApi<BookerStore>>();
|
||||||
|
if (!storeRef.current) {
|
||||||
|
storeRef.current = createBookerStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
return <BookerStoreContext.Provider value={storeRef.current}>{children}</BookerStoreContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useBookerStoreContext = <T,>(
|
||||||
|
selector: (store: BookerStore) => T,
|
||||||
|
equalityFn?: (a: T, b: T) => boolean
|
||||||
|
): T => {
|
||||||
|
const bookerStoreContext = useContext(BookerStoreContext);
|
||||||
|
|
||||||
|
if (!bookerStoreContext) {
|
||||||
|
throw new Error("useBookerStoreContext must be used within BookerStoreProvider");
|
||||||
|
}
|
||||||
|
|
||||||
|
return useStore(bookerStoreContext, selector, equalityFn);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useInitializeBookerStoreContext = ({
|
||||||
|
username,
|
||||||
|
eventSlug,
|
||||||
|
month,
|
||||||
|
eventId,
|
||||||
|
rescheduleUid = null,
|
||||||
|
rescheduledBy = null,
|
||||||
|
bookingData = null,
|
||||||
|
verifiedEmail = null,
|
||||||
|
layout,
|
||||||
|
isTeamEvent,
|
||||||
|
durationConfig,
|
||||||
|
org,
|
||||||
|
isInstantMeeting,
|
||||||
|
timezone = null,
|
||||||
|
teamMemberEmail,
|
||||||
|
crmOwnerRecordType,
|
||||||
|
crmAppSlug,
|
||||||
|
crmRecordId,
|
||||||
|
isPlatform = false,
|
||||||
|
allowUpdatingUrlParams = true,
|
||||||
|
}: StoreInitializeType) => {
|
||||||
|
const bookerStoreContext = useContext(BookerStoreContext);
|
||||||
|
|
||||||
|
if (!bookerStoreContext) {
|
||||||
|
throw new Error("useInitializeBookerStoreContext must be used within BookerStoreProvider");
|
||||||
|
}
|
||||||
|
|
||||||
|
const initializeStore = useStore(bookerStoreContext, (state) => state.initialize);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initializeStore({
|
||||||
|
username,
|
||||||
|
eventSlug,
|
||||||
|
month,
|
||||||
|
eventId,
|
||||||
|
rescheduleUid,
|
||||||
|
rescheduledBy,
|
||||||
|
bookingData,
|
||||||
|
layout,
|
||||||
|
isTeamEvent,
|
||||||
|
org,
|
||||||
|
verifiedEmail,
|
||||||
|
durationConfig,
|
||||||
|
isInstantMeeting,
|
||||||
|
timezone,
|
||||||
|
teamMemberEmail,
|
||||||
|
crmOwnerRecordType,
|
||||||
|
crmAppSlug,
|
||||||
|
crmRecordId,
|
||||||
|
isPlatform,
|
||||||
|
allowUpdatingUrlParams,
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
initializeStore,
|
||||||
|
org,
|
||||||
|
username,
|
||||||
|
eventSlug,
|
||||||
|
month,
|
||||||
|
eventId,
|
||||||
|
rescheduleUid,
|
||||||
|
rescheduledBy,
|
||||||
|
bookingData,
|
||||||
|
layout,
|
||||||
|
isTeamEvent,
|
||||||
|
verifiedEmail,
|
||||||
|
durationConfig,
|
||||||
|
isInstantMeeting,
|
||||||
|
timezone,
|
||||||
|
teamMemberEmail,
|
||||||
|
crmOwnerRecordType,
|
||||||
|
crmAppSlug,
|
||||||
|
crmRecordId,
|
||||||
|
isPlatform,
|
||||||
|
allowUpdatingUrlParams,
|
||||||
|
]);
|
||||||
|
};
|
||||||
@@ -10,18 +10,17 @@ import "@calcom/features/bookings/Booker/components/__mocks__/Section";
|
|||||||
import { constantsScenarios } from "@calcom/lib/__mocks__/constants";
|
import { constantsScenarios } from "@calcom/lib/__mocks__/constants";
|
||||||
import "@calcom/lib/__mocks__/logger";
|
import "@calcom/lib/__mocks__/logger";
|
||||||
|
|
||||||
import { render, screen } from "@testing-library/react";
|
import React from "react";
|
||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
|
|
||||||
import "@calcom/dayjs/__mocks__";
|
import "@calcom/dayjs/__mocks__";
|
||||||
import "@calcom/features/auth/Turnstile";
|
import "@calcom/features/auth/Turnstile";
|
||||||
|
|
||||||
import { Booker } from "../Booker";
|
import { Booker } from "../Booker";
|
||||||
import { useBookerStore } from "../store";
|
import { render, screen } from "./test-utils";
|
||||||
import type { BookerState } from "../types";
|
|
||||||
|
|
||||||
vi.mock("framer-motion", async (importOriginal) => {
|
vi.mock("framer-motion", async (importOriginal) => {
|
||||||
const actual = await importOriginal();
|
const actual = (await importOriginal()) as any;
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
};
|
};
|
||||||
@@ -87,27 +86,6 @@ vi.mock("@calcom/atoms/hooks/useIsPlatform", () => ({
|
|||||||
useIsPlatform: () => false,
|
useIsPlatform: () => false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Update mockStoreState to include all required state
|
|
||||||
const mockStoreState = {
|
|
||||||
state: "booking" as BookerState,
|
|
||||||
setState: vi.fn(),
|
|
||||||
selectedDate: "2024-01-01",
|
|
||||||
seatedEventData: {},
|
|
||||||
setSeatedEventData: vi.fn(),
|
|
||||||
tentativeSelectedTimeslots: [],
|
|
||||||
setTentativeSelectedTimeslots: vi.fn(),
|
|
||||||
dayCount: 7,
|
|
||||||
setDayCount: vi.fn(),
|
|
||||||
setSelectedTimeslot: vi.fn(),
|
|
||||||
selectedTimeslot: null,
|
|
||||||
formStep: 0,
|
|
||||||
setFormStep: vi.fn(),
|
|
||||||
bookerState: "booking",
|
|
||||||
setBookerState: vi.fn(),
|
|
||||||
layout: "default",
|
|
||||||
setLayout: vi.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update defaultProps to include missing required props
|
// Update defaultProps to include missing required props
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
username: "testuser",
|
username: "testuser",
|
||||||
@@ -188,7 +166,7 @@ const defaultProps = {
|
|||||||
describe("Booker", () => {
|
describe("Booker", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
constantsScenarios.set({
|
constantsScenarios.set({
|
||||||
PUBLIC_QUICK_AVAILABILITY_ROLLOUT: 100,
|
PUBLIC_QUICK_AVAILABILITY_ROLLOUT: "100",
|
||||||
POWERED_BY_URL: "https://go.cal.com/booking",
|
POWERED_BY_URL: "https://go.cal.com/booking",
|
||||||
APP_NAME: "Cal.com",
|
APP_NAME: "Cal.com",
|
||||||
});
|
});
|
||||||
@@ -196,24 +174,13 @@ describe("Booker", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should render null when in loading state", () => {
|
it("should render null when in loading state", () => {
|
||||||
useBookerStore.setState({
|
const { container } = render(<Booker {...defaultProps} />, {
|
||||||
...mockStoreState,
|
mockStore: { state: "loading" },
|
||||||
state: "loading",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { container } = render(<Booker {...defaultProps} />);
|
|
||||||
expect(container).toBeEmptyDOMElement();
|
expect(container).toBeEmptyDOMElement();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render DryRunMessage when in dry run mode", () => {
|
it("should render DryRunMessage when in dry run mode", () => {
|
||||||
useBookerStore.setState({
|
|
||||||
...mockStoreState,
|
|
||||||
state: "selecting_time",
|
|
||||||
selectedDate: "2024-01-01",
|
|
||||||
selectedTimeslot: "2024-01-01T10:00:00Z",
|
|
||||||
tentativeSelectedTimeslots: ["2024-01-01T10:00:00Z"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const propsWithDryRun = {
|
const propsWithDryRun = {
|
||||||
...defaultProps,
|
...defaultProps,
|
||||||
isBookingDryRun: true,
|
isBookingDryRun: true,
|
||||||
@@ -226,7 +193,14 @@ describe("Booker", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
render(<Booker {...propsWithDryRun} />);
|
render(<Booker {...propsWithDryRun} />, {
|
||||||
|
mockStore: {
|
||||||
|
state: "selecting_time",
|
||||||
|
selectedDate: "2024-01-01",
|
||||||
|
selectedTimeslot: "2024-01-01T10:00:00Z",
|
||||||
|
tentativeSelectedTimeslots: ["2024-01-01T10:00:00Z"],
|
||||||
|
},
|
||||||
|
});
|
||||||
expect(screen.getByTestId("dry-run-message")).toBeInTheDocument();
|
expect(screen.getByTestId("dry-run-message")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -242,12 +216,10 @@ describe("Booker", () => {
|
|||||||
invalidate: mockInvalidate,
|
invalidate: mockInvalidate,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
useBookerStore.setState({
|
|
||||||
...mockStoreState,
|
|
||||||
state: "booking",
|
|
||||||
});
|
|
||||||
|
|
||||||
render(<Booker {...propsWithInvalidate} />);
|
render(<Booker {...propsWithInvalidate} />, {
|
||||||
|
mockStore: { state: "booking" },
|
||||||
|
});
|
||||||
screen.logTestingPlaygroundURL();
|
screen.logTestingPlaygroundURL();
|
||||||
// Trigger form cancel
|
// Trigger form cancel
|
||||||
const cancelButton = screen.getByRole("button", { name: /cancel/i });
|
const cancelButton = screen.getByRole("button", { name: /cancel/i });
|
||||||
@@ -267,12 +239,9 @@ describe("Booker", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
useBookerStore.setState({
|
render(<Booker {...propsWithQuickChecks} />, {
|
||||||
...mockStoreState,
|
mockStore: { state: "booking" },
|
||||||
state: "booking",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
render(<Booker {...propsWithQuickChecks} />);
|
|
||||||
const bookEventForm = screen.getByTestId("book-event-form");
|
const bookEventForm = screen.getByTestId("book-event-form");
|
||||||
await expect(bookEventForm).toHaveAttribute("data-unavailable", "true");
|
await expect(bookEventForm).toHaveAttribute("data-unavailable", "true");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { render } from "@testing-library/react";
|
||||||
|
import type { RenderOptions } from "@testing-library/react";
|
||||||
|
import React from "react";
|
||||||
|
import type { ReactElement } from "react";
|
||||||
|
import { vi } from "vitest";
|
||||||
|
import type { StoreApi } from "zustand";
|
||||||
|
|
||||||
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import { BookerLayouts } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
|
import { BookerStoreContext } from "../BookerStoreProvider";
|
||||||
|
import type { BookerStore } from "../store";
|
||||||
|
|
||||||
|
interface CustomRenderOptions extends Omit<RenderOptions, "wrapper"> {
|
||||||
|
mockStore?: Partial<BookerStore>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createMockStore = (initialState?: Partial<BookerStore>): StoreApi<BookerStore> => {
|
||||||
|
let state: BookerStore = {
|
||||||
|
username: null,
|
||||||
|
eventSlug: null,
|
||||||
|
eventId: null,
|
||||||
|
verifiedEmail: null,
|
||||||
|
setVerifiedEmail: vi.fn(),
|
||||||
|
month: dayjs().format("YYYY-MM"),
|
||||||
|
setMonth: vi.fn(),
|
||||||
|
state: "loading",
|
||||||
|
setState: vi.fn(),
|
||||||
|
layout: BookerLayouts.MONTH_VIEW,
|
||||||
|
setLayout: vi.fn(),
|
||||||
|
selectedDate: null,
|
||||||
|
setSelectedDate: vi.fn(),
|
||||||
|
addToSelectedDate: vi.fn(),
|
||||||
|
selectedDatesAndTimes: null,
|
||||||
|
setSelectedDatesAndTimes: vi.fn(),
|
||||||
|
durationConfig: null,
|
||||||
|
selectedDuration: null,
|
||||||
|
setSelectedDuration: vi.fn(),
|
||||||
|
selectedTimeslot: null,
|
||||||
|
setSelectedTimeslot: vi.fn(),
|
||||||
|
tentativeSelectedTimeslots: [],
|
||||||
|
setTentativeSelectedTimeslots: vi.fn(),
|
||||||
|
recurringEventCount: null,
|
||||||
|
setRecurringEventCount: vi.fn(),
|
||||||
|
occurenceCount: null,
|
||||||
|
setOccurenceCount: vi.fn(),
|
||||||
|
dayCount: null,
|
||||||
|
setDayCount: vi.fn(),
|
||||||
|
rescheduleUid: null,
|
||||||
|
rescheduledBy: null,
|
||||||
|
bookingUid: null,
|
||||||
|
bookingData: null,
|
||||||
|
setBookingData: vi.fn(),
|
||||||
|
initialize: vi.fn(),
|
||||||
|
formValues: {},
|
||||||
|
setFormValues: vi.fn(),
|
||||||
|
isTeamEvent: false,
|
||||||
|
seatedEventData: {},
|
||||||
|
setSeatedEventData: vi.fn(),
|
||||||
|
isInstantMeeting: false,
|
||||||
|
org: null,
|
||||||
|
setOrg: vi.fn(),
|
||||||
|
timezone: null,
|
||||||
|
setTimezone: vi.fn(),
|
||||||
|
teamMemberEmail: null,
|
||||||
|
crmOwnerRecordType: null,
|
||||||
|
crmAppSlug: null,
|
||||||
|
crmRecordId: null,
|
||||||
|
isPlatform: false,
|
||||||
|
allowUpdatingUrlParams: true,
|
||||||
|
...initialState,
|
||||||
|
};
|
||||||
|
|
||||||
|
state.setMonth = vi.fn((month: string | null) => {
|
||||||
|
state.month = month;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
getState: () => state,
|
||||||
|
setState: vi.fn((updater) => {
|
||||||
|
if (typeof updater === "function") {
|
||||||
|
const newState = updater(state);
|
||||||
|
state = { ...state, ...newState };
|
||||||
|
} else {
|
||||||
|
state = { ...state, ...updater };
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
subscribe: vi.fn(),
|
||||||
|
destroy: vi.fn(),
|
||||||
|
} as unknown as StoreApi<BookerStore>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const renderWithBookerStore = (ui: ReactElement, options?: CustomRenderOptions) => {
|
||||||
|
const mockStore = createMockStore(options?.mockStore);
|
||||||
|
|
||||||
|
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<BookerStoreContext.Provider value={mockStore}>{children}</BookerStoreContext.Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
return render(ui, { wrapper: Wrapper, ...options });
|
||||||
|
};
|
||||||
|
|
||||||
|
export * from "@testing-library/react";
|
||||||
|
|
||||||
|
export { renderWithBookerStore as render };
|
||||||
@@ -2,6 +2,7 @@ import { useCallback, useMemo, useRef } from "react";
|
|||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { AvailableTimes, AvailableTimesSkeleton } from "@calcom/features/bookings";
|
import { AvailableTimes, AvailableTimesSkeleton } from "@calcom/features/bookings";
|
||||||
|
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||||
import type { IUseBookingLoadingStates } from "@calcom/features/bookings/Booker/components/hooks/useBookings";
|
import type { IUseBookingLoadingStates } from "@calcom/features/bookings/Booker/components/hooks/useBookings";
|
||||||
import type { BookerEvent } from "@calcom/features/bookings/types";
|
import type { BookerEvent } from "@calcom/features/bookings/types";
|
||||||
import type { Slot } from "@calcom/features/schedules/lib/use-schedule/types";
|
import type { Slot } from "@calcom/features/schedules/lib/use-schedule/types";
|
||||||
@@ -13,7 +14,6 @@ import { BookerLayouts } from "@calcom/prisma/zod-utils";
|
|||||||
import classNames from "@calcom/ui/classNames";
|
import classNames from "@calcom/ui/classNames";
|
||||||
|
|
||||||
import { AvailableTimesHeader } from "../../components/AvailableTimesHeader";
|
import { AvailableTimesHeader } from "../../components/AvailableTimesHeader";
|
||||||
import { useBookerStore } from "../store";
|
|
||||||
import type { useScheduleForEventReturnType } from "../utils/event";
|
import type { useScheduleForEventReturnType } from "../utils/event";
|
||||||
import { getQueryParam } from "../utils/query-param";
|
import { getQueryParam } from "../utils/query-param";
|
||||||
|
|
||||||
@@ -49,6 +49,7 @@ type AvailableTimeSlotsProps = {
|
|||||||
*/
|
*/
|
||||||
unavailableTimeSlots: string[];
|
unavailableTimeSlots: string[];
|
||||||
confirmButtonDisabled?: boolean;
|
confirmButtonDisabled?: boolean;
|
||||||
|
onAvailableTimeSlotSelect: (time: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -72,17 +73,17 @@ export const AvailableTimeSlots = ({
|
|||||||
unavailableTimeSlots,
|
unavailableTimeSlots,
|
||||||
confirmButtonDisabled,
|
confirmButtonDisabled,
|
||||||
confirmStepClassNames,
|
confirmStepClassNames,
|
||||||
|
onAvailableTimeSlotSelect,
|
||||||
...props
|
...props
|
||||||
}: AvailableTimeSlotsProps) => {
|
}: AvailableTimeSlotsProps) => {
|
||||||
const selectedDate = useBookerStore((state) => state.selectedDate);
|
const selectedDate = useBookerStoreContext((state) => state.selectedDate);
|
||||||
|
|
||||||
const setSelectedTimeslot = useBookerStore((state) => state.setSelectedTimeslot);
|
const setSeatedEventData = useBookerStoreContext((state) => state.setSeatedEventData);
|
||||||
const setSeatedEventData = useBookerStore((state) => state.setSeatedEventData);
|
|
||||||
const date = selectedDate || dayjs().format("YYYY-MM-DD");
|
const date = selectedDate || dayjs().format("YYYY-MM-DD");
|
||||||
const [layout] = useBookerStore((state) => [state.layout]);
|
const [layout] = useBookerStoreContext((state) => [state.layout]);
|
||||||
const isColumnView = layout === BookerLayouts.COLUMN_VIEW;
|
const isColumnView = layout === BookerLayouts.COLUMN_VIEW;
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const { setTentativeSelectedTimeslots, tentativeSelectedTimeslots } = useBookerStore((state) => ({
|
const { setTentativeSelectedTimeslots, tentativeSelectedTimeslots } = useBookerStoreContext((state) => ({
|
||||||
setTentativeSelectedTimeslots: state.setTentativeSelectedTimeslots,
|
setTentativeSelectedTimeslots: state.setTentativeSelectedTimeslots,
|
||||||
tentativeSelectedTimeslots: state.tentativeSelectedTimeslots,
|
tentativeSelectedTimeslots: state.tentativeSelectedTimeslots,
|
||||||
}));
|
}));
|
||||||
@@ -146,7 +147,9 @@ export const AvailableTimeSlots = ({
|
|||||||
showAvailableSeatsCount,
|
showAvailableSeatsCount,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setSelectedTimeslot(time);
|
|
||||||
|
onAvailableTimeSlotSelect(time);
|
||||||
|
|
||||||
const isTimeSlotAvailable = !unavailableTimeSlots.includes(time);
|
const isTimeSlotAvailable = !unavailableTimeSlots.includes(time);
|
||||||
if (skipConfirmStep && isTimeSlotAvailable) {
|
if (skipConfirmStep && isTimeSlotAvailable) {
|
||||||
onSubmit(time);
|
onSubmit(time);
|
||||||
@@ -156,12 +159,12 @@ export const AvailableTimeSlots = ({
|
|||||||
[
|
[
|
||||||
onSubmit,
|
onSubmit,
|
||||||
setSeatedEventData,
|
setSeatedEventData,
|
||||||
setSelectedTimeslot,
|
|
||||||
skipConfirmStep,
|
skipConfirmStep,
|
||||||
showAvailableSeatsCount,
|
showAvailableSeatsCount,
|
||||||
unavailableTimeSlots,
|
unavailableTimeSlots,
|
||||||
schedule,
|
schedule,
|
||||||
setTentativeSelectedTimeslots,
|
setTentativeSelectedTimeslots,
|
||||||
|
onAvailableTimeSlotSelect,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useMemo, useState } from "react";
|
|||||||
import type { FieldError } from "react-hook-form";
|
import type { FieldError } from "react-hook-form";
|
||||||
|
|
||||||
import { useIsPlatformBookerEmbed } from "@calcom/atoms/hooks/useIsPlatformBookerEmbed";
|
import { useIsPlatformBookerEmbed } from "@calcom/atoms/hooks/useIsPlatformBookerEmbed";
|
||||||
|
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||||
import type { BookerEvent } from "@calcom/features/bookings/types";
|
import type { BookerEvent } from "@calcom/features/bookings/types";
|
||||||
import ServerTrans from "@calcom/lib/components/ServerTrans";
|
import ServerTrans from "@calcom/lib/components/ServerTrans";
|
||||||
import { WEBSITE_PRIVACY_POLICY_URL, WEBSITE_TERMS_URL } from "@calcom/lib/constants";
|
import { WEBSITE_PRIVACY_POLICY_URL, WEBSITE_TERMS_URL } from "@calcom/lib/constants";
|
||||||
@@ -16,7 +17,6 @@ import { Button } from "@calcom/ui/components/button";
|
|||||||
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
|
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
|
||||||
import { Form } from "@calcom/ui/components/form";
|
import { Form } from "@calcom/ui/components/form";
|
||||||
|
|
||||||
import { useBookerStore } from "../../store";
|
|
||||||
import { formatEventFromTime } from "../../utils/dates";
|
import { formatEventFromTime } from "../../utils/dates";
|
||||||
import { useBookerTime } from "../hooks/useBookerTime";
|
import { useBookerTime } from "../hooks/useBookerTime";
|
||||||
import type { UseBookingFormReturnType } from "../hooks/useBookingForm";
|
import type { UseBookingFormReturnType } from "../hooks/useBookingForm";
|
||||||
@@ -43,6 +43,7 @@ type BookEventFormProps = {
|
|||||||
confirmButton?: string;
|
confirmButton?: string;
|
||||||
backButton?: string;
|
backButton?: string;
|
||||||
};
|
};
|
||||||
|
timeslot: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BookEventForm = ({
|
export const BookEventForm = ({
|
||||||
@@ -62,6 +63,7 @@ export const BookEventForm = ({
|
|||||||
shouldRenderCaptcha,
|
shouldRenderCaptcha,
|
||||||
confirmButtonDisabled,
|
confirmButtonDisabled,
|
||||||
classNames,
|
classNames,
|
||||||
|
timeslot,
|
||||||
}: Omit<BookEventFormProps, "event"> & {
|
}: Omit<BookEventFormProps, "event"> & {
|
||||||
eventQuery: {
|
eventQuery: {
|
||||||
isError: boolean;
|
isError: boolean;
|
||||||
@@ -70,12 +72,11 @@ export const BookEventForm = ({
|
|||||||
};
|
};
|
||||||
}) => {
|
}) => {
|
||||||
const eventType = eventQuery.data;
|
const eventType = eventQuery.data;
|
||||||
const setFormValues = useBookerStore((state) => state.setFormValues);
|
const setFormValues = useBookerStoreContext((state) => state.setFormValues);
|
||||||
const bookingData = useBookerStore((state) => state.bookingData);
|
const bookingData = useBookerStoreContext((state) => state.bookingData);
|
||||||
const rescheduleUid = useBookerStore((state) => state.rescheduleUid);
|
const rescheduleUid = useBookerStoreContext((state) => state.rescheduleUid);
|
||||||
const timeslot = useBookerStore((state) => state.selectedTimeslot);
|
const username = useBookerStoreContext((state) => state.username);
|
||||||
const username = useBookerStore((state) => state.username);
|
const isInstantMeeting = useBookerStoreContext((state) => state.isInstantMeeting);
|
||||||
const isInstantMeeting = useBookerStore((state) => state.isInstantMeeting);
|
|
||||||
const isPlatformBookerEmbed = useIsPlatformBookerEmbed();
|
const isPlatformBookerEmbed = useIsPlatformBookerEmbed();
|
||||||
const { timeFormat, timezone } = useBookerTime();
|
const { timeFormat, timezone } = useBookerTime();
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import React from "react";
|
|||||||
|
|
||||||
import { useEventTypeById } from "@calcom/atoms/hooks/event-types/private/useEventTypeById";
|
import { useEventTypeById } from "@calcom/atoms/hooks/event-types/private/useEventTypeById";
|
||||||
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
|
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
|
||||||
|
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||||
import { Dialog } from "@calcom/features/components/controlled-dialog";
|
import { Dialog } from "@calcom/features/components/controlled-dialog";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Badge } from "@calcom/ui/components/badge";
|
import { Badge } from "@calcom/ui/components/badge";
|
||||||
import { DialogContent } from "@calcom/ui/components/dialog";
|
import { DialogContent } from "@calcom/ui/components/dialog";
|
||||||
|
|
||||||
import { getDurationFormatted } from "../../../components/event-meta/Duration";
|
import { getDurationFormatted } from "../../../components/event-meta/Duration";
|
||||||
import { useBookerStore } from "../../store";
|
|
||||||
import { FromTime } from "../../utils/dates";
|
import { FromTime } from "../../utils/dates";
|
||||||
import { useEvent } from "../../utils/event";
|
import { useEvent } from "../../utils/event";
|
||||||
import { useBookerTime } from "../hooks/useBookerTime";
|
import { useBookerTime } from "../hooks/useBookerTime";
|
||||||
@@ -27,7 +27,7 @@ const PlatformBookEventFormWrapper = ({
|
|||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}) => {
|
}) => {
|
||||||
const eventId = useBookerStore((state) => state.eventId);
|
const eventId = useBookerStoreContext((state) => state.eventId);
|
||||||
const { data } = useEventTypeById(eventId);
|
const { data } = useEventTypeById(eventId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -44,8 +44,8 @@ export const BookEventFormWrapperComponent = ({
|
|||||||
eventLength?: number;
|
eventLength?: number;
|
||||||
}) => {
|
}) => {
|
||||||
const { i18n, t } = useLocale();
|
const { i18n, t } = useLocale();
|
||||||
const selectedTimeslot = useBookerStore((state) => state.selectedTimeslot);
|
const selectedTimeslot = useBookerStoreContext((state) => state.selectedTimeslot);
|
||||||
const selectedDuration = useBookerStore((state) => state.selectedDuration);
|
const selectedDuration = useBookerStoreContext((state) => state.selectedDuration);
|
||||||
const { timeFormat, timezone } = useBookerTime();
|
const { timeFormat, timezone } = useBookerTime();
|
||||||
if (!selectedTimeslot) {
|
if (!selectedTimeslot) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { render } from "@testing-library/react";
|
import React from "react";
|
||||||
import { vi, afterEach } from "vitest";
|
import { vi, afterEach } from "vitest";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { DatePicker as DatePickerComponent } from "@calcom/features/calendars/DatePicker";
|
import { DatePicker as DatePickerComponent } from "@calcom/features/calendars/DatePicker";
|
||||||
|
|
||||||
|
import { render } from "../__tests__/test-utils";
|
||||||
import { DatePicker } from "./DatePicker";
|
import { DatePicker } from "./DatePicker";
|
||||||
|
|
||||||
vi.mock("@calcom/features/calendars/DatePicker", () => {
|
vi.mock("@calcom/features/calendars/DatePicker", () => {
|
||||||
@@ -18,7 +19,7 @@ const noop = () => {
|
|||||||
|
|
||||||
describe("Tests for DatePicker Component", () => {
|
describe("Tests for DatePicker Component", () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
DatePickerComponent.mockClear();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("It passes the loading prop to the DatePicker component", async () => {
|
test("It passes the loading prop to the DatePicker component", async () => {
|
||||||
@@ -60,7 +61,7 @@ describe("Tests for DatePicker Component", () => {
|
|||||||
|
|
||||||
test("when there are only slots in the next month, skip the current month", async () => {
|
test("when there are only slots in the next month, skip the current month", async () => {
|
||||||
// there'll be one slot open on this day, next month.
|
// there'll be one slot open on this day, next month.
|
||||||
const slotDate = dayjs().add("1", "month");
|
const slotDate = dayjs().add(1, "month");
|
||||||
render(
|
render(
|
||||||
<DatePicker
|
<DatePicker
|
||||||
event={{}}
|
event={{}}
|
||||||
@@ -72,7 +73,12 @@ describe("Tests for DatePicker Component", () => {
|
|||||||
],
|
],
|
||||||
}}
|
}}
|
||||||
isLoading={false}
|
isLoading={false}
|
||||||
/>
|
/>,
|
||||||
|
{
|
||||||
|
mockStore: {
|
||||||
|
month: slotDate.format("YYYY-MM"), // Start with next month to simulate the auto-advance
|
||||||
|
},
|
||||||
|
}
|
||||||
);
|
);
|
||||||
// slot date is next month, so it'll have skipped the current month
|
// slot date is next month, so it'll have skipped the current month
|
||||||
// by setting the browsingDate to the next month.
|
// by setting the browsingDate to the next month.
|
||||||
@@ -94,13 +100,18 @@ describe("Tests for DatePicker Component", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
isLoading={false}
|
isLoading={false}
|
||||||
/>
|
/>,
|
||||||
|
{
|
||||||
|
mockStore: {
|
||||||
|
month: dayjs().add(1, "month").format("YYYY-MM"), // Start with next month to simulate the auto-advance
|
||||||
|
},
|
||||||
|
}
|
||||||
);
|
);
|
||||||
// slot date is next month, so it'll have skipped the current month
|
// slot date is next month, so it'll have skipped the current month
|
||||||
// by setting the browsingDate to the next month.
|
// by setting the browsingDate to the next month.
|
||||||
expect(DatePickerComponent).toHaveBeenCalledWith(
|
expect(DatePickerComponent).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
browsingDate: dayjs().add("1", "month").startOf("month"),
|
browsingDate: dayjs().add(1, "month").startOf("month"),
|
||||||
}),
|
}),
|
||||||
expect.anything()
|
expect.anything()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { shallow } from "zustand/shallow";
|
|||||||
|
|
||||||
import type { Dayjs } from "@calcom/dayjs";
|
import type { Dayjs } from "@calcom/dayjs";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||||
import type { DatePickerClassNames } from "@calcom/features/bookings/Booker/types";
|
import type { DatePickerClassNames } from "@calcom/features/bookings/Booker/types";
|
||||||
import { DatePicker as DatePickerComponent } from "@calcom/features/calendars/DatePicker";
|
import { DatePicker as DatePickerComponent } from "@calcom/features/calendars/DatePicker";
|
||||||
import { useNonEmptyScheduleDays } from "@calcom/features/schedules/lib/use-schedule/useNonEmptyScheduleDays";
|
import { useNonEmptyScheduleDays } from "@calcom/features/schedules/lib/use-schedule/useNonEmptyScheduleDays";
|
||||||
@@ -11,7 +12,6 @@ import type { User } from "@calcom/prisma/client";
|
|||||||
import type { PeriodData } from "@calcom/types/Event";
|
import type { PeriodData } from "@calcom/types/Event";
|
||||||
|
|
||||||
import type { Slots } from "../../types";
|
import type { Slots } from "../../types";
|
||||||
import { useBookerStore } from "../store";
|
|
||||||
|
|
||||||
const useMoveToNextMonthOnNoAvailability = ({
|
const useMoveToNextMonthOnNoAvailability = ({
|
||||||
browsingDate,
|
browsingDate,
|
||||||
@@ -76,12 +76,12 @@ export const DatePicker = ({
|
|||||||
showNoAvailabilityDialog?: boolean;
|
showNoAvailabilityDialog?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { i18n } = useLocale();
|
const { i18n } = useLocale();
|
||||||
const [month, selectedDate, layout] = useBookerStore(
|
const [month, selectedDate, layout] = useBookerStoreContext(
|
||||||
(state) => [state.month, state.selectedDate, state.layout],
|
(state) => [state.month, state.selectedDate, state.layout],
|
||||||
shallow
|
shallow
|
||||||
);
|
);
|
||||||
|
|
||||||
const [setSelectedDate, setMonth, setDayCount] = useBookerStore(
|
const [setSelectedDate, setMonth, setDayCount] = useBookerStoreContext(
|
||||||
(state) => [state.setSelectedDate, state.setMonth, state.setDayCount],
|
(state) => [state.setSelectedDate, state.setMonth, state.setDayCount],
|
||||||
shallow
|
shallow
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { shallow } from "zustand/shallow";
|
|||||||
import { Timezone as PlatformTimezoneSelect } from "@calcom/atoms/timezone";
|
import { Timezone as PlatformTimezoneSelect } from "@calcom/atoms/timezone";
|
||||||
import { useEmbedUiConfig, useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
import { useEmbedUiConfig, useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||||
import { EventDetails, EventMembers, EventMetaSkeleton, EventTitle } from "@calcom/features/bookings";
|
import { EventDetails, EventMembers, EventMetaSkeleton, EventTitle } from "@calcom/features/bookings";
|
||||||
|
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||||
import type { Timezone } from "@calcom/features/bookings/Booker/types";
|
import type { Timezone } from "@calcom/features/bookings/Booker/types";
|
||||||
import { SeatsAvailabilityText } from "@calcom/features/bookings/components/SeatsAvailabilityText";
|
import { SeatsAvailabilityText } from "@calcom/features/bookings/components/SeatsAvailabilityText";
|
||||||
import { EventMetaBlock } from "@calcom/features/bookings/components/event-meta/Details";
|
import { EventMetaBlock } from "@calcom/features/bookings/components/event-meta/Details";
|
||||||
@@ -19,7 +20,6 @@ import { EventTypeAutoTranslatedField } from "@calcom/prisma/enums";
|
|||||||
|
|
||||||
import i18nConfigration from "../../../../../i18n.json";
|
import i18nConfigration from "../../../../../i18n.json";
|
||||||
import { fadeInUp } from "../config";
|
import { fadeInUp } from "../config";
|
||||||
import { useBookerStore } from "../store";
|
|
||||||
import { FromToTime } from "../utils/dates";
|
import { FromToTime } from "../utils/dates";
|
||||||
import { useBookerTime } from "./hooks/useBookerTime";
|
import { useBookerTime } from "./hooks/useBookerTime";
|
||||||
|
|
||||||
@@ -54,6 +54,7 @@ export const EventMeta = ({
|
|||||||
locale,
|
locale,
|
||||||
timeZones,
|
timeZones,
|
||||||
children,
|
children,
|
||||||
|
selectedTimeslot,
|
||||||
roundRobinHideOrgAndTeam,
|
roundRobinHideOrgAndTeam,
|
||||||
}: {
|
}: {
|
||||||
event?: Pick<
|
event?: Pick<
|
||||||
@@ -91,17 +92,17 @@ export const EventMeta = ({
|
|||||||
locale?: string | null;
|
locale?: string | null;
|
||||||
timeZones?: Timezone[];
|
timeZones?: Timezone[];
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
selectedTimeslot: string | null;
|
||||||
roundRobinHideOrgAndTeam?: boolean;
|
roundRobinHideOrgAndTeam?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { timeFormat, timezone } = useBookerTime();
|
const { timeFormat, timezone } = useBookerTime();
|
||||||
const [setTimezone] = useTimePreferences((state) => [state.setTimezone]);
|
const [setTimezone] = useTimePreferences((state) => [state.setTimezone]);
|
||||||
const [setBookerStoreTimezone] = useBookerStore((state) => [state.setTimezone], shallow);
|
const [setBookerStoreTimezone] = useBookerStoreContext((state) => [state.setTimezone], shallow);
|
||||||
const selectedDuration = useBookerStore((state) => state.selectedDuration);
|
const selectedDuration = useBookerStoreContext((state) => state.selectedDuration);
|
||||||
const selectedTimeslot = useBookerStore((state) => state.selectedTimeslot);
|
const bookerState = useBookerStoreContext((state) => state.state);
|
||||||
const bookerState = useBookerStore((state) => state.state);
|
const bookingData = useBookerStoreContext((state) => state.bookingData);
|
||||||
const bookingData = useBookerStore((state) => state.bookingData);
|
const rescheduleUid = useBookerStoreContext((state) => state.rescheduleUid);
|
||||||
const rescheduleUid = useBookerStore((state) => state.rescheduleUid);
|
const [seatedEventData, setSeatedEventData] = useBookerStoreContext(
|
||||||
const [seatedEventData, setSeatedEventData] = useBookerStore(
|
|
||||||
(state) => [state.seatedEventData, state.setSeatedEventData],
|
(state) => [state.seatedEventData, state.setSeatedEventData],
|
||||||
shallow
|
shallow
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { shallow } from "zustand/shallow";
|
|||||||
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
|
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||||
|
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { BookerLayouts } from "@calcom/prisma/zod-utils";
|
import { BookerLayouts } from "@calcom/prisma/zod-utils";
|
||||||
@@ -14,7 +15,6 @@ import { Icon } from "@calcom/ui/components/icon";
|
|||||||
import { Tooltip } from "@calcom/ui/components/tooltip";
|
import { Tooltip } from "@calcom/ui/components/tooltip";
|
||||||
|
|
||||||
import { TimeFormatToggle } from "../../components/TimeFormatToggle";
|
import { TimeFormatToggle } from "../../components/TimeFormatToggle";
|
||||||
import { useBookerStore } from "../store";
|
|
||||||
import type { BookerLayout } from "../types";
|
import type { BookerLayout } from "../types";
|
||||||
|
|
||||||
export function Header({
|
export function Header({
|
||||||
@@ -36,10 +36,10 @@ export function Header({
|
|||||||
}) {
|
}) {
|
||||||
const { t, i18n } = useLocale();
|
const { t, i18n } = useLocale();
|
||||||
const isEmbed = useIsEmbed();
|
const isEmbed = useIsEmbed();
|
||||||
const [layout, setLayout] = useBookerStore((state) => [state.layout, state.setLayout], shallow);
|
const [layout, setLayout] = useBookerStoreContext((state) => [state.layout, state.setLayout], shallow);
|
||||||
const selectedDateString = useBookerStore((state) => state.selectedDate);
|
const selectedDateString = useBookerStoreContext((state) => state.selectedDate);
|
||||||
const setSelectedDate = useBookerStore((state) => state.setSelectedDate);
|
const setSelectedDate = useBookerStoreContext((state) => state.setSelectedDate);
|
||||||
const addToSelectedDate = useBookerStore((state) => state.addToSelectedDate);
|
const addToSelectedDate = useBookerStoreContext((state) => state.addToSelectedDate);
|
||||||
const isMonthView = layout === BookerLayouts.MONTH_VIEW;
|
const isMonthView = layout === BookerLayouts.MONTH_VIEW;
|
||||||
const today = dayjs();
|
const today = dayjs();
|
||||||
const selectedDate = selectedDateString ? dayjs(selectedDateString) : today;
|
const selectedDate = selectedDateString ? dayjs(selectedDateString) : today;
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { useMemo, useEffect } from "react";
|
import { useMemo, useEffect } from "react";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||||
import type { BookerEvent } from "@calcom/features/bookings/types";
|
import type { BookerEvent } from "@calcom/features/bookings/types";
|
||||||
import { Calendar } from "@calcom/features/calendars/weeklyview";
|
import { Calendar } from "@calcom/features/calendars/weeklyview";
|
||||||
import type { CalendarEvent } from "@calcom/features/calendars/weeklyview/types/events";
|
import type { CalendarEvent } from "@calcom/features/calendars/weeklyview/types/events";
|
||||||
import type { CalendarAvailableTimeslots } from "@calcom/features/calendars/weeklyview/types/state";
|
import type { CalendarAvailableTimeslots } from "@calcom/features/calendars/weeklyview/types/state";
|
||||||
import { localStorage } from "@calcom/lib/webstorage";
|
import { localStorage } from "@calcom/lib/webstorage";
|
||||||
|
|
||||||
import { useBookerStore } from "../store";
|
|
||||||
import type { useScheduleForEventReturnType } from "../utils/event";
|
import type { useScheduleForEventReturnType } from "../utils/event";
|
||||||
import { getQueryParam } from "../utils/query-param";
|
import { getQueryParam } from "../utils/query-param";
|
||||||
import { useOverlayCalendarStore } from "./OverlayCalendar/store";
|
import { useOverlayCalendarStore } from "./OverlayCalendar/store";
|
||||||
@@ -25,9 +25,9 @@ export const LargeCalendar = ({
|
|||||||
data?: Pick<BookerEvent, "length"> | null;
|
data?: Pick<BookerEvent, "length"> | null;
|
||||||
};
|
};
|
||||||
}) => {
|
}) => {
|
||||||
const selectedDate = useBookerStore((state) => state.selectedDate);
|
const selectedDate = useBookerStoreContext((state) => state.selectedDate);
|
||||||
const setSelectedTimeslot = useBookerStore((state) => state.setSelectedTimeslot);
|
const setSelectedTimeslot = useBookerStoreContext((state) => state.setSelectedTimeslot);
|
||||||
const selectedEventDuration = useBookerStore((state) => state.selectedDuration);
|
const selectedEventDuration = useBookerStoreContext((state) => state.selectedDuration);
|
||||||
const overlayEvents = useOverlayCalendarStore((state) => state.overlayBusyDates);
|
const overlayEvents = useOverlayCalendarStore((state) => state.overlayBusyDates);
|
||||||
const displayOverlay =
|
const displayOverlay =
|
||||||
getQueryParam("overlayCalendar") === "true" || localStorage?.getItem("overlayCalendarSwitchDefault");
|
getQueryParam("overlayCalendar") === "true" || localStorage?.getItem("overlayCalendarSwitchDefault");
|
||||||
|
|||||||
+4
-4
@@ -1,11 +1,11 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Switch } from "@calcom/ui/components/form";
|
|
||||||
import { Button } from "@calcom/ui/components/button";
|
|
||||||
import classNames from "@calcom/ui/classNames";
|
import classNames from "@calcom/ui/classNames";
|
||||||
|
import { Button } from "@calcom/ui/components/button";
|
||||||
|
import { Switch } from "@calcom/ui/components/form";
|
||||||
|
|
||||||
import { useBookerStore } from "../../store";
|
|
||||||
import { useOverlayCalendarStore } from "./store";
|
import { useOverlayCalendarStore } from "./store";
|
||||||
|
|
||||||
interface OverlayCalendarSwitchProps {
|
interface OverlayCalendarSwitchProps {
|
||||||
@@ -20,7 +20,7 @@ export function OverlayCalendarSwitch({ enabled, hasSession, onStateChange }: Ov
|
|||||||
const setCalendarSettingsOverlay = useOverlayCalendarStore(
|
const setCalendarSettingsOverlay = useOverlayCalendarStore(
|
||||||
(state) => state.setCalendarSettingsOverlayModal
|
(state) => state.setCalendarSettingsOverlayModal
|
||||||
);
|
);
|
||||||
const layout = useBookerStore((state) => state.layout);
|
const layout = useBookerStoreContext((state) => state.layout);
|
||||||
const switchEnabled = enabled;
|
const switchEnabled = enabled;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import type { MotionProps } from "framer-motion";
|
|||||||
import { m } from "framer-motion";
|
import { m } from "framer-motion";
|
||||||
import { forwardRef } from "react";
|
import { forwardRef } from "react";
|
||||||
|
|
||||||
|
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||||
import classNames from "@calcom/ui/classNames";
|
import classNames from "@calcom/ui/classNames";
|
||||||
|
|
||||||
import { useBookerStore } from "../store";
|
|
||||||
import type { BookerAreas, BookerLayout } from "../types";
|
import type { BookerAreas, BookerLayout } from "../types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,7 +44,7 @@ export const BookerSection = forwardRef<HTMLDivElement, BookerSectionProps>(func
|
|||||||
{ children, area, visible, className, ...props },
|
{ children, area, visible, className, ...props },
|
||||||
ref
|
ref
|
||||||
) {
|
) {
|
||||||
const layout = useBookerStore((state) => state.layout);
|
const layout = useBookerStoreContext((state) => state.layout);
|
||||||
let gridClassName: string;
|
let gridClassName: string;
|
||||||
|
|
||||||
if (typeof area === "string") {
|
if (typeof area === "string") {
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { useEffect, useRef } from "react";
|
|||||||
import { shallow } from "zustand/shallow";
|
import { shallow } from "zustand/shallow";
|
||||||
|
|
||||||
import { useEmbedType, useEmbedUiConfig, useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
import { useEmbedType, useEmbedUiConfig, useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||||
|
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||||
import type { BookerEvent } from "@calcom/features/bookings/types";
|
import type { BookerEvent } from "@calcom/features/bookings/types";
|
||||||
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
|
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
|
||||||
import type { BookerLayouts } from "@calcom/prisma/zod-utils";
|
import type { BookerLayouts } from "@calcom/prisma/zod-utils";
|
||||||
import { defaultBookerLayoutSettings } from "@calcom/prisma/zod-utils";
|
import { defaultBookerLayoutSettings } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
import { extraDaysConfig } from "../../config";
|
import { extraDaysConfig } from "../../config";
|
||||||
import { useBookerStore } from "../../store";
|
|
||||||
import type { BookerLayout } from "../../types";
|
import type { BookerLayout } from "../../types";
|
||||||
import { validateLayout } from "../../utils/layout";
|
import { validateLayout } from "../../utils/layout";
|
||||||
import { getQueryParam } from "../../utils/query-param";
|
import { getQueryParam } from "../../utils/query-param";
|
||||||
@@ -18,7 +18,7 @@ export type UseBookerLayoutType = ReturnType<typeof useBookerLayout>;
|
|||||||
export const useBookerLayout = (
|
export const useBookerLayout = (
|
||||||
profileBookerLayouts: BookerEvent["profile"]["bookerLayouts"] | undefined | null
|
profileBookerLayouts: BookerEvent["profile"]["bookerLayouts"] | undefined | null
|
||||||
) => {
|
) => {
|
||||||
const [_layout, setLayout] = useBookerStore((state) => [state.layout, state.setLayout], shallow);
|
const [_layout, setLayout] = useBookerStoreContext((state) => [state.layout, state.setLayout], shallow);
|
||||||
const isEmbed = useIsEmbed();
|
const isEmbed = useIsEmbed();
|
||||||
const isMobile = useMediaQuery("(max-width: 768px)");
|
const isMobile = useMediaQuery("(max-width: 768px)");
|
||||||
const isTablet = useMediaQuery("(max-width: 1024px)");
|
const isTablet = useMediaQuery("(max-width: 1024px)");
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { shallow } from "zustand/shallow";
|
import { shallow } from "zustand/shallow";
|
||||||
|
|
||||||
import { useBookerStore } from "@calcom/features/bookings/Booker/store";
|
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||||
|
|
||||||
import { useTimePreferences } from "../../../lib/timePreferences";
|
import { useTimePreferences } from "../../../lib/timePreferences";
|
||||||
import { getBookerTimezone } from "../../utils/getBookerTimezone";
|
import { getBookerTimezone } from "../../utils/getBookerTimezone";
|
||||||
|
|
||||||
export const useBookerTime = () => {
|
export const useBookerTime = () => {
|
||||||
const [timezoneFromBookerStore] = useBookerStore((state) => [state.timezone], shallow);
|
const [timezoneFromBookerStore] = useBookerStoreContext((state) => [state.timezone], shallow);
|
||||||
const { timezone: timezoneFromTimePreferences, timeFormat } = useTimePreferences();
|
const { timezone: timezoneFromTimePreferences, timeFormat } = useTimePreferences();
|
||||||
const timezone = getBookerTimezone({
|
const timezone = getBookerTimezone({
|
||||||
storeTimezone: timezoneFromBookerStore,
|
storeTimezone: timezoneFromBookerStore,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
|
|||||||
import { useHandleBookEvent } from "@calcom/atoms/hooks/bookings/useHandleBookEvent";
|
import { useHandleBookEvent } from "@calcom/atoms/hooks/bookings/useHandleBookEvent";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { sdkActionManager } from "@calcom/embed-core/embed-iframe";
|
import { sdkActionManager } from "@calcom/embed-core/embed-iframe";
|
||||||
|
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||||
import { useBookerStore } from "@calcom/features/bookings/Booker/store";
|
import { useBookerStore } from "@calcom/features/bookings/Booker/store";
|
||||||
import { updateQueryParam, getQueryParam } from "@calcom/features/bookings/Booker/utils/query-param";
|
import { updateQueryParam, getQueryParam } from "@calcom/features/bookings/Booker/utils/query-param";
|
||||||
import { createBooking, createRecurringBooking, createInstantBooking } from "@calcom/features/bookings/lib";
|
import { createBooking, createRecurringBooking, createInstantBooking } from "@calcom/features/bookings/lib";
|
||||||
@@ -56,6 +57,28 @@ export interface IUseBookings {
|
|||||||
isBookingDryRun?: boolean;
|
isBookingDryRun?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getBaseBookingEventPayload = (booking: {
|
||||||
|
title?: string;
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
eventTypeId?: number | null;
|
||||||
|
status?: BookingStatus;
|
||||||
|
paymentRequired: boolean;
|
||||||
|
isRecurring: boolean;
|
||||||
|
videoCallUrl?: string;
|
||||||
|
}) => {
|
||||||
|
return {
|
||||||
|
title: booking.title,
|
||||||
|
startTime: booking.startTime,
|
||||||
|
endTime: booking.endTime,
|
||||||
|
eventTypeId: booking.eventTypeId,
|
||||||
|
status: booking.status,
|
||||||
|
paymentRequired: booking.paymentRequired,
|
||||||
|
isRecurring: booking.isRecurring,
|
||||||
|
videoCallUrl: booking.videoCallUrl,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const getBookingSuccessfulEventPayload = (booking: {
|
const getBookingSuccessfulEventPayload = (booking: {
|
||||||
title?: string;
|
title?: string;
|
||||||
startTime: string;
|
startTime: string;
|
||||||
@@ -69,18 +92,15 @@ const getBookingSuccessfulEventPayload = (booking: {
|
|||||||
}) => {
|
}) => {
|
||||||
return {
|
return {
|
||||||
uid: booking.uid,
|
uid: booking.uid,
|
||||||
title: booking.title,
|
...getBaseBookingEventPayload(booking),
|
||||||
startTime: booking.startTime,
|
|
||||||
endTime: booking.endTime,
|
|
||||||
eventTypeId: booking.eventTypeId,
|
|
||||||
status: booking.status,
|
|
||||||
paymentRequired: booking.paymentRequired,
|
|
||||||
isRecurring: booking.isRecurring,
|
|
||||||
videoCallUrl: booking.videoCallUrl,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRescheduleBookingSuccessfulEventPayload = getBookingSuccessfulEventPayload;
|
const getRescheduleBookingSuccessfulEventPayload = getBookingSuccessfulEventPayload;
|
||||||
|
|
||||||
|
export const getDryRunBookingSuccessfulEventPayload = getBaseBookingEventPayload;
|
||||||
|
|
||||||
|
export const getDryRunRescheduleBookingSuccessfulEventPayload = getDryRunBookingSuccessfulEventPayload;
|
||||||
export interface IUseBookingLoadingStates {
|
export interface IUseBookingLoadingStates {
|
||||||
creatingBooking: boolean;
|
creatingBooking: boolean;
|
||||||
creatingRecurringBooking: boolean;
|
creatingRecurringBooking: boolean;
|
||||||
@@ -117,21 +137,21 @@ export const useBookings = ({
|
|||||||
isBookingDryRun,
|
isBookingDryRun,
|
||||||
}: IUseBookings) => {
|
}: IUseBookings) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const eventSlug = useBookerStore((state) => state.eventSlug);
|
const eventSlug = useBookerStoreContext((state) => state.eventSlug);
|
||||||
const eventTypeId = useBookerStore((state) => state.eventId);
|
const eventTypeId = useBookerStoreContext((state) => state.eventId);
|
||||||
const isInstantMeeting = useBookerStore((state) => state.isInstantMeeting);
|
const isInstantMeeting = useBookerStoreContext((state) => state.isInstantMeeting);
|
||||||
|
|
||||||
const rescheduleUid = useBookerStore((state) => state.rescheduleUid);
|
const rescheduleUid = useBookerStoreContext((state) => state.rescheduleUid);
|
||||||
const rescheduledBy = useBookerStore((state) => state.rescheduledBy);
|
const rescheduledBy = useBookerStoreContext((state) => state.rescheduledBy);
|
||||||
const bookingData = useBookerStore((state) => state.bookingData);
|
const bookingData = useBookerStoreContext((state) => state.bookingData);
|
||||||
const timeslot = useBookerStore((state) => state.selectedTimeslot);
|
const timeslot = useBookerStoreContext((state) => state.selectedTimeslot);
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const bookingSuccessRedirect = useBookingSuccessRedirect();
|
const bookingSuccessRedirect = useBookingSuccessRedirect();
|
||||||
const bookerFormErrorRef = useRef<HTMLDivElement>(null);
|
const bookerFormErrorRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const [instantMeetingTokenExpiryTime, setExpiryTime] = useState<Date | undefined>();
|
const [instantMeetingTokenExpiryTime, setExpiryTime] = useState<Date | undefined>();
|
||||||
const [instantVideoMeetingUrl, setInstantVideoMeetingUrl] = useState<string | undefined>();
|
const [instantVideoMeetingUrl, setInstantVideoMeetingUrl] = useState<string | undefined>();
|
||||||
const duration = useBookerStore((state) => state.selectedDuration);
|
const duration = useBookerStoreContext((state) => state.selectedDuration);
|
||||||
|
|
||||||
const isRescheduling = !!rescheduleUid && !!bookingData;
|
const isRescheduling = !!rescheduleUid && !!bookingData;
|
||||||
|
|
||||||
@@ -195,6 +215,30 @@ export const useBookings = ({
|
|||||||
mutationFn: createBooking,
|
mutationFn: createBooking,
|
||||||
onSuccess: (booking) => {
|
onSuccess: (booking) => {
|
||||||
if (booking.isDryRun) {
|
if (booking.isDryRun) {
|
||||||
|
const validDuration = event.data?.isDynamic
|
||||||
|
? duration || event.data?.length
|
||||||
|
: duration && event.data?.metadata?.multipleDuration?.includes(duration)
|
||||||
|
? duration
|
||||||
|
: event.data?.length;
|
||||||
|
|
||||||
|
if (isRescheduling) {
|
||||||
|
sdkActionManager?.fire(
|
||||||
|
"dryRunRescheduleBookingSuccessfulV2",
|
||||||
|
getDryRunRescheduleBookingSuccessfulEventPayload({
|
||||||
|
...booking,
|
||||||
|
isRecurring: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
sdkActionManager?.fire(
|
||||||
|
"dryRunBookingSuccessfulV2",
|
||||||
|
getDryRunBookingSuccessfulEventPayload({
|
||||||
|
...booking,
|
||||||
|
isRecurring: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
router.push("/booking/dry-run-successful");
|
router.push("/booking/dry-run-successful");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -342,6 +386,30 @@ export const useBookings = ({
|
|||||||
const booking = bookings[0] || {};
|
const booking = bookings[0] || {};
|
||||||
|
|
||||||
if (booking.isDryRun) {
|
if (booking.isDryRun) {
|
||||||
|
if (isRescheduling) {
|
||||||
|
sdkActionManager?.fire("dryRunRescheduleBookingSuccessfulV2", {
|
||||||
|
...getDryRunRescheduleBookingSuccessfulEventPayload({
|
||||||
|
...booking,
|
||||||
|
isRecurring: true,
|
||||||
|
}),
|
||||||
|
allBookings: bookings.map((booking) => ({
|
||||||
|
startTime: booking.startTime,
|
||||||
|
endTime: booking.endTime,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
sdkActionManager?.fire("dryRunBookingSuccessfulV2", {
|
||||||
|
...getDryRunBookingSuccessfulEventPayload({
|
||||||
|
...booking,
|
||||||
|
isRecurring: true,
|
||||||
|
}),
|
||||||
|
allBookings: bookings.map((booking) => ({
|
||||||
|
startTime: booking.startTime,
|
||||||
|
endTime: booking.endTime,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
router.push("/booking/dry-run-successful");
|
router.push("/booking/dry-run-successful");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useEffect, useRef } from "react";
|
|||||||
import { shallow } from "zustand/shallow";
|
import { shallow } from "zustand/shallow";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { useBookerStore } from "@calcom/features/bookings/Booker/store";
|
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||||
import { useSlotReservationId } from "@calcom/features/bookings/Booker/useSlotReservationId";
|
import { useSlotReservationId } from "@calcom/features/bookings/Booker/useSlotReservationId";
|
||||||
import { isBookingDryRun } from "@calcom/features/bookings/Booker/utils/isBookingDryRun";
|
import { isBookingDryRun } from "@calcom/features/bookings/Booker/utils/isBookingDryRun";
|
||||||
import {
|
import {
|
||||||
@@ -78,10 +78,10 @@ const useQuickAvailabilityChecks = ({
|
|||||||
export type UseSlotsReturnType = ReturnType<typeof useSlots>;
|
export type UseSlotsReturnType = ReturnType<typeof useSlots>;
|
||||||
|
|
||||||
export const useSlots = (event: { id: number; length: number } | null) => {
|
export const useSlots = (event: { id: number; length: number } | null) => {
|
||||||
const selectedDuration = useBookerStore((state) => state.selectedDuration);
|
const selectedDuration = useBookerStoreContext((state) => state.selectedDuration);
|
||||||
const searchParams = useCompatSearchParams();
|
const searchParams = useCompatSearchParams();
|
||||||
const [selectedTimeslot, setSelectedTimeslot, tentativeSelectedTimeslots, setTentativeSelectedTimeslots] =
|
const [selectedTimeslot, setSelectedTimeslot, tentativeSelectedTimeslots, setTentativeSelectedTimeslots] =
|
||||||
useBookerStore(
|
useBookerStoreContext(
|
||||||
(state) => [
|
(state) => [
|
||||||
state.selectedTimeslot,
|
state.selectedTimeslot,
|
||||||
state.setSelectedTimeslot,
|
state.setSelectedTimeslot,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user