Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9dd2dbff19 | ||
|
|
cebbffe230 | ||
|
|
e3e464abfa | ||
|
|
268438b0ff | ||
|
|
355c8de5e7 | ||
|
|
e6056bc4a4 | ||
|
|
f2eebe95bd | ||
|
|
3084013269 | ||
|
|
68568e5a25 | ||
|
|
0eae0bb8fe | ||
|
|
b61820d354 | ||
|
|
1606272850 | ||
|
|
ea04fe1c9b | ||
|
|
4c50b6a8af | ||
|
|
39aa848e9e | ||
|
|
64c56c1b17 | ||
|
|
8e883a6b10 | ||
|
|
9684aa3ad5 | ||
|
|
5fede84bf6 | ||
|
|
a788e07be1 | ||
|
|
68ffd2248b | ||
|
|
4b12d021e4 | ||
|
|
4daf9db9db | ||
|
|
a660b1eb05 | ||
|
|
6f2f098e12 | ||
|
|
6a75ef86b0 | ||
|
|
3d2397879c | ||
|
|
de474089a7 | ||
|
|
8b31b9aa05 | ||
|
|
ad41672958 | ||
|
|
86f2984b30 | ||
|
|
38e62786e4 |
@@ -62,6 +62,7 @@ jobs:
|
||||
env:
|
||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||
|
||||
@@ -49,6 +49,7 @@ jobs:
|
||||
env:
|
||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||
|
||||
@@ -55,6 +55,7 @@ jobs:
|
||||
env:
|
||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||
|
||||
@@ -53,6 +53,7 @@ jobs:
|
||||
env:
|
||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||
|
||||
@@ -6,6 +6,7 @@ on:
|
||||
env:
|
||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
|
||||
@@ -6,6 +6,7 @@ on:
|
||||
env:
|
||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
|
||||
@@ -7,6 +7,9 @@ export class RewriterMiddleware implements NestMiddleware {
|
||||
if (req.url.startsWith("/api/v2")) {
|
||||
req.url = req.url.replace("/api/v2", "/v2");
|
||||
}
|
||||
if (req.url.startsWith("/v2/ee")) {
|
||||
req.url = req.url.replace("/v2/ee", "/v2");
|
||||
}
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ describe("Bookings Endpoints", () => {
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/api/v2/ee/bookings")
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
@@ -138,7 +138,7 @@ describe("Bookings Endpoints", () => {
|
||||
|
||||
it("should get bookings", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get("/api/v2/ee/bookings?filters[status]=upcoming")
|
||||
.get("/v2/bookings?filters[status]=upcoming")
|
||||
.then((response) => {
|
||||
console.log("asap responseBody", JSON.stringify(response.body, null, 2));
|
||||
const responseBody: GetBookingsOutput = response.body;
|
||||
@@ -159,7 +159,7 @@ describe("Bookings Endpoints", () => {
|
||||
|
||||
it("should get booking", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/api/v2/ee/bookings/${createdBooking.uid}`)
|
||||
.get(`/v2/bookings/${createdBooking.uid}`)
|
||||
.then((response) => {
|
||||
const responseBody: GetBookingOutput = response.body;
|
||||
const bookingInfo = responseBody.data;
|
||||
@@ -199,7 +199,7 @@ describe("Bookings Endpoints", () => {
|
||||
// };
|
||||
|
||||
// return request(app.getHttpServer())
|
||||
// .post("/api/v2/ee/bookings/reccuring")
|
||||
// .post("/v2/bookings/reccuring")
|
||||
// .send(body)
|
||||
// .expect(201)
|
||||
// .then((response) => {
|
||||
@@ -231,7 +231,7 @@ describe("Bookings Endpoints", () => {
|
||||
// };
|
||||
|
||||
// return request(app.getHttpServer())
|
||||
// .post("/api/v2/ee/bookings/instant")
|
||||
// .post("/v2/bookings/instant")
|
||||
// .send(body)
|
||||
// .expect(201)
|
||||
// .then((response) => {
|
||||
@@ -250,7 +250,7 @@ describe("Bookings Endpoints", () => {
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/api/v2/ee/bookings/${bookingId}/cancel`)
|
||||
.post(`/v2/bookings/${bookingId}/cancel`)
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then((response) => {
|
||||
|
||||
@@ -68,7 +68,7 @@ const DEFAULT_PLATFORM_PARAMS = {
|
||||
};
|
||||
|
||||
@Controller({
|
||||
path: "ee/bookings",
|
||||
path: "/bookings",
|
||||
version: "2",
|
||||
})
|
||||
@UseGuards(PermissionsGuard)
|
||||
|
||||
@@ -11,7 +11,7 @@ import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { CalendarBusyTimesInput } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "ee/calendars",
|
||||
path: "/calendars",
|
||||
version: "2",
|
||||
})
|
||||
@UseGuards(AccessTokenGuard)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
@@ -48,9 +49,7 @@ export class EventTypesRepository {
|
||||
currentOrganizationId: user.movedToProfile?.organizationId || user.organizationId,
|
||||
eventTypeId,
|
||||
userId: user.id,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
prisma: this.dbRead.prisma,
|
||||
prisma: this.dbRead.prisma as unknown as PrismaClient,
|
||||
isUserOrganizationAdmin,
|
||||
isTrpcCall: true,
|
||||
});
|
||||
|
||||
@@ -85,14 +85,14 @@ describe("Platform Gcal Endpoints", () => {
|
||||
|
||||
it(`/GET/ee/gcal/oauth/auth-url: it should respond 401 with invalid access token`, async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(`/api/v2/ee/gcal/oauth/auth-url`)
|
||||
.get(`/v2/gcal/oauth/auth-url`)
|
||||
.set("Authorization", `Bearer invalid_access_token`)
|
||||
.expect(401);
|
||||
});
|
||||
|
||||
it(`/GET/ee/gcal/oauth/auth-url: it should auth-url to google oauth with valid access token `, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/ee/gcal/oauth/auth-url`)
|
||||
.get(`/v2/gcal/oauth/auth-url`)
|
||||
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||
.set("Origin", CLIENT_REDIRECT_URI)
|
||||
.expect(200);
|
||||
@@ -103,7 +103,7 @@ describe("Platform Gcal Endpoints", () => {
|
||||
it(`/GET/ee/gcal/oauth/save: without oauth code`, async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(
|
||||
`/api/v2/ee/gcal/oauth/save?state=accessToken=${accessTokenSecret}&origin%3D${CLIENT_REDIRECT_URI}&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
|
||||
`/v2/gcal/oauth/save?state=accessToken=${accessTokenSecret}&origin%3D${CLIENT_REDIRECT_URI}&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
|
||||
)
|
||||
.expect(400);
|
||||
});
|
||||
@@ -111,7 +111,7 @@ describe("Platform Gcal Endpoints", () => {
|
||||
it(`/GET/ee/gcal/oauth/save: without access token`, async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(
|
||||
`/api/v2/ee/gcal/oauth/save?state=origin%3D${CLIENT_REDIRECT_URI}&code=4/0AfJohXmBuT7QVrEPlAJLBu4ZcSnyj5jtDoJqSW_riPUhPXQ70RPGkOEbVO3xs-OzQwpPQw&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
|
||||
`/v2/gcal/oauth/save?state=origin%3D${CLIENT_REDIRECT_URI}&code=4/0AfJohXmBuT7QVrEPlAJLBu4ZcSnyj5jtDoJqSW_riPUhPXQ70RPGkOEbVO3xs-OzQwpPQw&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
|
||||
)
|
||||
.expect(400);
|
||||
});
|
||||
@@ -119,25 +119,25 @@ describe("Platform Gcal Endpoints", () => {
|
||||
it(`/GET/ee/gcal/oauth/save: without origin`, async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(
|
||||
`/api/v2/ee/gcal/oauth/save?state=accessToken=${accessTokenSecret}&code=4/0AfJohXmBuT7QVrEPlAJLBu4ZcSnyj5jtDoJqSW_riPUhPXQ70RPGkOEbVO3xs-OzQwpPQw&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
|
||||
`/v2/gcal/oauth/save?state=accessToken=${accessTokenSecret}&code=4/0AfJohXmBuT7QVrEPlAJLBu4ZcSnyj5jtDoJqSW_riPUhPXQ70RPGkOEbVO3xs-OzQwpPQw&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
|
||||
)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it(`/GET/ee/gcal/check with access token but without origin`, async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(`/api/v2/ee/gcal/check`)
|
||||
.get(`/v2/gcal/check`)
|
||||
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it(`/GET/ee/gcal/check without access token`, async () => {
|
||||
await request(app.getHttpServer()).get(`/api/v2/ee/gcal/check`).expect(401);
|
||||
await request(app.getHttpServer()).get(`/v2/gcal/check`).expect(401);
|
||||
});
|
||||
|
||||
it(`/GET/ee/gcal/check with access token and origin but no credentials`, async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(`/api/v2/ee/gcal/check`)
|
||||
.get(`/v2/gcal/check`)
|
||||
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||
.set("Origin", CLIENT_REDIRECT_URI)
|
||||
.expect(400);
|
||||
@@ -151,7 +151,7 @@ describe("Platform Gcal Endpoints", () => {
|
||||
"google-calendar"
|
||||
);
|
||||
await request(app.getHttpServer())
|
||||
.get(`/api/v2/ee/gcal/check`)
|
||||
.get(`/v2/gcal/check`)
|
||||
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||
.set("Origin", CLIENT_REDIRECT_URI)
|
||||
.expect(200);
|
||||
|
||||
@@ -44,7 +44,7 @@ const CALENDAR_SCOPES = [
|
||||
|
||||
// Controller for the GCalConnect Atom
|
||||
@Controller({
|
||||
path: "ee/gcal",
|
||||
path: "/gcal",
|
||||
version: "2",
|
||||
})
|
||||
@DocsTags("Google Calendar")
|
||||
@@ -60,7 +60,7 @@ export class GcalController {
|
||||
private readonly calendarsService: CalendarsService
|
||||
) {}
|
||||
|
||||
private redirectUri = `${this.config.get("api.url")}/ee/gcal/oauth/save`;
|
||||
private redirectUri = `${this.config.get("api.url")}/gcal/oauth/save`;
|
||||
|
||||
@Get("/oauth/auth-url")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
|
||||
@@ -71,7 +71,7 @@ describe("Me Endpoints", () => {
|
||||
|
||||
it("should get user associated with access token", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get("/api/v2/ee/me")
|
||||
.get("/v2/me")
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<UserResponse> = response.body;
|
||||
@@ -90,7 +90,7 @@ describe("Me Endpoints", () => {
|
||||
const body: UpdateManagedUserInput = { timeZone: "Europe/Rome" };
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch("/api/v2/ee/me")
|
||||
.patch("/v2/me")
|
||||
.send(body)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
@@ -114,22 +114,19 @@ describe("Me Endpoints", () => {
|
||||
it("should not update user associated with access token given invalid timezone", async () => {
|
||||
const bodyWithIncorrectTimeZone: UpdateManagedUserInput = { timeZone: "Narnia/Woods" };
|
||||
|
||||
return request(app.getHttpServer()).patch("/api/v2/ee/me").send(bodyWithIncorrectTimeZone).expect(400);
|
||||
return request(app.getHttpServer()).patch("/v2/me").send(bodyWithIncorrectTimeZone).expect(400);
|
||||
});
|
||||
|
||||
it("should not update user associated with access token given invalid time format", async () => {
|
||||
const bodyWithIncorrectTimeFormat: UpdateManagedUserInput = { timeFormat: 100 };
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch("/api/v2/ee/me")
|
||||
.send(bodyWithIncorrectTimeFormat)
|
||||
.expect(400);
|
||||
return request(app.getHttpServer()).patch("/v2/me").send(bodyWithIncorrectTimeFormat).expect(400);
|
||||
});
|
||||
|
||||
it("should not update user associated with access token given invalid week start", async () => {
|
||||
const bodyWithIncorrectWeekStart: UpdateManagedUserInput = { weekStart: "waba luba dub dub" };
|
||||
|
||||
return request(app.getHttpServer()).patch("/api/v2/ee/me").send(bodyWithIncorrectWeekStart).expect(400);
|
||||
return request(app.getHttpServer()).patch("/v2/me").send(bodyWithIncorrectWeekStart).expect(400);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
|
||||
@@ -14,7 +14,7 @@ import { PROFILE_READ, PROFILE_WRITE, SUCCESS_STATUS } from "@calcom/platform-co
|
||||
import { userSchemaResponse } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "ee/me",
|
||||
path: "/me",
|
||||
version: "2",
|
||||
})
|
||||
@UseGuards(AccessTokenGuard, PermissionsGuard)
|
||||
|
||||
@@ -20,7 +20,7 @@ import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
@Controller({
|
||||
path: "ee/provider",
|
||||
path: "/provider",
|
||||
version: "2",
|
||||
})
|
||||
@DocsTags("Cal provider")
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/oauth-clients/{clientId}/users": {
|
||||
"/v2/oauth-clients/{clientId}/users": {
|
||||
"get": {
|
||||
"operationId": "OAuthClientUsersController_getManagedUsers",
|
||||
"parameters": [
|
||||
@@ -90,7 +90,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/oauth-clients/{clientId}/users/{userId}": {
|
||||
"/v2/oauth-clients/{clientId}/users/{userId}": {
|
||||
"get": {
|
||||
"operationId": "OAuthClientUsersController_getUserById",
|
||||
"parameters": [
|
||||
@@ -210,7 +210,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/oauth-clients/{clientId}/users/{userId}/force-refresh": {
|
||||
"/v2/oauth-clients/{clientId}/users/{userId}/force-refresh": {
|
||||
"post": {
|
||||
"operationId": "OAuthClientUsersController_forceRefresh",
|
||||
"parameters": [
|
||||
@@ -248,7 +248,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/oauth-clients": {
|
||||
"/v2/oauth-clients": {
|
||||
"post": {
|
||||
"operationId": "OAuthClientsController_createOAuthClient",
|
||||
"summary": "",
|
||||
@@ -302,7 +302,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/oauth-clients/{clientId}": {
|
||||
"/v2/oauth-clients/{clientId}": {
|
||||
"get": {
|
||||
"operationId": "OAuthClientsController_getOAuthClientById",
|
||||
"summary": "",
|
||||
@@ -404,7 +404,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/oauth/{clientId}/authorize": {
|
||||
"/v2/oauth/{clientId}/authorize": {
|
||||
"post": {
|
||||
"operationId": "OAuthFlowController_authorize",
|
||||
"summary": "Authorize an OAuth client",
|
||||
@@ -442,7 +442,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/oauth/{clientId}/exchange": {
|
||||
"/v2/oauth/{clientId}/exchange": {
|
||||
"post": {
|
||||
"operationId": "OAuthFlowController_exchange",
|
||||
"summary": "Exchange authorization code for access tokens",
|
||||
@@ -495,7 +495,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/oauth/{clientId}/refresh": {
|
||||
"/v2/oauth/{clientId}/refresh": {
|
||||
"post": {
|
||||
"operationId": "OAuthFlowController_refreshAccessToken",
|
||||
"parameters": [
|
||||
@@ -543,7 +543,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/event-types": {
|
||||
"/v2/event-types": {
|
||||
"post": {
|
||||
"operationId": "EventTypesController_createEventType",
|
||||
"parameters": [],
|
||||
@@ -593,7 +593,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/event-types/{eventTypeId}": {
|
||||
"/v2/event-types/{eventTypeId}": {
|
||||
"get": {
|
||||
"operationId": "EventTypesController_getEventType",
|
||||
"parameters": [
|
||||
@@ -689,7 +689,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/event-types/{username}/{eventSlug}/public": {
|
||||
"/v2/event-types/{username}/{eventSlug}/public": {
|
||||
"get": {
|
||||
"operationId": "EventTypesController_getPublicEventType",
|
||||
"parameters": [
|
||||
@@ -743,7 +743,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/event-types/{username}/public": {
|
||||
"/v2/event-types/{username}/public": {
|
||||
"get": {
|
||||
"operationId": "EventTypesController_getPublicEventTypes",
|
||||
"parameters": [
|
||||
@@ -773,7 +773,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/gcal/oauth/auth-url": {
|
||||
"/v2/gcal/oauth/auth-url": {
|
||||
"get": {
|
||||
"operationId": "GcalController_redirect",
|
||||
"parameters": [
|
||||
@@ -803,7 +803,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/gcal/oauth/save": {
|
||||
"/v2/gcal/oauth/save": {
|
||||
"get": {
|
||||
"operationId": "GcalController_save",
|
||||
"parameters": [
|
||||
@@ -841,7 +841,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/gcal/check": {
|
||||
"/v2/gcal/check": {
|
||||
"get": {
|
||||
"operationId": "GcalController_check",
|
||||
"parameters": [],
|
||||
@@ -862,7 +862,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/provider/{clientId}": {
|
||||
"/v2/provider/{clientId}": {
|
||||
"get": {
|
||||
"operationId": "CalProviderController_verifyClientId",
|
||||
"parameters": [
|
||||
@@ -892,7 +892,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/provider/{clientId}/access-token": {
|
||||
"/v2/provider/{clientId}/access-token": {
|
||||
"get": {
|
||||
"operationId": "CalProviderController_verifyAccessToken",
|
||||
"parameters": [
|
||||
@@ -922,7 +922,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/schedules": {
|
||||
"/v2/schedules": {
|
||||
"post": {
|
||||
"operationId": "SchedulesController_createSchedule",
|
||||
"parameters": [],
|
||||
@@ -972,7 +972,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/schedules/default": {
|
||||
"/v2/schedules/default": {
|
||||
"get": {
|
||||
"operationId": "SchedulesController_getDefaultSchedule",
|
||||
"parameters": [],
|
||||
@@ -993,7 +993,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/schedules/{scheduleId}": {
|
||||
"/v2/schedules/{scheduleId}": {
|
||||
"get": {
|
||||
"operationId": "SchedulesController_getSchedule",
|
||||
"parameters": [
|
||||
@@ -1089,7 +1089,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/me": {
|
||||
"/v2/me": {
|
||||
"get": {
|
||||
"operationId": "MeController_getMe",
|
||||
"parameters": [],
|
||||
@@ -1139,7 +1139,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/calendars/busy-times": {
|
||||
"/v2/calendars/busy-times": {
|
||||
"get": {
|
||||
"operationId": "CalendarsController_getBusyTimes",
|
||||
"parameters": [],
|
||||
@@ -1160,7 +1160,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/calendars": {
|
||||
"/v2/calendars": {
|
||||
"get": {
|
||||
"operationId": "CalendarsController_getCalendars",
|
||||
"parameters": [],
|
||||
@@ -1181,7 +1181,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/bookings": {
|
||||
"/v2/bookings": {
|
||||
"get": {
|
||||
"operationId": "BookingsController_getBookings",
|
||||
"parameters": [
|
||||
@@ -1235,7 +1235,16 @@
|
||||
},
|
||||
"post": {
|
||||
"operationId": "BookingsController_createBooking",
|
||||
"parameters": [],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "x-cal-client-id",
|
||||
"required": true,
|
||||
"in": "header",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
@@ -1263,7 +1272,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/bookings/{bookingUid}": {
|
||||
"/v2/bookings/{bookingUid}": {
|
||||
"get": {
|
||||
"operationId": "BookingsController_getBooking",
|
||||
"parameters": [
|
||||
@@ -1293,7 +1302,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/bookings/{bookingUid}/reschedule": {
|
||||
"/v2/bookings/{bookingUid}/reschedule": {
|
||||
"get": {
|
||||
"operationId": "BookingsController_getBookingForReschedule",
|
||||
"parameters": [
|
||||
@@ -1323,7 +1332,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/bookings/{bookingId}/cancel": {
|
||||
"/v2/bookings/{bookingId}/cancel": {
|
||||
"post": {
|
||||
"operationId": "BookingsController_cancelBooking",
|
||||
"parameters": [
|
||||
@@ -1334,6 +1343,14 @@
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "x-cal-client-id",
|
||||
"required": true,
|
||||
"in": "header",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
@@ -1363,10 +1380,19 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/bookings/reccuring": {
|
||||
"/v2/bookings/reccuring": {
|
||||
"post": {
|
||||
"operationId": "BookingsController_createReccuringBooking",
|
||||
"parameters": [],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "x-cal-client-id",
|
||||
"required": true,
|
||||
"in": "header",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
@@ -1397,10 +1423,19 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/bookings/instant": {
|
||||
"/v2/bookings/instant": {
|
||||
"post": {
|
||||
"operationId": "BookingsController_createInstantBooking",
|
||||
"parameters": [],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "x-cal-client-id",
|
||||
"required": true,
|
||||
"in": "header",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
@@ -1428,7 +1463,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/slots/reserve": {
|
||||
"/v2/slots/reserve": {
|
||||
"post": {
|
||||
"operationId": "SlotsController_reserveSlot",
|
||||
"parameters": [],
|
||||
@@ -1459,7 +1494,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/slots/selected-slot": {
|
||||
"/v2/slots/selected-slot": {
|
||||
"delete": {
|
||||
"operationId": "SlotsController_deleteSelectedSlot",
|
||||
"parameters": [],
|
||||
@@ -1480,7 +1515,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/slots/available": {
|
||||
"/v2/slots/available": {
|
||||
"get": {
|
||||
"operationId": "SlotsController_getAvailableSlots",
|
||||
"parameters": [],
|
||||
@@ -1501,7 +1536,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/timezones": {
|
||||
"/v2/timezones": {
|
||||
"get": {
|
||||
"operationId": "TimezonesController_getTimeZones",
|
||||
"parameters": [],
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Hello World
|
||||
@@ -1 +0,0 @@
|
||||
## Cal.com Platform Example (https://platform.cal.com)
|
||||
+10
-2
@@ -1,3 +1,4 @@
|
||||
import { useRouter } from "next/navigation";
|
||||
import React from "react";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
@@ -38,7 +39,7 @@ export const OAuthClientCard = ({
|
||||
isLoading,
|
||||
areEmailsEnabled,
|
||||
}: OAuthClientCardProps) => {
|
||||
console.log(areEmailsEnabled);
|
||||
const router = useRouter();
|
||||
|
||||
const clientPermissions = Object.values(PERMISSIONS_GROUPED_MAP).map((value, index) => {
|
||||
let permissionsMessage = "";
|
||||
@@ -139,7 +140,14 @@ export const OAuthClientCard = ({
|
||||
<span className="text-sm font-semibold">Emails enabled:</span> {areEmailsEnabled ? "Yes" : "No"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start">
|
||||
<div className="flex items-start gap-4">
|
||||
<Button
|
||||
className="bg-subtle hover:bg-emphasis text-white"
|
||||
loading={isLoading}
|
||||
disabled={isLoading}
|
||||
onClick={() => router.push(`/settings/organizations/platform/oauth-clients/create?clientId=${id}`)}>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-red-500 text-white hover:bg-red-600"
|
||||
loading={isLoading}
|
||||
|
||||
+105
-22
@@ -1,6 +1,6 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { FC } from "react";
|
||||
import React, { useState, useCallback } from "react";
|
||||
import React, { useState, useCallback, useEffect } from "react";
|
||||
import { useForm, useFieldArray } from "react-hook-form";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -8,7 +8,24 @@ import { PERMISSIONS_GROUPED_MAP } from "@calcom/platform-constants/permissions"
|
||||
import { showToast } from "@calcom/ui";
|
||||
import { Meta, Button, TextField, Label, Tooltip } from "@calcom/ui";
|
||||
|
||||
import { useCreateOAuthClient } from "@lib/hooks/settings/organizations/platform/oauth-clients/usePersistOAuthClient";
|
||||
import { useOAuthClient } from "@lib/hooks/settings/organizations/platform/oauth-clients/useOAuthClients";
|
||||
import {
|
||||
useCreateOAuthClient,
|
||||
useUpdateOAuthClient,
|
||||
} from "@lib/hooks/settings/organizations/platform/oauth-clients/usePersistOAuthClient";
|
||||
|
||||
import {
|
||||
hasAppsReadPermission,
|
||||
hasAppsWritePermission,
|
||||
hasBookingReadPermission,
|
||||
hasBookingWritePermission,
|
||||
hasEventTypeReadPermission,
|
||||
hasEventTypeWritePermission,
|
||||
hasProfileReadPermission,
|
||||
hasProfileWritePermission,
|
||||
hasScheduleReadPermission,
|
||||
hasScheduleWritePermission,
|
||||
} from "../../../../../../../packages/platform/utils/permissions";
|
||||
|
||||
type FormValues = {
|
||||
name: string;
|
||||
@@ -33,9 +50,10 @@ type FormValues = {
|
||||
areEmailsEnabled?: boolean;
|
||||
};
|
||||
|
||||
export const OAuthClientForm: FC = () => {
|
||||
export const OAuthClientForm: FC<{ clientId?: string }> = ({ clientId }) => {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const { data, isFetched, isError, refetch } = useOAuthClient(clientId);
|
||||
const { register, control, handleSubmit, setValue } = useForm<FormValues>({
|
||||
defaultValues: {
|
||||
redirectUris: [{ uri: "" }],
|
||||
@@ -45,6 +63,32 @@ export const OAuthClientForm: FC = () => {
|
||||
control,
|
||||
name: "redirectUris",
|
||||
});
|
||||
useEffect(() => {
|
||||
if (isFetched && data && !isError) {
|
||||
setValue("name", data.name);
|
||||
data.bookingRedirectUri && setValue("bookingRedirectUri", data.bookingRedirectUri);
|
||||
data.bookingCancelRedirectUri && setValue("bookingCancelRedirectUri", data.bookingCancelRedirectUri);
|
||||
data.bookingRescheduleRedirectUri &&
|
||||
setValue("bookingRescheduleRedirectUri", data.bookingRescheduleRedirectUri);
|
||||
setValue("areEmailsEnabled", data?.areEmailsEnabled);
|
||||
data?.redirectUris.forEach((uri: string, index: number) => {
|
||||
index === 0 && setValue(`redirectUris.${index}.uri`, uri);
|
||||
index !== 0 && append({ uri });
|
||||
});
|
||||
if (hasAppsReadPermission(data.permissions)) setValue("appsRead", true);
|
||||
if (hasAppsWritePermission(data.permissions)) setValue("appsWrite", true);
|
||||
if (hasBookingReadPermission(data.permissions)) setValue("bookingRead", true);
|
||||
if (hasBookingWritePermission(data.permissions)) setValue("bookingWrite", true);
|
||||
if (hasEventTypeReadPermission(data.permissions)) setValue("eventTypeRead", true);
|
||||
if (hasEventTypeWritePermission(data.permissions)) setValue("eventTypeWrite", true);
|
||||
if (hasProfileReadPermission(data.permissions)) setValue("profileRead", true);
|
||||
if (hasProfileWritePermission(data.permissions)) setValue("profileWrite", true);
|
||||
if (hasScheduleReadPermission(data.permissions)) setValue("scheduleRead", true);
|
||||
if (hasScheduleWritePermission(data.permissions)) setValue("scheduleWrite", true);
|
||||
}
|
||||
}, [isFetched, data]);
|
||||
const disabledForm = Boolean(clientId && !isFetched && isError);
|
||||
|
||||
const [isSelectAllPermissionsChecked, setIsSelectAllPermissionsChecked] = useState(false);
|
||||
|
||||
const selectAllPermissions = useCallback(() => {
|
||||
@@ -59,15 +103,27 @@ export const OAuthClientForm: FC = () => {
|
||||
setIsSelectAllPermissionsChecked((preValue) => !preValue);
|
||||
}, [isSelectAllPermissionsChecked, setValue]);
|
||||
|
||||
const { mutateAsync, isPending } = useCreateOAuthClient({
|
||||
const { mutateAsync: save, isPending: isSaving } = useCreateOAuthClient({
|
||||
onSuccess: () => {
|
||||
showToast("OAuth client created successfully", "success");
|
||||
refetch();
|
||||
router.push("/settings/organizations/platform/oauth-clients");
|
||||
},
|
||||
onError: () => {
|
||||
showToast("Internal server error, please try again later", "error");
|
||||
},
|
||||
});
|
||||
const { mutateAsync: update, isPending: isUpdating } = useUpdateOAuthClient({
|
||||
onSuccess: () => {
|
||||
showToast("OAuth client updated successfully", "success");
|
||||
refetch();
|
||||
router.push("/settings/organizations/platform/oauth-clients");
|
||||
},
|
||||
onError: () => {
|
||||
showToast("Internal server error, please try again later", "error");
|
||||
},
|
||||
clientId,
|
||||
});
|
||||
|
||||
const onSubmit = (data: FormValues) => {
|
||||
let userPermissions = 0;
|
||||
@@ -82,17 +138,31 @@ export const OAuthClientForm: FC = () => {
|
||||
if (data[`${entityKey}Write`]) userPermissions |= write;
|
||||
});
|
||||
|
||||
mutateAsync({
|
||||
name: data.name,
|
||||
permissions: userPermissions,
|
||||
// logo: data.logo,
|
||||
redirectUris: userRedirectUris,
|
||||
bookingRedirectUri: data.bookingRedirectUri,
|
||||
bookingCancelRedirectUri: data.bookingCancelRedirectUri,
|
||||
bookingRescheduleRedirectUri: data.bookingRescheduleRedirectUri,
|
||||
areEmailsEnabled: data.areEmailsEnabled,
|
||||
});
|
||||
if (clientId) {
|
||||
// don't update permissions if client is already created
|
||||
update({
|
||||
name: data.name,
|
||||
// logo: data.logo,
|
||||
redirectUris: userRedirectUris,
|
||||
bookingRedirectUri: data.bookingRedirectUri,
|
||||
bookingCancelRedirectUri: data.bookingCancelRedirectUri,
|
||||
bookingRescheduleRedirectUri: data.bookingRescheduleRedirectUri,
|
||||
areEmailsEnabled: data.areEmailsEnabled,
|
||||
});
|
||||
} else {
|
||||
save({
|
||||
name: data.name,
|
||||
permissions: userPermissions,
|
||||
// logo: data.logo,
|
||||
redirectUris: userRedirectUris,
|
||||
bookingRedirectUri: data.bookingRedirectUri,
|
||||
bookingCancelRedirectUri: data.bookingCancelRedirectUri,
|
||||
bookingRescheduleRedirectUri: data.bookingRescheduleRedirectUri,
|
||||
areEmailsEnabled: data.areEmailsEnabled,
|
||||
});
|
||||
}
|
||||
};
|
||||
const isPending = isSaving || isUpdating;
|
||||
|
||||
const permissionsCheckboxes = Object.keys(PERMISSIONS_GROUPED_MAP).map((key) => {
|
||||
const entity = key as keyof typeof PERMISSIONS_GROUPED_MAP;
|
||||
@@ -109,6 +179,7 @@ export const OAuthClientForm: FC = () => {
|
||||
id={`${permissionKey}Read`}
|
||||
className="bg-default border-default h-4 w-4 shrink-0 cursor-pointer rounded-[4px] border ring-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed"
|
||||
type="checkbox"
|
||||
disabled={Boolean(clientId)}
|
||||
/>
|
||||
<label htmlFor={`${permissionKey}Read`} className="cursor-pointer text-sm">
|
||||
Read
|
||||
@@ -120,6 +191,7 @@ export const OAuthClientForm: FC = () => {
|
||||
id={`${permissionKey}Write`}
|
||||
className="bg-default border-default h-4 w-4 shrink-0 cursor-pointer rounded-[4px] border ring-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed"
|
||||
type="checkbox"
|
||||
disabled={Boolean(clientId)}
|
||||
/>
|
||||
<label htmlFor={`${permissionKey}Write`} className="cursor-pointer text-sm">
|
||||
Write
|
||||
@@ -133,15 +205,15 @@ export const OAuthClientForm: FC = () => {
|
||||
return (
|
||||
<div>
|
||||
<Meta
|
||||
title={t("oauth_form_title")}
|
||||
description={t("oauth_form_description")}
|
||||
title={t("oauth_form_title") + (clientId ? " - Update" : "")}
|
||||
description={t("oauth_form_description") + (clientId ? " - Update" : "")}
|
||||
borderInShellHeader={true}
|
||||
/>
|
||||
<form
|
||||
className="border-subtle rounded-b-lg border border-t-0 px-4 pb-8 pt-2"
|
||||
onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="mt-6">
|
||||
<TextField required={true} label="Client name" {...register("name")} />
|
||||
<TextField disabled={disabledForm} required={true} label="Client name" {...register("name")} />
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<Label>Redirect uris</Label>
|
||||
@@ -154,6 +226,7 @@ export const OAuthClientForm: FC = () => {
|
||||
required={index === 0}
|
||||
className="w-[100%]"
|
||||
label=""
|
||||
disabled={disabledForm}
|
||||
{...register(`redirectUris.${index}.uri` as const)}
|
||||
/>
|
||||
</div>
|
||||
@@ -168,6 +241,7 @@ export const OAuthClientForm: FC = () => {
|
||||
onClick={() => {
|
||||
append({ uri: "" });
|
||||
}}
|
||||
disabled={disabledForm}
|
||||
/>
|
||||
{index > 0 && (
|
||||
<Button
|
||||
@@ -180,6 +254,7 @@ export const OAuthClientForm: FC = () => {
|
||||
onClick={() => {
|
||||
remove(index);
|
||||
}}
|
||||
disabled={disabledForm}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -224,6 +299,7 @@ export const OAuthClientForm: FC = () => {
|
||||
label="Booking redirect uri"
|
||||
className="w-[100%]"
|
||||
{...register("bookingRedirectUri")}
|
||||
disabled={disabledForm}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -234,6 +310,7 @@ export const OAuthClientForm: FC = () => {
|
||||
label="Booking cancel redirect uri"
|
||||
className="w-[100%]"
|
||||
{...register("bookingCancelRedirectUri")}
|
||||
disabled={disabledForm}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -244,6 +321,7 @@ export const OAuthClientForm: FC = () => {
|
||||
label="Booking reschedule redirect uri"
|
||||
className="w-[100%]"
|
||||
{...register("bookingRescheduleRedirectUri")}
|
||||
disabled={disabledForm}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -253,22 +331,27 @@ export const OAuthClientForm: FC = () => {
|
||||
id="areEmailsEnabled"
|
||||
className="bg-default border-default h-4 w-4 shrink-0 cursor-pointer rounded-[4px] border ring-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed"
|
||||
type="checkbox"
|
||||
disabled={disabledForm}
|
||||
/>
|
||||
<label htmlFor="areEmailsEnabled" className="cursor-pointer px-2 text-base font-semibold">
|
||||
Enable emails
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<div className="flex justify-between">
|
||||
<h1 className="text-base font-semibold underline">Permissions</h1>
|
||||
<Button type="button" onClick={selectAllPermissions}>
|
||||
<Tooltip side="right" content="Permissions once set cannot be modified">
|
||||
<h1 className="text-base font-semibold underline">Permissions</h1>
|
||||
</Tooltip>
|
||||
<Button type="button" onClick={selectAllPermissions} disabled={disabledForm || Boolean(clientId)}>
|
||||
{!isSelectAllPermissionsChecked ? "Select all" : "Discard all"}
|
||||
</Button>
|
||||
</div>
|
||||
<div>{permissionsCheckboxes}</div>
|
||||
</div>
|
||||
<Button className="mt-6" type="submit" loading={isPending}>
|
||||
Submit
|
||||
|
||||
<Button className="mt-6" type="submit" loading={isPending} disabled={disabledForm}>
|
||||
{clientId ? "Update" : "Submit"}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -72,6 +72,7 @@ export const getEventTypesFromDB = async (id: number) => {
|
||||
seatsPerTimeSlot: true,
|
||||
seatsShowAttendees: true,
|
||||
seatsShowAvailabilityCount: true,
|
||||
schedulingType: true,
|
||||
periodStartDate: true,
|
||||
periodEndDate: true,
|
||||
},
|
||||
|
||||
+21
-3
@@ -17,20 +17,38 @@ export const useOAuthClients = () => {
|
||||
return { ...query, data: query.data?.data ?? [] };
|
||||
};
|
||||
|
||||
export const useOAuthClient = (clientId: string) => {
|
||||
export const useOAuthClient = (clientId?: string) => {
|
||||
const {
|
||||
isLoading,
|
||||
error,
|
||||
data: response,
|
||||
isFetched,
|
||||
isError,
|
||||
isFetching,
|
||||
isSuccess,
|
||||
isFetchedAfterMount,
|
||||
refetch,
|
||||
} = useQuery<ApiSuccessResponse<PlatformOAuthClient>>({
|
||||
queryKey: ["oauth-client"],
|
||||
queryKey: ["oauth-client", clientId],
|
||||
queryFn: () => {
|
||||
return fetch(`/api/v2/oauth-clients/${clientId}`, {
|
||||
method: "get",
|
||||
headers: { "Content-type": "application/json" },
|
||||
}).then((res) => res.json());
|
||||
},
|
||||
enabled: clientId !== undefined,
|
||||
staleTime: Infinity,
|
||||
});
|
||||
|
||||
return { isLoading, error, data: response?.data };
|
||||
return {
|
||||
isLoading,
|
||||
error,
|
||||
data: response?.data,
|
||||
isFetched,
|
||||
isError,
|
||||
isFetching,
|
||||
isSuccess,
|
||||
isFetchedAfterMount,
|
||||
refetch,
|
||||
};
|
||||
};
|
||||
|
||||
+38
-1
@@ -46,6 +46,43 @@ export const useCreateOAuthClient = (
|
||||
return mutation;
|
||||
};
|
||||
|
||||
export const useUpdateOAuthClient = (
|
||||
{ onSuccess, onError, clientId }: IPersistOAuthClient & { clientId?: string } = {
|
||||
onSuccess: () => {
|
||||
return;
|
||||
},
|
||||
onError: () => {
|
||||
return;
|
||||
},
|
||||
}
|
||||
) => {
|
||||
const mutation = useMutation<
|
||||
ApiResponse<{ clientId: string; clientSecret: string }>,
|
||||
unknown,
|
||||
Omit<CreateOAuthClientInput, "permissions">
|
||||
>({
|
||||
mutationFn: (data) => {
|
||||
return fetch(`/api/v2/oauth-clients/${clientId}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
}).then((res) => res?.json());
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
if (data.status === SUCCESS_STATUS) {
|
||||
onSuccess?.();
|
||||
} else {
|
||||
onError?.();
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
onError?.();
|
||||
},
|
||||
});
|
||||
|
||||
return mutation;
|
||||
};
|
||||
|
||||
export const useDeleteOAuthClient = (
|
||||
{ onSuccess, onError }: IPersistOAuthClient = {
|
||||
onSuccess: () => {
|
||||
@@ -62,7 +99,7 @@ export const useDeleteOAuthClient = (
|
||||
return fetch(`/api/v2/oauth-clients/${id}`, {
|
||||
method: "delete",
|
||||
headers: { "Content-type": "application/json" },
|
||||
}).then((res) => res.json());
|
||||
}).then((res) => res?.json());
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
if (data.status === SUCCESS_STATUS) {
|
||||
|
||||
@@ -213,6 +213,9 @@ function getTeamWithoutMetadata<T extends Pick<Team, "metadata">>(team: T) {
|
||||
const teamMetadata = teamMetadataSchema.parse(metadata);
|
||||
return {
|
||||
...rest,
|
||||
requestedSlug: teamMetadata?.requestedSlug,
|
||||
// add requestedSlug if available.
|
||||
...(typeof teamMetadata?.requestedSlug !== "undefined"
|
||||
? { requestedSlug: teamMetadata?.requestedSlug }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -44,11 +44,12 @@ import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||
import { getIs24hClockFromLocalStorage, isBrowserLocale24h } from "@calcom/lib/timeFormat";
|
||||
import { localStorage } from "@calcom/lib/webstorage";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import { BookingStatus, SchedulingType } from "@calcom/prisma/enums";
|
||||
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import {
|
||||
Alert,
|
||||
Avatar,
|
||||
Badge,
|
||||
Button,
|
||||
EmailInput,
|
||||
@@ -225,6 +226,7 @@ export default function Success(props: PageProps) {
|
||||
|
||||
const giphyAppData = getEventTypeAppData(eventType, "giphy");
|
||||
const giphyImage = giphyAppData?.thankYouPage;
|
||||
const isRoundRobin = eventType.schedulingType === SchedulingType.ROUND_ROBIN;
|
||||
|
||||
const eventName = getEventName(eventNameObject, true);
|
||||
// Confirmation can be needed in two cases as of now
|
||||
@@ -294,6 +296,7 @@ export default function Success(props: PageProps) {
|
||||
|
||||
function getTitle(): string {
|
||||
const titleSuffix = props.recurringBookings ? "_recurring" : "";
|
||||
const titlePrefix = isRoundRobin ? "round_robin_" : "";
|
||||
if (isCancelled) {
|
||||
return "";
|
||||
}
|
||||
@@ -305,6 +308,11 @@ export default function Success(props: PageProps) {
|
||||
}
|
||||
return t(`needs_to_be_confirmed_or_rejected${titleSuffix}`);
|
||||
}
|
||||
if (bookingInfo.user) {
|
||||
return t(`${titlePrefix}emailed_you_and_attendees${titleSuffix}`, {
|
||||
user: bookingInfo.user.name || bookingInfo.user.email,
|
||||
});
|
||||
}
|
||||
return t(`emailed_you_and_attendees${titleSuffix}`);
|
||||
}
|
||||
|
||||
@@ -396,27 +404,36 @@ export default function Success(props: PageProps) {
|
||||
{!isFeedbackMode && (
|
||||
<>
|
||||
<div
|
||||
className={classNames(
|
||||
"mx-auto flex items-center justify-center",
|
||||
!giphyImage && !isCancelled && !needsConfirmation
|
||||
? "bg-success h-12 w-12 rounded-full"
|
||||
: "",
|
||||
!giphyImage && !isCancelled && needsConfirmation
|
||||
? "bg-subtle h-12 w-12 rounded-full"
|
||||
: "",
|
||||
isCancelled ? "bg-error h-12 w-12 rounded-full " : ""
|
||||
)}>
|
||||
{giphyImage && !needsConfirmation && !isCancelled && (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img src={giphyImage} alt="Gif from Giphy" />
|
||||
className={classNames(isRoundRobin && "min-w-32 min-h-24 relative mx-auto h-24 w-32")}>
|
||||
{isRoundRobin && bookingInfo.user && (
|
||||
<Avatar
|
||||
className="mx-auto flex items-center justify-center"
|
||||
alt={bookingInfo.user.name || bookingInfo.user.email}
|
||||
size="xl"
|
||||
imageSrc={`${bookingInfo.user.avatarUrl}`}
|
||||
/>
|
||||
)}
|
||||
{!giphyImage && !needsConfirmation && !isCancelled && (
|
||||
<Icon name="check" className="h-5 w-5 text-green-600 dark:text-green-400" />
|
||||
)}
|
||||
{needsConfirmation && !isCancelled && (
|
||||
<Icon name="calendar" className="text-emphasis h-5 w-5" />
|
||||
)}
|
||||
{isCancelled && <Icon name="x" className="h-5 w-5 text-red-600 dark:text-red-200" />}
|
||||
<div
|
||||
className={classNames(
|
||||
"mx-auto flex h-12 w-12 items-center justify-center rounded-full",
|
||||
isRoundRobin &&
|
||||
"border-cal-bg dark:border-cal-bg-muted absolute bottom-0 right-0 z-10 h-12 w-12 border-8",
|
||||
!giphyImage && !isCancelled && !needsConfirmation ? "bg-success" : "",
|
||||
!giphyImage && !isCancelled && needsConfirmation ? "bg-subtle" : "",
|
||||
isCancelled ? "bg-error" : ""
|
||||
)}>
|
||||
{giphyImage && !needsConfirmation && !isCancelled && (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img src={giphyImage} alt="Gif from Giphy" />
|
||||
)}
|
||||
{!giphyImage && !needsConfirmation && !isCancelled && (
|
||||
<Icon name="check" className="h-5 w-5 text-green-600 dark:text-green-400" />
|
||||
)}
|
||||
{needsConfirmation && !isCancelled && (
|
||||
<Icon name="calendar" className="text-emphasis h-5 w-5" />
|
||||
)}
|
||||
{isCancelled && <Icon name="x" className="h-5 w-5 text-red-600 dark:text-red-200" />}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-8 mt-6 text-center last:mb-0">
|
||||
<h3
|
||||
@@ -463,7 +480,7 @@ export default function Success(props: PageProps) {
|
||||
)}
|
||||
<div className="font-medium">{t("what")}</div>
|
||||
<div className="col-span-2 mb-6 last:mb-0" data-testid="booking-title">
|
||||
{eventName}
|
||||
{isRoundRobin ? bookingInfo.title : eventName}
|
||||
</div>
|
||||
<div className="font-medium">{t("when")}</div>
|
||||
<div className="col-span-2 mb-6 last:mb-0">
|
||||
|
||||
@@ -161,6 +161,7 @@ const nextConfig = {
|
||||
experimental: {
|
||||
// externalize server-side node_modules with size > 1mb, to improve dev mode performance/RAM usage
|
||||
serverComponentsExternalPackages: ["next-i18next"],
|
||||
optimizePackageImports: ["@calcom/ui"],
|
||||
},
|
||||
i18n: {
|
||||
...i18n,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "3.9.9",
|
||||
"version": "4.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
||||
@@ -25,15 +25,15 @@ function VerifyEmailChange(props: PageProps) {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
async function updateSession() {
|
||||
async function updateSessionAndDisplayToast() {
|
||||
await update({ email: props.updatedEmail });
|
||||
router.push("/event-types");
|
||||
}
|
||||
if (props.updateSession) {
|
||||
updateSession();
|
||||
if (isLocaleReady) {
|
||||
showToast(t("verify_email_change_success_toast", { email: props.updatedEmail }), "success");
|
||||
}
|
||||
router.push("/event-types");
|
||||
}
|
||||
if (props.updateSession) {
|
||||
updateSessionAndDisplayToast();
|
||||
} else {
|
||||
if (isLocaleReady) {
|
||||
showToast(t("verify_email_change_failure_toast"), "error");
|
||||
|
||||
@@ -1,16 +1,32 @@
|
||||
import { Troubleshooter } from "@calcom/features/troubleshooter/Troubleshooter";
|
||||
import dynamic from "next/dynamic";
|
||||
import { Suspense } from "react";
|
||||
|
||||
import { getLayout } from "@calcom/features/troubleshooter/layout";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { HeadSeo } from "@calcom/ui";
|
||||
import { HeadSeo, Loader } from "@calcom/ui";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
const TroubleshooterClientOnly = dynamic(
|
||||
() => import("@calcom/features/troubleshooter/Troubleshooter").then((mod) => mod.Troubleshooter),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
function TroubleshooterPage() {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<>
|
||||
<HeadSeo title={t("troubleshoot")} description={t("troubleshoot_availability")} />
|
||||
<Troubleshooter month={null} />
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Loader />
|
||||
</div>
|
||||
}>
|
||||
<TroubleshooterClientOnly month={null} />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,31 +3,23 @@
|
||||
import Head from "next/head";
|
||||
|
||||
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button } from "@calcom/ui";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
export default function MaintenancePage() {
|
||||
const { t, isLocaleReady } = useLocale();
|
||||
if (!isLocaleReady) return null;
|
||||
return (
|
||||
<div className="bg-subtle flex h-screen">
|
||||
<Head>
|
||||
<title>
|
||||
{t("under_maintenance")} | {APP_NAME}
|
||||
</title>
|
||||
<title>Under Maintenance | {APP_NAME}</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<div className="bg-default m-auto rounded-md p-10 text-right ltr:text-left">
|
||||
<h1 className="text-emphasis text-2xl font-medium">{t("under_maintenance")}</h1>
|
||||
<h1 className="text-emphasis text-2xl font-medium">Down for maintenance</h1>
|
||||
<p className="text-default mb-6 mt-4 max-w-2xl text-sm">
|
||||
{t("under_maintenance_description", { appName: APP_NAME })}
|
||||
The Cal.com team are performing scheduled maintenance. If you have any questions, please contact
|
||||
support.
|
||||
</p>
|
||||
<Button href={`${WEBSITE_URL}/support`}>{t("contact_support")}</Button>
|
||||
<Button href={`${WEBSITE_URL}/support`}>Contact Support</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
MaintenancePage.PageWrapper = PageWrapper;
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import React from "react";
|
||||
|
||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
import { OAuthClientForm } from "@components/settings/organizations/platform/oauth-clients/OAuthClientForm";
|
||||
|
||||
export const CreateOAuthClient = () => {
|
||||
const searchParams = useCompatSearchParams();
|
||||
const clientId = searchParams?.get("clientId") || "";
|
||||
return (
|
||||
<div>
|
||||
<OAuthClientForm />
|
||||
<OAuthClientForm clientId={clientId} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
|
||||
import { useRouter } from "next/router";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React from "react";
|
||||
|
||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||
@@ -62,8 +62,6 @@ export const OAuthClients = () => {
|
||||
<>
|
||||
<div className="border-subtle rounded-b-lg border border-t-0">
|
||||
{data.map((client, index) => {
|
||||
console.log(client);
|
||||
|
||||
return (
|
||||
<OAuthClientCard
|
||||
name={client.name}
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
bookFirstEvent,
|
||||
bookOptinEvent,
|
||||
bookTimeSlot,
|
||||
expectEmailsToHaveSubject,
|
||||
selectFirstAvailableTimeSlotNextMonth,
|
||||
testEmail,
|
||||
testName,
|
||||
@@ -62,6 +61,7 @@ testBothFutureAndLegacyRoutes.describe("free user", () => {
|
||||
|
||||
test("cannot book same slot multiple times", async ({ page, users, emails }) => {
|
||||
const [user] = users.get();
|
||||
|
||||
const bookerObj = {
|
||||
email: users.trackEmail({ username: "testEmail", domain: "example.com" }),
|
||||
name: "testBooker",
|
||||
@@ -80,12 +80,6 @@ testBothFutureAndLegacyRoutes.describe("free user", () => {
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
const { title: eventTitle } = await user.getFirstEventAsOwner();
|
||||
|
||||
await expectEmailsToHaveSubject({
|
||||
emails,
|
||||
organizer: user,
|
||||
booker: bookerObj,
|
||||
eventTitle,
|
||||
});
|
||||
await page.goto(bookingUrl);
|
||||
|
||||
// book same time spot again
|
||||
|
||||
@@ -103,7 +103,7 @@ test.describe("Bookings", () => {
|
||||
page,
|
||||
},
|
||||
async () => {
|
||||
await bookTeamEvent({ page, team, event: teamEvent });
|
||||
await bookTeamEvent({ page, team, event: teamEvent, teamMatesObj });
|
||||
|
||||
// Since all the users have the same leastRecentlyBooked value
|
||||
// Anyone of the teammates could be the Host of the booking.
|
||||
@@ -421,13 +421,15 @@ async function bookTeamEvent({
|
||||
page,
|
||||
team,
|
||||
event,
|
||||
teamMatesObj,
|
||||
}: {
|
||||
page: Page;
|
||||
team: {
|
||||
slug: string | null;
|
||||
name: string | null;
|
||||
};
|
||||
event: { slug: string; title: string };
|
||||
event: { slug: string; title: string; schedulingType: SchedulingType | null };
|
||||
teamMatesObj?: { name: string }[];
|
||||
}) {
|
||||
// Note that even though the default way to access a team booking in an organization is to not use /team in the URL, but it isn't testable with playwright as the rewrite is taken care of by Next.js config which can't handle on the fly org slug's handling
|
||||
// So, we are using /team in the URL to access the team booking
|
||||
@@ -440,8 +442,19 @@ async function bookTeamEvent({
|
||||
await expect(page.getByTestId("success-page")).toBeVisible();
|
||||
|
||||
// The title of the booking
|
||||
const BookingTitle = `${event.title} between ${team.name} and ${testName}`;
|
||||
await expect(page.getByTestId("booking-title")).toHaveText(BookingTitle);
|
||||
if (event.schedulingType === SchedulingType.ROUND_ROBIN) {
|
||||
const bookingTitle = await page.getByTestId("booking-title").textContent();
|
||||
|
||||
expect(
|
||||
teamMatesObj?.some((teamMate) => {
|
||||
const BookingTitle = `${event.title} between ${teamMate.name} and ${testName}`;
|
||||
return BookingTitle === bookingTitle;
|
||||
})
|
||||
).toBe(true);
|
||||
} else {
|
||||
const BookingTitle = `${event.title} between ${team.name} and ${testName}`;
|
||||
await expect(page.getByTestId("booking-title")).toHaveText(BookingTitle);
|
||||
}
|
||||
// The booker should be in the attendee list
|
||||
await expect(page.getByTestId(`attendee-name-${testName}`)).toHaveText(testName);
|
||||
}
|
||||
|
||||
@@ -147,8 +147,13 @@ testBothFutureAndLegacyRoutes.describe("Teams - NonOrg", (routeVariant) => {
|
||||
await expect(page.locator(`[data-testid="attendee-name-${testName}"]`)).toHaveText(testName);
|
||||
|
||||
// The title of the booking
|
||||
const BookingTitle = `${teamEventTitle} between ${team.name} and ${testName}`;
|
||||
await expect(page.locator("[data-testid=booking-title]")).toHaveText(BookingTitle);
|
||||
const bookingTitle = await page.getByTestId("booking-title").textContent();
|
||||
expect(
|
||||
teamMatesObj?.some((teamMate) => {
|
||||
const BookingTitle = `${teamEventTitle} between ${teamMate.name} and ${testName}`;
|
||||
return BookingTitle === bookingTitle;
|
||||
})
|
||||
).toBe(true);
|
||||
|
||||
// Since all the users have the same leastRecentlyBooked value
|
||||
// Anyone of the teammates could be the Host of the booking.
|
||||
|
||||
@@ -326,8 +326,10 @@
|
||||
"add_another_calendar": "Add another calendar",
|
||||
"other": "Other",
|
||||
"email_sign_in_subject": "Your sign-in link for {{appName}}",
|
||||
"round_robin_emailed_you_and_attendees": "You are meeting with {{user}}. We sent an email with a calendar invitation with the details to everyone.",
|
||||
"emailed_you_and_attendees": "We sent an email with a calendar invitation with the details to everyone.",
|
||||
"emailed_you_and_attendees_recurring": "We sent an email with a calendar invitation with the details to everyone for the first of these recurring events.",
|
||||
"round_robin_emailed_you_and_attendees_recurring": "You are meeting with {{user}}.We sent an email with a calendar invitation with the details to everyone for the first of these recurring events.",
|
||||
"emailed_you_and_any_other_attendees": "We sent an email to everyone with this information.",
|
||||
"needs_to_be_confirmed_or_rejected": "Your booking still needs to be confirmed or rejected.",
|
||||
"needs_to_be_confirmed_or_rejected_recurring": "Your recurring meeting still needs to be confirmed or rejected.",
|
||||
|
||||
@@ -2301,6 +2301,9 @@
|
||||
"field_identifiers_as_variables": "להשתמש במזהי השדות כמשתנים להפניות האירועים בהתאמה אישית",
|
||||
"field_identifiers_as_variables_with_example": "להשתמש במזהי השדות כמשתנים להפניות האירועים בהתאמה אישית (למשל: {{variable}})",
|
||||
"account_already_linked": "החשבון כבר מקושר",
|
||||
"travel_schedule": "תזמון טיול",
|
||||
"schedule_timezone_change": "תזמון שינוי אזור זמן",
|
||||
"date": "תאריך",
|
||||
"email_team_invite|subject|invited_to_subteam": "הוזמנת על ידי {{user}} להצטרף לצוות {{team}} של הארגון {{parentTeamName}} אצל {{appName}}",
|
||||
"email_team_invite|subject|invited_to_regular_team": "{{user}} הזמין אותך להצטרף לצוות {{team}} ב-{{appName}}",
|
||||
"email_team_invite|heading|invited_to_subteam": "הוזמנת לצוות בארגון {{parentTeamName}}",
|
||||
|
||||
@@ -81,6 +81,7 @@ export async function ssrInit(context: GetServerSidePropsContext, options?: { no
|
||||
// Provides a better UX to the users who have already upgraded.
|
||||
ssr.viewer.teams.hasTeamPlan.prefetch(),
|
||||
ssr.viewer.public.session.prefetch(),
|
||||
ssr.viewer.me.prefetch(),
|
||||
]);
|
||||
|
||||
return ssr;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { DestinationCalendar } from "@prisma/client";
|
||||
|
||||
import { metadata as OutlookMetadata } from "../../office365calendar";
|
||||
|
||||
/**
|
||||
* When inviting attendees to a calendar event, sometimes the external ID is only used for internal purposes
|
||||
* Need to process the correct external ID for the calendar service
|
||||
*/
|
||||
const processExternalId = (destinationCalendar: DestinationCalendar) => {
|
||||
if (destinationCalendar.integration === OutlookMetadata.type) {
|
||||
// Primary email should always be present for Outlook
|
||||
return destinationCalendar.primaryEmail || destinationCalendar.externalId;
|
||||
}
|
||||
|
||||
return destinationCalendar.externalId;
|
||||
};
|
||||
|
||||
export default processExternalId;
|
||||
@@ -346,6 +346,7 @@ export const getLocationValueForDB = (
|
||||
) => {
|
||||
let bookingLocation = bookingLocationTypeOrValue;
|
||||
let conferenceCredentialId = undefined;
|
||||
|
||||
eventLocations.forEach((location) => {
|
||||
if (location.type === bookingLocationTypeOrValue) {
|
||||
const eventLocationType = getEventLocationType(bookingLocationTypeOrValue);
|
||||
@@ -362,6 +363,11 @@ export const getLocationValueForDB = (
|
||||
bookingLocation = location[eventLocationType.defaultValueVariable] || bookingLocation;
|
||||
}
|
||||
});
|
||||
|
||||
if (bookingLocation.trim().length === 0) {
|
||||
bookingLocation = DailyLocationType;
|
||||
}
|
||||
|
||||
return { bookingLocation, conferenceCredentialId };
|
||||
};
|
||||
|
||||
|
||||
@@ -67,6 +67,8 @@ module.exports = {
|
||||
booker: `var(--cal-border-booker, ${subtleColor})`,
|
||||
error: "var(--cal-border-error, #AA2E26)",
|
||||
focus: "var(--cal-border-focus, #1A1A1A)",
|
||||
"cal-bg": "var(--cal-bg, white)",
|
||||
"cal-bg-muted": "var(--cal-bg-muted)",
|
||||
},
|
||||
textColor: {
|
||||
emphasis: "var(--cal-text-emphasis, #111827)",
|
||||
|
||||
@@ -535,6 +535,7 @@ export default class EventManager {
|
||||
};
|
||||
|
||||
if (event.destinationCalendar && event.destinationCalendar.length > 0) {
|
||||
let eventCreated = false;
|
||||
// Since GCal pushes events to multiple calendars we only want to create one event per booking
|
||||
let gCalAdded = false;
|
||||
const destinationCalendars: DestinationCalendar[] = event.destinationCalendar.reduce(
|
||||
@@ -554,6 +555,7 @@ export default class EventManager {
|
||||
[] as DestinationCalendar[]
|
||||
);
|
||||
for (const destination of destinationCalendars) {
|
||||
if (eventCreated) break;
|
||||
log.silly("Creating Calendar event", JSON.stringify({ destination }));
|
||||
if (destination.credentialId) {
|
||||
let credential = this.calendarCredentials.find((c) => c.id === destination.credentialId);
|
||||
@@ -582,6 +584,7 @@ export default class EventManager {
|
||||
const createdEvent = await createEvent(credential, event, destination.externalId);
|
||||
if (createdEvent) {
|
||||
createdEvents.push(createdEvent);
|
||||
eventCreated = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -607,6 +610,7 @@ export default class EventManager {
|
||||
})
|
||||
);
|
||||
createdEvents.push(await createEvent(firstCalendarCredential, event));
|
||||
eventCreated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ export const sanitizeDisplayName = (nameAndEmail: string) => {
|
||||
};
|
||||
|
||||
const sanitize = (name: string) => {
|
||||
const charsToReplace = /[;,"<>()]/g;
|
||||
const charsToReplace = /[;,"<>():]/g;
|
||||
|
||||
return name.replace(charsToReplace, " ");
|
||||
return name.replace(charsToReplace, " ").replace(/\s+/g, " ");
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ const recommended = {
|
||||
parserOptions: { sourceType: "module" },
|
||||
rules: {
|
||||
"@calcom/eslint/deprecated-imports": "error",
|
||||
"@calcom/eslint/deprecated-imports-next-router": "error",
|
||||
"@calcom/eslint/avoid-web-storage": "error",
|
||||
"@calcom/eslint/avoid-prisma-client-import-for-enums": "error",
|
||||
},
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { ESLintUtils } from "@typescript-eslint/utils";
|
||||
|
||||
const createRule = ESLintUtils.RuleCreator((name) => `https://developer.cal.com/eslint/rule/${name}`);
|
||||
|
||||
const rule = createRule({
|
||||
name: "deprecated-imports-next-router",
|
||||
meta: {
|
||||
fixable: "code",
|
||||
docs: {
|
||||
description: "Importing router from 'next/router' is deprecated, use 'next/navigation' instead",
|
||||
recommended: "error",
|
||||
},
|
||||
messages: {
|
||||
"deprecated-next-router":
|
||||
"Importing router from 'next/router' is deprecated, use 'next/navigation' instead",
|
||||
},
|
||||
type: "problem",
|
||||
schema: [],
|
||||
},
|
||||
defaultOptions: [],
|
||||
create(context) {
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
if (node.source.value === "next/router") {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "deprecated-next-router",
|
||||
fix: function (fixer) {
|
||||
return fixer.replaceText(node.source, "'next/navigation'");
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default rule;
|
||||
@@ -5,4 +5,5 @@ export default {
|
||||
"deprecated-imports": require("./deprecated-imports").default,
|
||||
"avoid-web-storage": require("./avoid-web-storage").default,
|
||||
"avoid-prisma-client-import-for-enums": require("./avoid-prisma-client-import-for-enums").default,
|
||||
"deprecated-imports-next-router": require("./deprecated-imports-next-router").default,
|
||||
} as ESLint.Plugin["rules"];
|
||||
|
||||
@@ -6,7 +6,7 @@ import z from "zod";
|
||||
import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button, Icon } from "@calcom/ui";
|
||||
import { Button } from "@calcom/ui";
|
||||
|
||||
interface Props {
|
||||
samlTenantID: string;
|
||||
|
||||
@@ -247,18 +247,6 @@ const BookerComponent = ({
|
||||
<>
|
||||
{event.data && !isPlatform ? <BookingPageTagManager eventType={event.data} /> : <></>}
|
||||
|
||||
{bookerState !== "booking" && event.data?.isInstantEvent && (
|
||||
<div
|
||||
className="animate-fade-in-up fixed bottom-2 z-40 my-2 opacity-0"
|
||||
style={{ animationDelay: "1s" }}>
|
||||
<InstantBooking
|
||||
event={event.data}
|
||||
onConnectNow={() => {
|
||||
onConnectNowInstantMeeting();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={classNames(
|
||||
// In a popup embed, if someone clicks outside the main(having main class or main tag), it closes the embed
|
||||
@@ -439,6 +427,22 @@ const BookerComponent = ({
|
||||
setDayCount(null);
|
||||
}}
|
||||
/>
|
||||
|
||||
{bookerState !== "booking" && event.data?.isInstantEvent && (
|
||||
<div
|
||||
className={classNames(
|
||||
"animate-fade-in-up z-40 my-2 opacity-0",
|
||||
layout === BookerLayouts.MONTH_VIEW && isEmbed ? "" : "fixed bottom-2"
|
||||
)}
|
||||
style={{ animationDelay: "1s" }}>
|
||||
<InstantBooking
|
||||
event={event.data}
|
||||
onConnectNow={() => {
|
||||
onConnectNowInstantMeeting();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!hideBranding && !isPlatform && (
|
||||
<m.span
|
||||
key="logo"
|
||||
|
||||
-1
@@ -9,7 +9,6 @@ import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
EmptyScreen,
|
||||
Icon,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemTitle,
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as HoverCard from "@radix-ui/react-hover-card";
|
||||
import { AnimatePresence, m } from "framer-motion";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
import { useIsPlatform } from "@calcom/atoms/monorepo";
|
||||
import type { IOutOfOfficeData } from "@calcom/core/getUserAvailability";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { OutOfOfficeInSlots } from "@calcom/features/bookings/Booker/components/OutOfOfficeInSlots";
|
||||
@@ -255,8 +256,10 @@ interface IOOOSlotProps {
|
||||
}
|
||||
|
||||
const OOOSlot: React.FC<IOOOSlotProps> = (props) => {
|
||||
const isPlatform = useIsPlatform();
|
||||
const { fromUser, toUser, reason, emoji, time, className = "" } = props;
|
||||
|
||||
if (isPlatform) return <></>;
|
||||
return (
|
||||
<OutOfOfficeInSlots
|
||||
fromUser={fromUser}
|
||||
|
||||
@@ -29,6 +29,7 @@ const getUserBooking = async (uid: string) => {
|
||||
email: true,
|
||||
username: true,
|
||||
timeZone: true,
|
||||
avatarUrl: true,
|
||||
},
|
||||
},
|
||||
attendees: {
|
||||
@@ -44,6 +45,7 @@ const getUserBooking = async (uid: string) => {
|
||||
eventName: true,
|
||||
slug: true,
|
||||
timeZone: true,
|
||||
schedulingType: true,
|
||||
},
|
||||
},
|
||||
seatsReferences: {
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { Logger } from "tslog";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
import z from "zod";
|
||||
|
||||
import processExternalId from "@calcom/app-store/_utils/calendars/processExternalId";
|
||||
import { metadata as GoogleMeetMetadata } from "@calcom/app-store/googlevideo/_metadata";
|
||||
import type { LocationObject } from "@calcom/app-store/locations";
|
||||
import {
|
||||
@@ -1405,9 +1406,13 @@ async function handler(
|
||||
|
||||
// Organizer or user owner of this event type it's not listed as a team member.
|
||||
const teamMemberPromises = users.slice(1).map(async (user) => {
|
||||
// TODO: Add back once EventManager tests are ready https://github.com/calcom/cal.com/pull/14610#discussion_r1567817120
|
||||
// push to teamDestinationCalendars if it's a team event but collective only
|
||||
if (isTeamEventType && eventType.schedulingType === "COLLECTIVE" && user.destinationCalendar) {
|
||||
teamDestinationCalendars.push(user.destinationCalendar);
|
||||
teamDestinationCalendars.push({
|
||||
...user.destinationCalendar,
|
||||
externalId: processExternalId(user.destinationCalendar),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -2416,6 +2416,150 @@ describe("handleNewBooking", () => {
|
||||
},
|
||||
timeout
|
||||
);
|
||||
test(
|
||||
`cannot book same slot multiple times `,
|
||||
async ({ emails }) => {
|
||||
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
|
||||
const booker = getBooker({
|
||||
email: "booker@example.com",
|
||||
name: "Booker",
|
||||
});
|
||||
|
||||
const organizerOtherEmail = "organizer2@example.com";
|
||||
const organizerDestinationCalendarEmailOnEventType = "organizerEventTypeEmail@example.com";
|
||||
|
||||
const organizer = getOrganizer({
|
||||
name: "Organizer",
|
||||
email: "organizer@example.com",
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
destinationCalendar: {
|
||||
integration: "google_calendar",
|
||||
externalId: "organizer@google-calendar.com",
|
||||
primaryEmail: organizerOtherEmail,
|
||||
},
|
||||
});
|
||||
|
||||
await createBookingScenario(
|
||||
getScenarioData({
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slotInterval: 30,
|
||||
length: 30,
|
||||
useEventTypeDestinationCalendarEmail: true,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
destinationCalendar: {
|
||||
integration: "google_calendar",
|
||||
externalId: "event-type-1@google-calendar.com",
|
||||
primaryEmail: organizerDestinationCalendarEmailOnEventType,
|
||||
},
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
|
||||
})
|
||||
);
|
||||
|
||||
mockSuccessfulVideoMeetingCreation({
|
||||
metadataLookupKey: "dailyvideo",
|
||||
videoMeetingData: {
|
||||
id: "MOCK_ID",
|
||||
password: "MOCK_PASS",
|
||||
url: `http://mock-dailyvideo.example.com/meeting-1`,
|
||||
},
|
||||
});
|
||||
|
||||
const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", {
|
||||
create: {
|
||||
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
|
||||
},
|
||||
});
|
||||
|
||||
const mockBookingData = getMockRequestDataForBooking({
|
||||
data: {
|
||||
user: organizer.username,
|
||||
eventTypeId: 1,
|
||||
responses: {
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
location: { optionValue: "", value: BookingLocations.CalVideo },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { req } = createMockNextJsRequest({
|
||||
method: "POST",
|
||||
body: mockBookingData,
|
||||
});
|
||||
|
||||
const createdBooking = await handleNewBooking(req);
|
||||
|
||||
expect(createdBooking.responses).toContain({
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
});
|
||||
|
||||
expect(createdBooking).toContain({
|
||||
location: BookingLocations.CalVideo,
|
||||
});
|
||||
|
||||
await expectBookingToBeInDatabase({
|
||||
description: "",
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
uid: createdBooking.uid!,
|
||||
eventTypeId: mockBookingData.eventTypeId,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
references: [
|
||||
{
|
||||
type: appStoreMetadata.dailyvideo.type,
|
||||
uid: "MOCK_ID",
|
||||
meetingId: "MOCK_ID",
|
||||
meetingPassword: "MOCK_PASS",
|
||||
meetingUrl: "http://mock-dailyvideo.example.com/meeting-1",
|
||||
},
|
||||
{
|
||||
type: appStoreMetadata.googlecalendar.type,
|
||||
uid: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
|
||||
meetingId: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
|
||||
meetingPassword: "MOCK_PASSWORD",
|
||||
meetingUrl: "https://UNUSED_URL",
|
||||
},
|
||||
],
|
||||
iCalUID: createdBooking.iCalUID,
|
||||
});
|
||||
|
||||
expectSuccessfulCalendarEventCreationInCalendar(calendarMock, {
|
||||
calendarId: "event-type-1@google-calendar.com",
|
||||
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
|
||||
});
|
||||
|
||||
const iCalUID = expectICalUIDAsString(createdBooking.iCalUID);
|
||||
|
||||
expectSuccessfulBookingCreationEmails({
|
||||
booking: {
|
||||
uid: createdBooking.uid!,
|
||||
urlOrigin: WEBSITE_URL,
|
||||
},
|
||||
booker,
|
||||
organizer,
|
||||
emails,
|
||||
iCalUID,
|
||||
destinationEmail: organizerDestinationCalendarEmailOnEventType,
|
||||
});
|
||||
|
||||
await expect(async () => await handleNewBooking(req)).rejects.toThrowError(
|
||||
ErrorCode.NoAvailableUsersFound
|
||||
);
|
||||
},
|
||||
timeout
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { Button, ButtonGroup, Icon } from "@calcom/ui";
|
||||
import { Button, ButtonGroup } from "@calcom/ui";
|
||||
|
||||
import { useCalendarStore } from "../../state/store";
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { Directory } from "@boxyhq/saml-jackson";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, showToast, Label, Tooltip } from "@calcom/ui";
|
||||
import { Icon } from "@calcom/ui";
|
||||
|
||||
const DirectoryInfo = ({ directory }: { directory: Directory }) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
@@ -203,7 +203,7 @@ const CreateANewOrganizationFormChild = ({ session }: { session: Ensure<SessionC
|
||||
name="seats"
|
||||
type="number"
|
||||
label="Seats (optional)"
|
||||
min={MINIMUM_NUMBER_OF_ORG_SEATS}
|
||||
min={isAdmin ? 1 : MINIMUM_NUMBER_OF_ORG_SEATS}
|
||||
defaultValue={value || MINIMUM_NUMBER_OF_ORG_SEATS}
|
||||
onChange={(e) => {
|
||||
onChange(+e.target.value);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { useIsPlatform } from "@calcom/atoms/monorepo";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
@@ -12,6 +13,8 @@ export type OrgUpgradeBannerProps = {
|
||||
export function OrgUpgradeBanner({ data }: OrgUpgradeBannerProps) {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const isPlatform = useIsPlatform();
|
||||
|
||||
const publishOrgMutation = trpc.viewer.organizations.publish.useMutation({
|
||||
onSuccess(data) {
|
||||
router.push(data.url);
|
||||
@@ -25,6 +28,9 @@ export function OrgUpgradeBanner({ data }: OrgUpgradeBannerProps) {
|
||||
const [membership] = data;
|
||||
if (!membership) return null;
|
||||
|
||||
// TODO: later figure out how to not show this banner on platform since platform is different to orgs (it just uses the same code)
|
||||
if (isPlatform) return null;
|
||||
|
||||
return (
|
||||
<TopBanner
|
||||
text={t("org_upgrade_banner_description", { teamName: membership.team.name })}
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
Icon,
|
||||
Tooltip,
|
||||
UserAvatar,
|
||||
} from "@calcom/ui";
|
||||
@@ -82,12 +81,7 @@ export default function MemberListItem(props: Props) {
|
||||
<div className="flex md:hidden">
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="icon"
|
||||
color="minimal"
|
||||
StartIcon="ellipsis"
|
||||
/>
|
||||
<Button type="button" variant="icon" color="minimal" StartIcon="ellipsis" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem className="outline-none">
|
||||
|
||||
@@ -28,6 +28,7 @@ export const useIntercom = () => {
|
||||
const { hasTeamPlan } = useHasTeamPlan();
|
||||
|
||||
const boot = async () => {
|
||||
if (!data) return;
|
||||
let userHash;
|
||||
|
||||
const req = await fetch(`/api/intercom-hash`);
|
||||
|
||||
@@ -155,6 +155,7 @@ export const purchaseTeamOrOrgSubscription = async (input: {
|
||||
currency: priceObj.currency,
|
||||
recurring: { interval: "month" }, // Define your subscription interval
|
||||
product: typeof priceObj.product === "string" ? priceObj.product : priceObj.product.id,
|
||||
tax_behavior: "exclusive",
|
||||
});
|
||||
return customPriceObj.id;
|
||||
} catch (e) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { classNames } from "@calcom/lib";
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { Avatar, Badge, Button, ButtonGroup, Icon, Select, Switch, Tooltip } from "@calcom/ui";
|
||||
import { Avatar, Badge, Button, ButtonGroup, Select, Switch, Tooltip } from "@calcom/ui";
|
||||
|
||||
export type ChildrenEventType = {
|
||||
value: string;
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
RadioGroup as RadioArea,
|
||||
showToast,
|
||||
TextField,
|
||||
Tooltip,
|
||||
} from "@calcom/ui";
|
||||
|
||||
// this describes the uniform data needed to create a new event type on Profile or Team
|
||||
@@ -197,7 +198,13 @@ export default function CreateEventTypeDialog({
|
||||
<TextField
|
||||
label={`${t("url")}: ${urlPrefix}`}
|
||||
required
|
||||
addOnLeading={<>/{!isManagedEventType ? pageSlug : t("username_placeholder")}/</>}
|
||||
addOnLeading={
|
||||
<Tooltip content={!isManagedEventType ? pageSlug : t("username_placeholder")}>
|
||||
<span className="max-w-24 md:max-w-56">
|
||||
/{!isManagedEventType ? pageSlug : t("username_placeholder")}/
|
||||
</span>
|
||||
</Tooltip>
|
||||
}
|
||||
{...register("slug")}
|
||||
onChange={(e) => {
|
||||
form.setValue("slug", slugify(e?.target.value), { shouldTouch: true });
|
||||
@@ -214,9 +221,12 @@ export default function CreateEventTypeDialog({
|
||||
label={t("url")}
|
||||
required
|
||||
addOnLeading={
|
||||
<>
|
||||
{urlPrefix}/{!isManagedEventType ? pageSlug : t("username_placeholder")}/
|
||||
</>
|
||||
<Tooltip
|
||||
content={`${urlPrefix}/{!isManagedEventType ? pageSlug : t("username_placeholder")}/`}>
|
||||
<span className="max-w-24 md:max-w-56">
|
||||
{urlPrefix}/{!isManagedEventType ? pageSlug : t("username_placeholder")}/
|
||||
</span>
|
||||
</Tooltip>
|
||||
}
|
||||
{...register("slug")}
|
||||
/>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useFilterContext } from "@calcom/features/insights/context/provider";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { RouterOutputs } from "@calcom/trpc";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
import { Button, Dropdown, DropdownItem, DropdownMenuContent, DropdownMenuTrigger, Icon } from "@calcom/ui";
|
||||
import { Button, Dropdown, DropdownItem, DropdownMenuContent, DropdownMenuTrigger } from "@calcom/ui";
|
||||
|
||||
const Download = () => {
|
||||
const { filter } = useFilterContext();
|
||||
|
||||
@@ -2,7 +2,7 @@ import dayjs from "@calcom/dayjs";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import type { TimeRange, WorkingHours } from "@calcom/types/schedule";
|
||||
import { Button, DialogTrigger, Icon, Tooltip } from "@calcom/ui";
|
||||
import { Button, DialogTrigger, Tooltip } from "@calcom/ui";
|
||||
|
||||
import DateOverrideInputDialog from "./DateOverrideInputDialog";
|
||||
|
||||
|
||||
@@ -53,7 +53,6 @@ import { useFormbricks } from "@calcom/lib/formbricks-client";
|
||||
import getBrandColours from "@calcom/lib/getBrandColours";
|
||||
import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import { isKeyInObject } from "@calcom/lib/isKeyInObject";
|
||||
import { localStorage } from "@calcom/lib/webstorage";
|
||||
@@ -216,15 +215,17 @@ const useBanners = () => {
|
||||
const Layout = (props: LayoutProps) => {
|
||||
const banners = useBanners();
|
||||
|
||||
const showIntercom = localStorage.getItem("showIntercom");
|
||||
const isMobile = useMediaQuery("(max-width: 768px)");
|
||||
const { data: user } = trpc.viewer.me.useQuery();
|
||||
const { boot } = useIntercom();
|
||||
const pageTitle = typeof props.heading === "string" && !props.title ? props.heading : props.title;
|
||||
|
||||
useEffect(() => {
|
||||
if (showIntercom === "false" || isMobile) return;
|
||||
// not using useMediaQuery as it toggles between true and false
|
||||
const showIntercom = localStorage.getItem("showIntercom");
|
||||
if (showIntercom === "false" || window.innerWidth <= 768 || !user) return;
|
||||
boot();
|
||||
}, [showIntercom, isMobile]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [user]);
|
||||
|
||||
const bannersHeight = useMemo(() => {
|
||||
const activeBanners =
|
||||
|
||||
@@ -71,11 +71,7 @@ const DateOverride = ({ workingHours, disabled }: { workingHours: WorkingHours[]
|
||||
excludedDates={excludedDates}
|
||||
onChange={(ranges) => ranges.forEach((range) => append({ ranges: [range] }))}
|
||||
Trigger={
|
||||
<Button
|
||||
color="secondary"
|
||||
StartIcon="plus"
|
||||
data-testid="add-override"
|
||||
disabled={disabled}>
|
||||
<Button color="secondary" StartIcon="plus" data-testid="add-override" disabled={disabled}>
|
||||
{t("add_an_override")}
|
||||
</Button>
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
import type { UserProfile } from "@calcom/types/UserProfile";
|
||||
import { Button, ButtonGroup, DataTable, Icon, UserAvatar } from "@calcom/ui";
|
||||
import { Button, ButtonGroup, DataTable, UserAvatar } from "@calcom/ui";
|
||||
|
||||
import { UpgradeTip } from "../../tips/UpgradeTip";
|
||||
import { createTimezoneBuddyStore, TBContext } from "../store";
|
||||
|
||||
@@ -79,14 +79,14 @@ function EmptyCalendarToggleItem() {
|
||||
|
||||
export function CalendarToggleContainer() {
|
||||
const { t } = useLocale();
|
||||
const { data, isPending } = trpc.viewer.connectedCalendars.useQuery();
|
||||
const { data, isLoading } = trpc.viewer.connectedCalendars.useQuery();
|
||||
|
||||
const hasConnectedCalendars = data && data?.connectedCalendars.length > 0;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-3">
|
||||
<p className="text-sm font-medium leading-none">{t("calendars_were_checking_for_conflicts")}</p>
|
||||
{hasConnectedCalendars && !isPending ? (
|
||||
{hasConnectedCalendars && !isLoading ? (
|
||||
<>
|
||||
{data.connectedCalendars.map((calendar) => {
|
||||
const foundPrimary = calendar.calendars?.find((item) => item.primary);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useMemo } from "react";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, ButtonGroup, Icon } from "@calcom/ui";
|
||||
import { Button, ButtonGroup } from "@calcom/ui";
|
||||
|
||||
import { useTroubleshooterStore } from "../store";
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ZodError } from "zod";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { ZTestTriggerInputSchema } from "@calcom/trpc/server/routers/viewer/webhook/testTrigger.schema";
|
||||
import { Badge, Button, Icon, showToast } from "@calcom/ui";
|
||||
import { Badge, Button, showToast } from "@calcom/ui";
|
||||
|
||||
export default function WebhookTestDisclosure() {
|
||||
const [subscriberUrl, webhookSecret]: [string, string] = useWatch({ name: ["subscriberUrl", "secret"] });
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { DefaultSeoProps, NextSeoProps } from "next-seo";
|
||||
import type { Router } from "next/router";
|
||||
|
||||
import { APP_NAME, SEO_IMG_DEFAULT, SEO_IMG_OGIMG } from "@calcom/lib/constants";
|
||||
|
||||
@@ -47,12 +46,6 @@ export const seoConfig: {
|
||||
* @param path NextJS' useRouter().asPath
|
||||
* @returns
|
||||
*/
|
||||
export const buildCanonical = ({
|
||||
origin,
|
||||
path,
|
||||
}: {
|
||||
origin: Location["origin"];
|
||||
path: Router["asPath"] | null;
|
||||
}) => {
|
||||
export const buildCanonical = ({ origin, path }: { origin: Location["origin"]; path: string | null }) => {
|
||||
return `${origin}${path === "/" ? "" : path}`.split("?")[0];
|
||||
};
|
||||
|
||||
@@ -183,10 +183,7 @@ const DateOverride = ({
|
||||
onChange={(ranges) => ranges.forEach((range) => append({ ranges: [range] }))}
|
||||
userTimeFormat={userTimeFormat}
|
||||
Trigger={
|
||||
<Button
|
||||
color="secondary"
|
||||
StartIcon="plus"
|
||||
data-testid="add-override">
|
||||
<Button color="secondary" StartIcon="plus" data-testid="add-override">
|
||||
{t("add_an_override")}
|
||||
</Button>
|
||||
}
|
||||
@@ -330,11 +327,7 @@ export function AvailabilitySettings({
|
||||
openSidebar ? "translate-x-0 opacity-100" : "translate-x-full opacity-0"
|
||||
)}>
|
||||
<div className="flex flex-row items-center pt-5">
|
||||
<Button
|
||||
StartIcon="arrow-left"
|
||||
color="minimal"
|
||||
onClick={() => setOpenSidebar(false)}
|
||||
/>
|
||||
<Button StartIcon="arrow-left" color="minimal" onClick={() => setOpenSidebar(false)} />
|
||||
<p className="-ml-2">{t("availability_settings")}</p>
|
||||
<DeleteDialogButton
|
||||
buttonClassName="ml-16 inline"
|
||||
|
||||
@@ -8,12 +8,26 @@ const queryClient = new QueryClient();
|
||||
export type CalProviderProps = {
|
||||
children?: ReactNode;
|
||||
clientId: string;
|
||||
accessToken: string;
|
||||
accessToken?: string;
|
||||
options: { refreshUrl?: string; apiUrl: string };
|
||||
autoUpdateTimezone?: boolean;
|
||||
onTimezoneChange?: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a CalProvider component.
|
||||
*
|
||||
* @component
|
||||
* @param {string} props.clientId - The platform oauth client ID.
|
||||
* @param {string} props.accessToken - The access token of your managed user. - Optional
|
||||
* @param {object} props.options - The options object.
|
||||
* @param {string} [options.apiUrl] - The API URL. https://api.cal.com/v2
|
||||
* @param {string} [options.refreshUrl] - The url point to your refresh endpoint. - Optional, required if accessToken is provided.
|
||||
* @param {boolean} [autoUpdateTimezone=true] - Whether to automatically update the timezone. - Optional
|
||||
* @param {function} props.onTimezoneChange - The callback function for timezone change. - Optional
|
||||
* @param {ReactNode} props.children - The child components. - Optional
|
||||
* @returns {JSX.Element} The rendered CalProvider component.
|
||||
*/
|
||||
export function CalProvider({
|
||||
clientId,
|
||||
accessToken,
|
||||
|
||||
@@ -21,7 +21,7 @@ export const useCalendarsBusyTimes = ({ onError, enabled, ...rest }: UseCalendar
|
||||
],
|
||||
queryFn: () => {
|
||||
return http
|
||||
.get<ApiResponse<EventBusyDate[]>>("/ee/calendars/busy-times", {
|
||||
.get<ApiResponse<EventBusyDate[]>>("/calendars/busy-times", {
|
||||
params: rest,
|
||||
})
|
||||
.then((res) => {
|
||||
|
||||
@@ -26,7 +26,7 @@ export const useCancelBooking = (
|
||||
) => {
|
||||
const cancelBooking = useMutation<ApiResponse, Error, inputParams>({
|
||||
mutationFn: (data) => {
|
||||
return http.post<ApiResponse>(`/ee/bookings/${data.id}/cancel`, data).then((res) => {
|
||||
return http.post<ApiResponse>(`/bookings/${data.id}/cancel`, data).then((res) => {
|
||||
if (res.data.status === SUCCESS_STATUS) {
|
||||
return res.data;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export const useConnectedCalendars = (props: { enabled?: boolean }) => {
|
||||
const calendars = useQuery({
|
||||
queryKey: [QUERY_KEY],
|
||||
queryFn: () => {
|
||||
return http.get<ApiResponse<ConnectedDestinationCalendars>>("/ee/calendars").then((res) => {
|
||||
return http.get<ApiResponse<ConnectedDestinationCalendars>>("/calendars").then((res) => {
|
||||
if (res.data.status === SUCCESS_STATUS) {
|
||||
return (res.data as ApiSuccessResponse<ConnectedDestinationCalendars>)?.data;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export const useCreateBooking = (
|
||||
) => {
|
||||
const createBooking = useMutation<ApiResponse<BookingResponse>, Error, BookingCreateBody>({
|
||||
mutationFn: (data) => {
|
||||
return http.post<ApiResponse<BookingResponse>>("/ee/bookings", data).then((res) => {
|
||||
return http.post<ApiResponse<BookingResponse>>("/bookings", data).then((res) => {
|
||||
if (res.data.status === SUCCESS_STATUS) {
|
||||
return res.data;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export const useCreateInstantBooking = (
|
||||
) => {
|
||||
const createInstantBooking = useMutation<ApiResponse<BookingResponse>, Error, BookingCreateBody>({
|
||||
mutationFn: (data) => {
|
||||
return http.post<ApiResponse<BookingResponse>>("/ee/bookings/instant", data).then((res) => {
|
||||
return http.post<ApiResponse<BookingResponse>>("/bookings/instant", data).then((res) => {
|
||||
if (res.data.status === SUCCESS_STATUS) {
|
||||
return res.data;
|
||||
}
|
||||
|
||||
@@ -27,9 +27,7 @@ export const useCreateRecurringBooking = (
|
||||
RecurringBookingCreateBody[]
|
||||
>({
|
||||
mutationFn: (data) => {
|
||||
return http
|
||||
.post<ApiResponse<BookingResponse[]>>("/ee/bookings/recurring", data)
|
||||
.then((res) => res.data);
|
||||
return http.post<ApiResponse<BookingResponse[]>>("/bookings/recurring", data).then((res) => res.data);
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
if (data.status === SUCCESS_STATUS) {
|
||||
|
||||
@@ -16,7 +16,7 @@ export const useGcal = ({ isAuth, onCheckError }: useGcalProps) => {
|
||||
|
||||
const redirectToGcalOAuth = () => {
|
||||
http
|
||||
?.get("/ee/gcal/oauth/auth-url")
|
||||
?.get("/gcal/oauth/auth-url")
|
||||
.then(({ data: responseBody }) => {
|
||||
if (responseBody.data?.authUrl) {
|
||||
window.location.href = responseBody.data.authUrl;
|
||||
@@ -28,7 +28,7 @@ export const useGcal = ({ isAuth, onCheckError }: useGcalProps) => {
|
||||
useEffect(() => {
|
||||
if (isAuth) {
|
||||
http
|
||||
?.get("/ee/gcal/check")
|
||||
?.get("/gcal/check")
|
||||
.then(() => setAllowConnect(false))
|
||||
.catch((err) => {
|
||||
setAllowConnect(true);
|
||||
|
||||
@@ -6,6 +6,11 @@ import type { ApiResponse, UserResponse } from "@calcom/platform-types";
|
||||
import http from "../lib/http";
|
||||
|
||||
export const QUERY_KEY = "get-me";
|
||||
/**
|
||||
* Custom hook to fetch the current user's information.
|
||||
* Access Token must be provided to CalProvider in order to use this hook
|
||||
* @returns The result of the query containing the user's profile.
|
||||
*/
|
||||
export const useMe = () => {
|
||||
const pathname = `/${V2_ENDPOINTS.me}`;
|
||||
const me = useQuery({
|
||||
@@ -18,6 +23,7 @@ export const useMe = () => {
|
||||
throw new Error(res.data.error.message);
|
||||
});
|
||||
},
|
||||
enabled: Boolean(http.getAuthorizationHeader()),
|
||||
});
|
||||
|
||||
return me;
|
||||
|
||||
@@ -31,7 +31,7 @@ export const useOAuthClient = ({ clientId, apiUrl, refreshUrl, onError, onSucces
|
||||
if (clientId && http.getUrl() && prevClientId !== clientId) {
|
||||
try {
|
||||
http
|
||||
.get<ApiResponse>(`/ee/provider/${clientId}`)
|
||||
.get<ApiResponse>(`/provider/${clientId}`)
|
||||
.then(() => {
|
||||
onSuccess();
|
||||
http.setClientIdHeader(clientId);
|
||||
|
||||
@@ -58,7 +58,7 @@ export const useOAuthFlow = ({ accessToken, refreshUrl, clientId, onError, onSuc
|
||||
http.setAuthorizationHeader(accessToken);
|
||||
try {
|
||||
http
|
||||
.get<ApiResponse>(`/ee/provider/${clientId}/access-token`)
|
||||
.get<ApiResponse>(`/provider/${clientId}/access-token`)
|
||||
.catch(async (err: AxiosError) => {
|
||||
if ((err.response?.status === 498 || err.response?.status === 401) && refreshUrl) {
|
||||
setIsRefreshing(true);
|
||||
|
||||
@@ -6,7 +6,9 @@ export { useIsPlatform } from "./hooks/useIsPlatform";
|
||||
export { useAtomsContext } from "./hooks/useAtomsContext";
|
||||
export { useConnectedCalendars } from "./hooks/useConnectedCalendars";
|
||||
export { useEventTypesPublic } from "./hooks/event-types/useEventTypesPublic";
|
||||
export { usePublicEvent } from "./hooks/usePublicEvent";
|
||||
export { useGetEventTypeById } from "./hooks/event-types/useGetEventTypeById";
|
||||
export { useCancelBooking } from "./hooks/useCancelBooking";
|
||||
export { useGetBooking } from "./hooks/useGetBooking";
|
||||
export { useGetBookings } from "./hooks/useGetBookings";
|
||||
export { useMe } from "./hooks/useMe";
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"type": "module",
|
||||
"description": "Customizable UI components to integrate scheduling into your product.",
|
||||
"authors": "Cal.com, Inc.",
|
||||
"version": "1.0.18",
|
||||
"version": "1.0.23",
|
||||
"scripts": {
|
||||
"dev": "yarn vite build --watch & npx tailwindcss -i ./globals.css -o ./globals.min.css --postcss --minify --watch",
|
||||
"build": "yarn vite build && npx tailwindcss -i ./globals.css -o ./globals.min.css --postcss --minify",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
export const BASE_URL = "http://localhost:5555";
|
||||
export const API_VERSION = "v2";
|
||||
export const V2_ENDPOINTS = {
|
||||
me: "ee/me",
|
||||
me: "me",
|
||||
availability: "schedules",
|
||||
eventTypes: "event-types",
|
||||
bookings: "ee/bookings",
|
||||
bookings: "bookings",
|
||||
};
|
||||
|
||||
export const SUCCESS_STATUS = "success";
|
||||
|
||||
@@ -29,7 +29,7 @@ REDIS_URL="redis://localhost:6379"
|
||||
2. Open ".env" file and paste client id from step 8 in `NEXT_PUBLIC_X_CAL_ID` and client secret in `X_CAL_SECRET_KEY`. If in step 2 you used the same environment variables, then `NEXT_PUBLIC_CALCOM_API_URL` can stay as is. Otherwise adjust the port to point to the same `API_PORT` as you used in step 2.
|
||||
10. Navigate to example app and setup database by running `rm -f prisma/dev.db && yarn prisma db push`.
|
||||
11. Start the example app by running `yarn dev` and go to `http://localhost:4321`.
|
||||
12. In the Google Cloud Console "API & Services" "Credentials" `https://console.cloud.google.com/apis/credentials` open web project setup in step 1 and add `http://localhost:5555/api/v2/ee/gcal/oauth/save` to the authorized redirect URIs.
|
||||
12. In the Google Cloud Console "API & Services" "Credentials" `https://console.cloud.google.com/apis/credentials` open web project setup in step 1 and add `http://localhost:5555/v2/gcal/oauth/save` to the authorized redirect URIs.
|
||||
|
||||
<img width="1000" alt="Screenshot 2024-03-21 at 09 42 36" src="https://github.com/calcom/atoms-examples/assets/42170848/82ce4d7a-fc08-489a-ab06-a8eb41a68a2a">
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Navbar } from "@/components/Navbar";
|
||||
import { CheckCircle2Icon } from "lucide-react";
|
||||
import { X } from "lucide-react";
|
||||
import { Inter } from "next/font/google";
|
||||
// eslint-disable-next-line @calcom/eslint/deprecated-imports-next-router
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { useGetBooking, useCancelBooking } from "@calcom/atoms";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Navbar } from "@/components/Navbar";
|
||||
import { Inter } from "next/font/google";
|
||||
// eslint-disable-next-line @calcom/eslint/deprecated-imports-next-router
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Navbar } from "@/components/Navbar";
|
||||
import { Inter } from "next/font/google";
|
||||
// eslint-disable-next-line @calcom/eslint/deprecated-imports-next-router
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { useGetBookings } from "@calcom/atoms";
|
||||
|
||||
+29
-29
@@ -7,18 +7,18 @@ export interface paths {
|
||||
"/health": {
|
||||
get: operations["AppController_getHealth"];
|
||||
};
|
||||
"/api/v2/events/public": {
|
||||
"/v2/events/public": {
|
||||
get: operations["EventsController_getPublicEvent"];
|
||||
};
|
||||
"/api/v2/oauth-clients/{clientId}/users": {
|
||||
"/v2/oauth-clients/{clientId}/users": {
|
||||
post: operations["OAuthClientUsersController_createUser"];
|
||||
};
|
||||
"/api/v2/oauth-clients/{clientId}/users/{userId}": {
|
||||
"/v2/oauth-clients/{clientId}/users/{userId}": {
|
||||
get: operations["OAuthClientUsersController_getUserById"];
|
||||
delete: operations["OAuthClientUsersController_deleteUser"];
|
||||
patch: operations["OAuthClientUsersController_updateUser"];
|
||||
};
|
||||
"/api/v2/oauth-clients": {
|
||||
"/v2/oauth-clients": {
|
||||
/**
|
||||
* @description ⚠️ First, this endpoint requires `Cookie: next-auth.session-token=eyJhbGciOiJ` header. Log into Cal web app using owner of organization that was created after visiting `/settings/organizations/new`, refresh swagger docs, and the cookie will be added to requests automatically to pass the NextAuthGuard.
|
||||
* Second, make sure that the logged in user has organizationId set to pass the OrganizationRolesGuard guard.
|
||||
@@ -30,7 +30,7 @@ export interface paths {
|
||||
*/
|
||||
post: operations["OAuthClientsController_createOAuthClient"];
|
||||
};
|
||||
"/api/v2/oauth-clients/{clientId}": {
|
||||
"/v2/oauth-clients/{clientId}": {
|
||||
/**
|
||||
* @description ⚠️ First, this endpoint requires `Cookie: next-auth.session-token=eyJhbGciOiJ` header. Log into Cal web app using owner of organization that was created after visiting `/settings/organizations/new`, refresh swagger docs, and the cookie will be added to requests automatically to pass the NextAuthGuard.
|
||||
* Second, make sure that the logged in user has organizationId set to pass the OrganizationRolesGuard guard.
|
||||
@@ -47,89 +47,89 @@ export interface paths {
|
||||
*/
|
||||
patch: operations["OAuthClientsController_updateOAuthClient"];
|
||||
};
|
||||
"/api/v2/oauth/{clientId}/authorize": {
|
||||
"/v2/oauth/{clientId}/authorize": {
|
||||
/**
|
||||
* Authorize an OAuth client
|
||||
* @description Redirects the user to the specified 'redirect_uri' with an authorization code in query parameter if the client is authorized successfully. The code is then exchanged for access and refresh tokens via the `/exchange` endpoint.
|
||||
*/
|
||||
post: operations["OAuthFlowController_authorize"];
|
||||
};
|
||||
"/api/v2/oauth/{clientId}/exchange": {
|
||||
"/v2/oauth/{clientId}/exchange": {
|
||||
/**
|
||||
* Exchange authorization code for access tokens
|
||||
* @description Exchanges the authorization code received from the `/authorize` endpoint for access and refresh tokens. The authorization code should be provided in the 'Authorization' header prefixed with 'Bearer '.
|
||||
*/
|
||||
post: operations["OAuthFlowController_exchange"];
|
||||
};
|
||||
"/api/v2/oauth/{clientId}/refresh": {
|
||||
"/v2/oauth/{clientId}/refresh": {
|
||||
post: operations["OAuthFlowController_refreshAccessToken"];
|
||||
};
|
||||
"/api/v2/event-types": {
|
||||
"/v2/event-types": {
|
||||
get: operations["EventTypesController_getEventTypes"];
|
||||
post: operations["EventTypesController_createEventType"];
|
||||
};
|
||||
"/api/v2/event-types/{eventTypeId}": {
|
||||
"/v2/event-types/{eventTypeId}": {
|
||||
get: operations["EventTypesController_getEventType"];
|
||||
};
|
||||
"/api/v2/event-types/{username}/public": {
|
||||
"/v2/event-types/{username}/public": {
|
||||
get: operations["EventTypesController_getPublicEventTypes"];
|
||||
};
|
||||
"/api/v2/platform/gcal/oauth/auth-url": {
|
||||
"/v2/gcal/oauth/auth-url": {
|
||||
get: operations["GcalController_redirect"];
|
||||
};
|
||||
"/api/v2/platform/gcal/oauth/save": {
|
||||
"/v2/gcal/oauth/save": {
|
||||
get: operations["GcalController_save"];
|
||||
};
|
||||
"/api/v2/platform/gcal/check": {
|
||||
"/v2/gcal/check": {
|
||||
get: operations["GcalController_check"];
|
||||
};
|
||||
"/api/v2/platform/provider/{clientId}": {
|
||||
"/v2/provider/{clientId}": {
|
||||
get: operations["CalProviderController_verifyClientId"];
|
||||
};
|
||||
"/api/v2/platform/provider/{clientId}/access-token": {
|
||||
"/v2/provider/{clientId}/access-token": {
|
||||
get: operations["CalProviderController_verifyAccessToken"];
|
||||
};
|
||||
"/api/v2/schedules": {
|
||||
"/v2/schedules": {
|
||||
get: operations["SchedulesController_getSchedules"];
|
||||
post: operations["SchedulesController_createSchedule"];
|
||||
};
|
||||
"/api/v2/schedules/default": {
|
||||
"/v2/schedules/default": {
|
||||
get: operations["SchedulesController_getDefaultSchedule"];
|
||||
};
|
||||
"/api/v2/schedules/time-zones": {
|
||||
"/v2/schedules/time-zones": {
|
||||
get: operations["SchedulesController_getTimeZones"];
|
||||
};
|
||||
"/api/v2/schedules/{scheduleId}": {
|
||||
"/v2/schedules/{scheduleId}": {
|
||||
get: operations["SchedulesController_getSchedule"];
|
||||
delete: operations["SchedulesController_deleteSchedule"];
|
||||
patch: operations["SchedulesController_updateSchedule"];
|
||||
};
|
||||
"/api/v2/me": {
|
||||
"/v2/me": {
|
||||
get: operations["MeController_getMe"];
|
||||
patch: operations["MeController_updateMe"];
|
||||
};
|
||||
"/api/v2/ee/calendars/busy-times": {
|
||||
"/v2/calendars/busy-times": {
|
||||
get: operations["CalendarsController_getBusyTimes"];
|
||||
};
|
||||
"/api/v2/ee/calendars": {
|
||||
"/v2/calendars": {
|
||||
get: operations["CalendarsController_getCalendars"];
|
||||
};
|
||||
"/api/v2/ee/bookings": {
|
||||
"/v2/bookings": {
|
||||
post: operations["BookingsController_createBooking"];
|
||||
};
|
||||
"/api/v2/ee/bookings/reccuring": {
|
||||
"/v2/bookings/reccuring": {
|
||||
post: operations["BookingsController_createReccuringBooking"];
|
||||
};
|
||||
"/api/v2/ee/bookings/instant": {
|
||||
"/v2/bookings/instant": {
|
||||
post: operations["BookingsController_createInstantBooking"];
|
||||
};
|
||||
"/api/v2/slots/reserve": {
|
||||
"/v2/slots/reserve": {
|
||||
post: operations["SlotsController_reserveSlot"];
|
||||
};
|
||||
"/api/v2/slots/selected-slot": {
|
||||
"/v2/slots/selected-slot": {
|
||||
delete: operations["SlotsController_deleteSelectedSlot"];
|
||||
};
|
||||
"/api/v2/slots/available": {
|
||||
"/v2/slots/available": {
|
||||
get: operations["SlotsController_getAvailableSlots"];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,6 +6,10 @@ import {
|
||||
PERMISSIONS,
|
||||
SCHEDULE_READ,
|
||||
SCHEDULE_WRITE,
|
||||
APPS_READ,
|
||||
APPS_WRITE,
|
||||
PROFILE_READ,
|
||||
PROFILE_WRITE,
|
||||
} from "@calcom/platform-constants";
|
||||
import type { PLATFORM_PERMISSION } from "@calcom/platform-types";
|
||||
|
||||
@@ -43,6 +47,22 @@ export const hasScheduleWritePermission = (userPermissions: number): boolean =>
|
||||
return hasPermission(userPermissions, SCHEDULE_WRITE);
|
||||
};
|
||||
|
||||
export const hasAppsReadPermission = (userPermissions: number): boolean => {
|
||||
return hasPermission(userPermissions, APPS_READ);
|
||||
};
|
||||
|
||||
export const hasAppsWritePermission = (userPermissions: number): boolean => {
|
||||
return hasPermission(userPermissions, APPS_WRITE);
|
||||
};
|
||||
|
||||
export const hasProfileReadPermission = (userPermissions: number): boolean => {
|
||||
return hasPermission(userPermissions, PROFILE_READ);
|
||||
};
|
||||
|
||||
export const hasProfileWritePermission = (userPermissions: number): boolean => {
|
||||
return hasPermission(userPermissions, PROFILE_WRITE);
|
||||
};
|
||||
|
||||
export const listPermissions = (userPermissions: number): PLATFORM_PERMISSION[] => {
|
||||
return PERMISSIONS.reduce((acc, permission) => {
|
||||
if (hasPermission(userPermissions, permission)) {
|
||||
|
||||
@@ -152,7 +152,7 @@ export const deleteCredentialHandler = async ({ ctx, input }: DeleteCredentialOp
|
||||
...metadata,
|
||||
apps: {
|
||||
...metadata?.apps,
|
||||
[appSlug]: null,
|
||||
[appSlug]: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -353,7 +353,7 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions)
|
||||
});
|
||||
}
|
||||
|
||||
if (updatedUser) {
|
||||
if (updatedUser && hasEmailBeenChanged) {
|
||||
// Skip sending verification email when user tries to change his primary email to a verified secondary email
|
||||
if (secondaryEmail?.emailVerified) {
|
||||
secondaryEmails.push({
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
|
||||
import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains";
|
||||
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
||||
import { isTeamAdmin } from "@calcom/lib/server/queries/teams";
|
||||
import { uploadLogo } from "@calcom/lib/server/uploadLogo";
|
||||
import { closeComUpdateTeam } from "@calcom/lib/sync/SyncServiceManager";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { RedirectType } from "@calcom/prisma/enums";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
@@ -91,6 +93,40 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||
data,
|
||||
});
|
||||
|
||||
if (updatedTeam.parentId && prevTeam.slug) {
|
||||
// No changes made lets skip this logic
|
||||
if (updatedTeam.slug === prevTeam.slug) return;
|
||||
|
||||
// Fetch parent team slug to construct toUrl
|
||||
const parentTeam = await prisma.team.findUnique({
|
||||
where: {
|
||||
id: updatedTeam.parentId,
|
||||
},
|
||||
select: {
|
||||
slug: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!parentTeam?.slug) {
|
||||
throw new Error(`Parent team wth slug: ${parentTeam?.slug} not found`);
|
||||
}
|
||||
|
||||
const orgUrlPrefix = getOrgFullOrigin(parentTeam.slug);
|
||||
|
||||
const toUrlOld = `${orgUrlPrefix}/${prevTeam.slug}`;
|
||||
const toUrlNew = `${orgUrlPrefix}/${updatedTeam.slug}`;
|
||||
|
||||
await prisma.tempOrgRedirect.updateMany({
|
||||
where: {
|
||||
type: RedirectType.Team,
|
||||
toUrl: toUrlOld,
|
||||
},
|
||||
data: {
|
||||
toUrl: toUrlNew,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Sync Services: Close.com
|
||||
if (prevTeam) closeComUpdateTeam(prevTeam, updatedTeam);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user