Compare commits

...
32 Commits
Author SHA1 Message Date
Keith Williams 9dd2dbff19 v4.0.1 (#14618)
release.yml / release (push) Canceled after 0s
2024-04-16 21:05:18 +00:00
Joe Au-Yeung cebbffe230 fix: Minimum fix for N Calendar Invites (#14617)
* Remove pushing team member destination calendars

* Only create the event on the first destination calendar

* Only create event on the first destination calendar
2024-04-16 20:05:40 +00:00
Morgan e3e464abfa fix: booker atoms using next/router (#14612)
* fix: booker OOO using next/router

* fixup! fix: booker OOO using next/router
2024-04-16 18:37:24 +00:00
268438b0ff fix: rescheduling daily (#13736)
* fix: rescheduling daily

* fix: e2e test

* chore: test

* add location to default event type

* revert fix

* chore: add env variable

* fix: sort alphabetically

* test: checking

* chore: use another user

* chore: try adding location

* chore: undo

* chore: add variable

* chore: commented

* F

* chore: comment

* test: add unit test

* fix: type err

---------

Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2024-04-16 12:57:34 -04:00
Hariom Balhara 355c8de5e7 Set exclusive tax behaviour (#14574) 2024-04-16 13:18:08 +01:00
e6056bc4a4 fix: change max width to avoid overflow in dialog when creating new event type (#14198)
* fix: change max width to avoid overflow when creating new event dialog

* style: make max with responsive at md breakpoint

* chore: add tooltip

---------

Co-authored-by: xylonx <x956200361@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
2024-04-16 10:48:10 +00:00
Omar López f2eebe95bd chore: Adds deprecated-imports-next-router (#14486) 2024-04-16 11:46:45 +01:00
sean-brydonandJoe Au-Yeung 3084013269 fix: always load via SSR (#14476)
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-04-16 11:37:03 +01:00
68568e5a25 feat: round robin: show avatar and name of person on success page (#14245)
* feat: round robin: show avatar and name of person on success page

* fix: add background border

* fix: e2e tests

* fix: remove avatar on select, use proper border variables

* fix: bookings-list e2e round robin title

* chore: change name

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
2024-04-16 11:36:11 +01:00
Lauris Skraucis 0eae0bb8fe refactor: remove /ee/ url segment from atoms hooks (#14602) 2024-04-16 07:29:12 -03:00
Somay Chauhan b61820d354 fix: custom attributes not being synced in intercom (#14600)
* fix: custom attributes not being synced in intercom

* fix: ssrInit
2024-04-16 11:03:45 +01:00
MorganandBailey Pumfleet 1606272850 chore: bump atoms (#14585)
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2024-04-16 00:17:04 -03:00
Kaleem ea04fe1c9b fix: remove @ts-ignore (#14582) 2024-04-16 07:44:00 +05:30
Peer RichelsenandAlex van Andel 4c50b6a8af fix: hide "upgrade org banner" for isPlatform (#14522)
* not great but does the job

* comments

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-04-16 02:41:07 +01:00
sean-brydon 39aa848e9e fix: update sub-team slug - update tempOrgRedirect (#14523) 2024-04-16 01:14:41 +00:00
Peer Richelsen 64c56c1b17 Delete apps/platform directory (#14520) 2024-04-16 01:12:54 +00:00
MorganandRyukemeister 8e883a6b10 feat: update platform oauth client form (#14542)
* feat: update platform oauth client form

* fixup! feat: update platform oauth client form

* add tooltip to mention permissions cannot be modified

---------

Co-authored-by: Ryukemeister <sahalrajiv-extc@atharvacoe.ac.in>
2024-04-16 01:55:00 +01:00
Peer Richelsen 9684aa3ad5 for admins min 1 org user. for self-serve 30 (#14567) 2024-04-16 01:53:22 +01:00
sean-brydon 5fede84bf6 feat: use experimental optimizePackageImports (#14587) 2024-04-16 01:52:08 +01:00
Alex van Andel a788e07be1 fix: Re-instate hasEmailBeenChanged before sending email (#14596) 2024-04-15 22:09:06 +00:00
Somay Chauhan 68ffd2248b fix: intercom visible on mobile (#14563) 2024-04-15 21:18:42 +00:00
Carina WollendorferandCarinaWolli 4b12d021e4 remove : from email from name (#14594)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-04-15 20:27:10 +01:00
Alex van Andel 4daf9db9db fix: acme.cal.local crashes due to missing requestedSlug in seed (#14592) 2024-04-15 18:53:41 +01:00
sean-brydon a660b1eb05 fix: conditionally remove fixed prop if we're in embed (#14586)
* fix: conditionally remove fixed prop if we're in embed

* fix: conditionally remove fixed prop if we're in embed
2024-04-15 14:19:25 +01:00
Somay Chauhan 6f2f098e12 fix: removing stripe results in the event-types page loading indefintely (#14583)
* fix: removing stripe results in the event-types page loading indefinitely

* undo yarn.lock
2024-04-15 10:44:47 +00:00
Keith Williams 6a75ef86b0 v4.0.0
release.yml / release (push) Canceled after 0s
2024-04-14 20:02:28 -03:00
Alex van Andel 3d2397879c fix: Remove PageWrapper (#14579)
* fix: Remove PageWrapper

* Remove useLocale
2024-04-14 20:46:08 +00:00
Alex van Andel de474089a7 Revert "fix: Production deploys (#14578)"
This reverts commit 8b31b9aa05.
2024-04-14 16:41:25 +01:00
Keith Williams 8b31b9aa05 fix: Production deploys (#14578) 2024-04-14 15:04:30 +00:00
Crowdin Bot ad41672958 New Crowdin translations by Github Action 2024-04-14 12:51:43 +00:00
Morgan 86f2984b30 fix: only call useMe if access token is set (#14576)
* fix: only call useMe if access token is set

* chore: export use me hook
2024-04-14 12:48:30 +00:00
Morgan 38e62786e4 chore: remove /ee from apiv2 urls (#14568)
* chore: remove /ee from apiv2 urls

* fixup! chore: remove /ee from apiv2 urls

* chore: remove /ee from apiv2 urls

* fixup! chore: remove /ee from apiv2 urls

* fixup! fixup! chore: remove /ee from apiv2 urls

* fixup! fixup! fixup! chore: remove /ee from apiv2 urls
2024-04-13 22:16:37 +00:00
96 changed files with 807 additions and 274 deletions
+1
View File
@@ -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 }}
+1
View File
@@ -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 }}
+1
View File
@@ -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 }}
+1
View File
@@ -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 }}
+1
View File
@@ -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);
+2 -2
View File
@@ -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 () => {
+1 -1
View File
@@ -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")
+71 -36
View File
@@ -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
View File
@@ -1 +0,0 @@
Hello World
-1
View File
@@ -1 +0,0 @@
## Cal.com Platform Example (https://platform.cal.com)
@@ -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}
@@ -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>
+1
View File
@@ -72,6 +72,7 @@ export const getEventTypesFromDB = async (id: number) => {
seatsPerTimeSlot: true,
seatsShowAttendees: true,
seatsShowAvailabilityCount: true,
schedulingType: true,
periodStartDate: true,
periodEndDate: true,
},
@@ -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,
};
};
@@ -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">
+1
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "3.9.9",
"version": "4.0.1",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
+5 -5
View File
@@ -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");
+19 -3
View File
@@ -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>
</>
);
}
+5 -13
View File
@@ -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}
+1 -7
View File
@@ -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);
}
+7 -2
View File
@@ -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}}",
+1
View File
@@ -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;
+6
View File
@@ -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 };
};
+2
View File
@@ -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)",
+4
View File
@@ -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;
}
}
}
+2 -2
View File
@@ -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"];
+1 -1
View File
@@ -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;
+16 -12
View File
@@ -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"
@@ -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";
+6 -5
View File
@@ -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 -8
View File
@@ -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) {
+2 -2
View File
@@ -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
View File
@@ -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);
+2
View File
@@ -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";
+1 -1
View File
@@ -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",
+2 -2
View File
@@ -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";
+1 -1
View File
@@ -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
View File
@@ -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"];
};
}
+20
View File
@@ -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);