Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60ae817f30 | ||
|
|
74166fc2cb | ||
|
|
fb997045f2 | ||
|
|
73a79cf29b | ||
|
|
54a1663a8b | ||
|
|
883d745784 | ||
|
|
571e93a19a | ||
|
|
d16428da6c | ||
|
|
8c5438663c | ||
|
|
ed76d163cf | ||
|
|
9215725dad | ||
|
|
00131668aa | ||
|
|
0306ad737f | ||
|
|
20dfc34e70 | ||
|
|
b7e8b252d2 | ||
|
|
74f09c332b | ||
|
|
3ce6c23603 | ||
|
|
ad3ee14501 | ||
|
|
1931758fc7 | ||
|
|
fe551af145 | ||
|
|
9fdaaed100 | ||
|
|
c0f8a355e3 | ||
|
|
df95ec4ce8 | ||
|
|
a15cad4d03 | ||
|
|
8904f20eb8 | ||
|
|
0ee81d0739 | ||
|
|
c0bc6f8ccb | ||
|
|
1f1cd1e236 | ||
|
|
dbbd78fb26 | ||
|
|
f6e45842fd | ||
|
|
d25c043046 | ||
|
|
5695ba7c13 | ||
|
|
eeb0322ce1 | ||
|
|
480977c2b5 | ||
|
|
8b2e46a6fa | ||
|
|
842be660b0 | ||
|
|
4f69c0e502 | ||
|
|
be128181e2 | ||
|
|
0aa1628925 | ||
|
|
fd58f669d0 | ||
|
|
51927f82c0 | ||
|
|
0f8a0eaf4a | ||
|
|
b9ac22d4ed | ||
|
|
ebca5c6409 | ||
|
|
6ef35e4dba | ||
|
|
8bb5200dd8 | ||
|
|
4aeb6b7c9b | ||
|
|
ae64d7ba82 | ||
|
|
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 |
@@ -38,6 +38,7 @@ BASECAMP3_USER_AGENT=
|
||||
DAILY_API_KEY=
|
||||
DAILY_SCALE_PLAN=''
|
||||
DAILY_WEBHOOK_SECRET=''
|
||||
DAILY_MEETING_ENDED_WEBHOOK_SECRET=''
|
||||
|
||||
# - GOOGLE CALENDAR/MEET/LOGIN
|
||||
# Needed to enable Google Calendar integration and Login with Google
|
||||
|
||||
+5
-1
@@ -240,7 +240,7 @@ CSP_POLICY=
|
||||
# Vercel Edge Config
|
||||
EDGE_CONFIG=
|
||||
|
||||
NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes
|
||||
NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes
|
||||
NEXT_PUBLIC_BOOKER_NUMBER_OF_DAYS_TO_LOAD=0 # Override the booker to only load X number of days worth of data
|
||||
|
||||
# Control time intervals on a user's Schedule availability
|
||||
@@ -347,6 +347,10 @@ SENTRY_DISABLE_SERVER_WEBPACK_PLUGIN=1
|
||||
# api v2
|
||||
NEXT_PUBLIC_API_V2_URL="http://localhost:5555/api/v2"
|
||||
|
||||
# Tasker features
|
||||
TASKER_ENABLE_WEBHOOKS=0
|
||||
TASKER_ENABLE_EMAILS=0
|
||||
|
||||
# Ratelimiting via unkey
|
||||
UNKEY_ROOT_KEY=
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ env:
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
NEXT_PUBLIC_API_V2_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_URL }}
|
||||
NEXT_PUBLIC_API_V2_ROOT_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_ROOT_URL }}
|
||||
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
@@ -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 }}
|
||||
@@ -91,6 +92,7 @@ jobs:
|
||||
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
||||
- name: Upload Test Results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -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 }}
|
||||
@@ -73,6 +74,7 @@ jobs:
|
||||
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
||||
- name: Upload Test Results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -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 }}
|
||||
@@ -80,6 +81,7 @@ jobs:
|
||||
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
||||
- name: Upload Test Results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -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 }}
|
||||
@@ -82,6 +83,7 @@ jobs:
|
||||
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
||||
- name: Upload Test Results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -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 }}
|
||||
@@ -31,6 +32,7 @@ env:
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
NEXT_PUBLIC_API_V2_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_URL }}
|
||||
NEXT_PUBLIC_API_V2_ROOT_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_ROOT_URL }}
|
||||
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -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 }}
|
||||
@@ -35,6 +36,7 @@ env:
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
NEXT_PUBLIC_API_V2_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_URL }}
|
||||
NEXT_PUBLIC_API_V2_ROOT_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_ROOT_URL }}
|
||||
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -82,7 +82,6 @@ export const schemaUserBaseBodyParams = User.pick({
|
||||
brandColor: true,
|
||||
darkBrandColor: true,
|
||||
allowDynamicBooking: true,
|
||||
away: true,
|
||||
role: true,
|
||||
// @note: disallowing avatar changes via API for now. We can add it later if needed. User should upload image via UI.
|
||||
// avatar: true,
|
||||
@@ -171,7 +170,6 @@ export const schemaUserReadPublic = User.pick({
|
||||
brandColor: true,
|
||||
darkBrandColor: true,
|
||||
allowDynamicBooking: true,
|
||||
away: true,
|
||||
createdDate: true,
|
||||
verified: true,
|
||||
invitedTo: true,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { createMocks } from "node-mocks-http";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
|
||||
import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload";
|
||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
import { buildBooking, buildEventType, buildWebhook } from "@calcom/lib/test/builder";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
@@ -7,6 +7,12 @@ 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");
|
||||
}
|
||||
if (req.url.includes("reccuring")) {
|
||||
req.url = req.url.replace("reccuring", "recurring");
|
||||
}
|
||||
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/recurring")
|
||||
// .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) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input";
|
||||
import { CreateReccuringBookingInput } from "@/ee/bookings/inputs/create-reccuring-booking.input";
|
||||
import { CreateRecurringBookingInput } from "@/ee/bookings/inputs/create-recurring-booking.input";
|
||||
import { GetBookingOutput } from "@/ee/bookings/outputs/get-booking.output";
|
||||
import { GetBookingsOutput } from "@/ee/bookings/outputs/get-bookings.output";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
@@ -57,6 +57,8 @@ type OAuthRequestParams = {
|
||||
platformRescheduleUrl: string;
|
||||
platformCancelUrl: string;
|
||||
platformBookingUrl: string;
|
||||
platformBookingLocation?: string;
|
||||
arePlatformEmailsEnabled: boolean;
|
||||
};
|
||||
|
||||
const DEFAULT_PLATFORM_PARAMS = {
|
||||
@@ -64,11 +66,12 @@ const DEFAULT_PLATFORM_PARAMS = {
|
||||
platformCancelUrl: "",
|
||||
platformRescheduleUrl: "",
|
||||
platformBookingUrl: "",
|
||||
areEmailsEnabled: true,
|
||||
arePlatformEmailsEnabled: false,
|
||||
platformBookingLocation: undefined,
|
||||
};
|
||||
|
||||
@Controller({
|
||||
path: "ee/bookings",
|
||||
path: "/bookings",
|
||||
version: "2",
|
||||
})
|
||||
@UseGuards(PermissionsGuard)
|
||||
@@ -141,12 +144,15 @@ export class BookingsController {
|
||||
@Post("/")
|
||||
async createBooking(
|
||||
@Req() req: BookingRequest,
|
||||
@Body() _: CreateBookingInput,
|
||||
@Body() body: CreateBookingInput,
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string
|
||||
): Promise<ApiResponse<unknown>> {
|
||||
const oAuthClientId = clientId?.toString();
|
||||
const locationUrl = body.locationUrl;
|
||||
try {
|
||||
const booking = await handleNewBooking(await this.createNextApiBookingRequest(req, oAuthClientId));
|
||||
const booking = await handleNewBooking(
|
||||
await this.createNextApiBookingRequest(req, oAuthClientId, locationUrl)
|
||||
);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: booking,
|
||||
@@ -167,6 +173,7 @@ export class BookingsController {
|
||||
const oAuthClientId = clientId?.toString();
|
||||
if (bookingId) {
|
||||
try {
|
||||
req.body.id = parseInt(bookingId);
|
||||
await handleCancelBooking(await this.createNextApiBookingRequest(req, oAuthClientId));
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
@@ -180,10 +187,10 @@ export class BookingsController {
|
||||
throw new InternalServerErrorException("Could not cancel booking.");
|
||||
}
|
||||
|
||||
@Post("/reccuring")
|
||||
async createReccuringBooking(
|
||||
@Post("/recurring")
|
||||
async createRecurringBooking(
|
||||
@Req() req: BookingRequest,
|
||||
@Body() _: CreateReccuringBookingInput[],
|
||||
@Body() _: CreateRecurringBookingInput[],
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string
|
||||
): Promise<ApiResponse<BookingResponse[]>> {
|
||||
const oAuthClientId = clientId?.toString();
|
||||
@@ -234,10 +241,7 @@ export class BookingsController {
|
||||
}
|
||||
}
|
||||
|
||||
async getOAuthClientsParams(
|
||||
req: BookingRequest,
|
||||
clientId: string
|
||||
): Promise<OAuthRequestParams & { areEmailsEnabled: boolean }> {
|
||||
async getOAuthClientsParams(req: BookingRequest, clientId: string): Promise<OAuthRequestParams> {
|
||||
const res = DEFAULT_PLATFORM_PARAMS;
|
||||
try {
|
||||
const client = await this.oAuthClientRepository.getOAuthClient(clientId);
|
||||
@@ -247,7 +251,7 @@ export class BookingsController {
|
||||
res.platformCancelUrl = client.bookingCancelRedirectUri ?? "";
|
||||
res.platformRescheduleUrl = client.bookingRescheduleRedirectUri ?? "";
|
||||
res.platformBookingUrl = client.bookingRedirectUri ?? "";
|
||||
res.areEmailsEnabled = client.areEmailsEnabled;
|
||||
res.arePlatformEmailsEnabled = client.areEmailsEnabled ?? false;
|
||||
}
|
||||
return res;
|
||||
} catch (err) {
|
||||
@@ -258,14 +262,15 @@ export class BookingsController {
|
||||
|
||||
async createNextApiBookingRequest(
|
||||
req: BookingRequest,
|
||||
oAuthClientId?: string
|
||||
oAuthClientId?: string,
|
||||
platformBookingLocation?: string
|
||||
): Promise<NextApiRequest & { userId?: number } & OAuthRequestParams> {
|
||||
const userId = (await this.getOwnerId(req)) ?? -1;
|
||||
const oAuthParams = oAuthClientId
|
||||
? await this.getOAuthClientsParams(req, oAuthClientId)
|
||||
: DEFAULT_PLATFORM_PARAMS;
|
||||
Object.assign(req, { userId, ...oAuthParams });
|
||||
req.body = { ...req.body, areEmailsEnabled: oAuthParams.areEmailsEnabled };
|
||||
Object.assign(req, { userId, ...oAuthParams, platformBookingLocation });
|
||||
req.body = { ...req.body, noEmail: !oAuthParams.arePlatformEmailsEnabled };
|
||||
return req as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,4 +98,8 @@ export class CreateBookingInput {
|
||||
|
||||
@Type(() => Response)
|
||||
responses!: Response;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
locationUrl?: string;
|
||||
}
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { IsBoolean, IsNumber, IsOptional } from "class-validator";
|
||||
|
||||
import type { AppsStatus } from "@calcom/platform-libraries";
|
||||
|
||||
export class CreateReccuringBookingInput extends CreateBookingInput {
|
||||
export class CreateRecurringBookingInput extends CreateBookingInput {
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
noEmail?: boolean;
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
||||
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
@@ -18,6 +19,20 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
|
||||
|
||||
const CLIENT_REDIRECT_URI = "http://localhost:5555";
|
||||
|
||||
class CalendarsServiceMock {
|
||||
async getCalendars() {
|
||||
return {
|
||||
connectedCalendars: [
|
||||
{
|
||||
integration: {
|
||||
type: "google_calendar",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
describe("Platform Gcal Endpoints", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
@@ -42,6 +57,8 @@ describe("Platform Gcal Endpoints", () => {
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.overrideProvider(CalendarsService)
|
||||
.useClass(CalendarsServiceMock)
|
||||
.compile();
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
@@ -85,14 +102,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 +120,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 +128,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 +136,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 +168,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")
|
||||
|
||||
+3
-3
@@ -199,7 +199,7 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
it(`/GET/:id`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/oauth-clients/${oAuthClient.id}/users/${postResponseData.user.id}`)
|
||||
.set("Authorization", `Bearer ${postResponseData.accessToken}`)
|
||||
.set("x-cal-secret-key", oAuthClient.secret)
|
||||
.set("Origin", `${CLIENT_REDIRECT_URI}`)
|
||||
.expect(200);
|
||||
|
||||
@@ -216,7 +216,7 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.patch(`/api/v2/oauth-clients/${oAuthClient.id}/users/${postResponseData.user.id}`)
|
||||
.set("Authorization", `Bearer ${postResponseData.accessToken}`)
|
||||
.set("x-cal-secret-key", oAuthClient.secret)
|
||||
.set("Origin", `${CLIENT_REDIRECT_URI}`)
|
||||
.send(body)
|
||||
.expect(200);
|
||||
@@ -231,7 +231,7 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
it(`/DELETE/:id`, () => {
|
||||
return request(app.getHttpServer())
|
||||
.delete(`/api/v2/oauth-clients/${oAuthClient.id}/users/${postResponseData.user.id}`)
|
||||
.set("Authorization", `Bearer ${postResponseData.accessToken}`)
|
||||
.set("x-cal-secret-key", oAuthClient.secret)
|
||||
.set("Origin", `${CLIENT_REDIRECT_URI}`)
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
-5
@@ -76,11 +76,6 @@ export class OAuthClientUsersController {
|
||||
this.logger.log(
|
||||
`Creating user with data: ${JSON.stringify(body, null, 2)} for OAuth Client with ID ${oAuthClientId}`
|
||||
);
|
||||
const existingUser = await this.userRepository.findByEmail(body.email);
|
||||
|
||||
if (existingUser) {
|
||||
throw new BadRequestException("A user with the provided email already exists.");
|
||||
}
|
||||
const client = await this.oauthRepository.getOAuthClient(oAuthClientId);
|
||||
|
||||
const isPlatformManaged = true;
|
||||
|
||||
@@ -23,7 +23,7 @@ export class OAuthClientUsersService {
|
||||
isPlatformManaged: boolean,
|
||||
organizationId?: number
|
||||
) {
|
||||
const existsWithEmail = await this.userExistsWithEmail(oAuthClientId, body.email);
|
||||
const existsWithEmail = await this.managedUserExistsWithEmail(oAuthClientId, body.email);
|
||||
if (existsWithEmail) {
|
||||
throw new BadRequestException("User with the provided e-mail already exists.");
|
||||
}
|
||||
@@ -73,7 +73,7 @@ export class OAuthClientUsersService {
|
||||
};
|
||||
}
|
||||
|
||||
async userExistsWithEmail(oAuthClientId: string, email: string) {
|
||||
async managedUserExistsWithEmail(oAuthClientId: string, email: string) {
|
||||
const oAuthEmail = this.getOAuthUserEmail(oAuthClientId, email);
|
||||
const user = await this.userRepository.findByEmail(oAuthEmail);
|
||||
return !!user;
|
||||
|
||||
@@ -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/recurring": {
|
||||
"post": {
|
||||
"operationId": "BookingsController_createReccuringBooking",
|
||||
"parameters": [],
|
||||
"operationId": "BookingsController_createRecurringBooking",
|
||||
"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": [],
|
||||
@@ -4190,6 +4225,9 @@
|
||||
},
|
||||
"responses": {
|
||||
"$ref": "#/components/schemas/Response"
|
||||
},
|
||||
"locationUrl": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Hello World
|
||||
@@ -1 +0,0 @@
|
||||
## Cal.com Platform Example (https://platform.cal.com)
|
||||
@@ -0,0 +1 @@
|
||||
export { GET } from "@calcom/features/tasker/api/cleanup";
|
||||
@@ -0,0 +1 @@
|
||||
export { GET } from "@calcom/features/tasker/api/cron";
|
||||
@@ -139,7 +139,7 @@ export const AppList = ({ data, handleDisconnect, variant, listClassName }: AppL
|
||||
...app,
|
||||
credentialOwner: {
|
||||
name: team.name,
|
||||
avatar: team.logo,
|
||||
avatar: team.logoUrl,
|
||||
teamId: team.teamId,
|
||||
credentialId: team.credentialId,
|
||||
readOnly: !team.isAdmin,
|
||||
|
||||
@@ -2,7 +2,7 @@ import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
||||
import { doesAppSupportTeamInstall } from "@calcom/app-store/utils";
|
||||
import { Spinner } from "@calcom/features/calendars/weeklyview/components/spinner/Spinner";
|
||||
import type { UserAdminTeams } from "@calcom/features/ee/teams/lib/getUserAdminTeams";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import type { AppFrontendPayload } from "@calcom/types/App";
|
||||
@@ -124,8 +124,8 @@ export const InstallAppButtonChild = ({
|
||||
disabled={isInstalled}
|
||||
CustomStartIcon={
|
||||
<Avatar
|
||||
alt={team.logo || ""}
|
||||
imageSrc={team.logo || `${WEBAPP_URL}/${team.logo}/avatar.png`} // if no image, use default avatar
|
||||
alt={team.logoUrl || ""}
|
||||
imageSrc={getPlaceholderAvatar(team.logoUrl, team.name)} // if no image, use default avatar
|
||||
size="sm"
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||
// credentialIds: team?.credentialId ? [team.credentialId] : [],
|
||||
credentialOwner: {
|
||||
name: team.name,
|
||||
avatar: team.logo,
|
||||
avatar: team.logoUrl,
|
||||
teamId: team.teamId,
|
||||
credentialId: team.credentialId,
|
||||
},
|
||||
|
||||
@@ -480,6 +480,7 @@ export const EventSetupTab = (
|
||||
label={t("title")}
|
||||
{...(isManagedEventType || isChildrenManagedEventType ? titleLockedProps : {})}
|
||||
defaultValue={eventType.title}
|
||||
data-testid="event-title"
|
||||
{...formMethods.register("title")}
|
||||
/>
|
||||
<div>
|
||||
@@ -494,6 +495,7 @@ export const EventSetupTab = (
|
||||
label={t("URL")}
|
||||
{...(isManagedEventType || isChildrenManagedEventType ? urlLockedProps : {})}
|
||||
defaultValue={eventType.slug}
|
||||
data-testid="event-slug"
|
||||
addOnLeading={
|
||||
<>
|
||||
{urlPrefix}/
|
||||
|
||||
@@ -481,7 +481,6 @@ function EventTypeSingleLayout({
|
||||
sticky
|
||||
linkShallow
|
||||
itemClassname="items-start"
|
||||
iconClassName="md:mt-px"
|
||||
/>
|
||||
</div>
|
||||
<div className="p-2 md:mx-0 md:p-0 xl:hidden">
|
||||
|
||||
@@ -33,7 +33,7 @@ const UserProfile = () => {
|
||||
|
||||
const mutation = trpc.viewer.updateProfile.useMutation({
|
||||
onSuccess: async (_data, context) => {
|
||||
if (context.avatar) {
|
||||
if (context.avatarUrl) {
|
||||
showToast(t("your_user_profile_updated_successfully"), "success");
|
||||
await utils.viewer.me.refetch();
|
||||
} else
|
||||
@@ -74,7 +74,7 @@ const UserProfile = () => {
|
||||
event.preventDefault();
|
||||
const enteredAvatar = avatarRef.current?.value;
|
||||
mutation.mutate({
|
||||
avatar: enteredAvatar,
|
||||
avatarUrl: enteredAvatar,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+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,
|
||||
},
|
||||
|
||||
@@ -52,7 +52,6 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||
});
|
||||
|
||||
let name: string;
|
||||
let isAway = false;
|
||||
let hideBranding = false;
|
||||
|
||||
const notFound = {
|
||||
@@ -96,7 +95,6 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||
}
|
||||
|
||||
name = username;
|
||||
isAway = user.away;
|
||||
hideBranding = user.hideBranding;
|
||||
}
|
||||
|
||||
@@ -129,7 +127,6 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||
eventData.length
|
||||
),
|
||||
booking,
|
||||
away: isAway,
|
||||
user: name,
|
||||
slug,
|
||||
trpcState: ssr.dehydrate(),
|
||||
|
||||
@@ -33,7 +33,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
logo: true,
|
||||
logoUrl: 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) {
|
||||
|
||||
@@ -53,7 +53,10 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
|
||||
if ((process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true" && !token) || signupDisabled) {
|
||||
return {
|
||||
notFound: true,
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: `/auth/error?error=Signup is disabled in this instance`,
|
||||
},
|
||||
} as const;
|
||||
}
|
||||
|
||||
@@ -98,7 +101,10 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
|
||||
if (!verificationToken || verificationToken.expires < new Date()) {
|
||||
return {
|
||||
notFound: true,
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: `/auth/error?error=Verification Token is missing or has expired`,
|
||||
},
|
||||
} as const;
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
metadata: eventData.metadata,
|
||||
},
|
||||
booking,
|
||||
away: false,
|
||||
user: teamSlug,
|
||||
teamId: team.id,
|
||||
slug: meetingSlug,
|
||||
|
||||
@@ -98,6 +98,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
isPrivate: true,
|
||||
isOrganization: true,
|
||||
metadata: true,
|
||||
logoUrl: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -213,6 +214,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">
|
||||
@@ -860,10 +877,10 @@ export default function Success(props: PageProps) {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="my-3 flex justify-end">
|
||||
<div className="my-3 flex justify-center space-x-1">
|
||||
<button
|
||||
className={classNames(
|
||||
"m-1 items-center justify-center rounded-full border p-1 text-2xl hover:opacity-100",
|
||||
"flex h-10 w-10 items-center justify-center rounded-full border text-2xl hover:opacity-100",
|
||||
rateValue === 1
|
||||
? "border-focus bg-emphasis"
|
||||
: "border-muted bg-default opacity-50"
|
||||
@@ -874,7 +891,7 @@ export default function Success(props: PageProps) {
|
||||
</button>
|
||||
<button
|
||||
className={classNames(
|
||||
"m-1 items-center justify-center rounded-full border p-1 text-2xl hover:opacity-100",
|
||||
"flex h-10 w-10 items-center justify-center rounded-full border text-2xl hover:opacity-100",
|
||||
rateValue === 2
|
||||
? "border-focus bg-emphasis"
|
||||
: "border-muted bg-default opacity-50"
|
||||
@@ -885,7 +902,7 @@ export default function Success(props: PageProps) {
|
||||
</button>
|
||||
<button
|
||||
className={classNames(
|
||||
"m-1 items-center justify-center rounded-full border p-1 text-2xl hover:opacity-100",
|
||||
"flex h-10 w-10 items-center justify-center rounded-full border text-2xl hover:opacity-100",
|
||||
rateValue === 3
|
||||
? "border-focus bg-emphasis"
|
||||
: " border-muted bg-default opacity-50"
|
||||
@@ -896,7 +913,7 @@ export default function Success(props: PageProps) {
|
||||
</button>
|
||||
<button
|
||||
className={classNames(
|
||||
"m-1 items-center justify-center rounded-full border p-1 text-2xl hover:opacity-100",
|
||||
"flex h-10 w-10 items-center justify-center rounded-full border text-2xl hover:opacity-100",
|
||||
rateValue === 4
|
||||
? "border-focus bg-emphasis"
|
||||
: "border-muted bg-default opacity-50"
|
||||
@@ -907,7 +924,7 @@ export default function Success(props: PageProps) {
|
||||
</button>
|
||||
<button
|
||||
className={classNames(
|
||||
"m-1 items-center justify-center rounded-full border p-1 text-2xl hover:opacity-100",
|
||||
"flex h-10 w-10 items-center justify-center rounded-full border text-2xl hover:opacity-100",
|
||||
rateValue === 5
|
||||
? "border-focus bg-emphasis"
|
||||
: "border-muted bg-default opacity-50"
|
||||
|
||||
@@ -34,11 +34,26 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
} as const;
|
||||
return redirect;
|
||||
}
|
||||
|
||||
await ssr.viewer.eventTypes.get.prefetch({ id: typeParam });
|
||||
|
||||
const { eventType } = await ssr.viewer.eventTypes.get.fetch({ id: typeParam });
|
||||
|
||||
const getEventTypeById = async (eventTypeId: number) => {
|
||||
await ssr.viewer.eventTypes.get.prefetch({ id: eventTypeId });
|
||||
try {
|
||||
const { eventType } = await ssr.viewer.eventTypes.get.fetch({ id: eventTypeId });
|
||||
return eventType;
|
||||
} catch (e: unknown) {
|
||||
// reject, user has no access to this event type.
|
||||
return null;
|
||||
}
|
||||
};
|
||||
const eventType = await getEventTypeById(typeParam);
|
||||
if (!eventType) {
|
||||
const redirect = {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/event-types",
|
||||
},
|
||||
} as const;
|
||||
return redirect;
|
||||
}
|
||||
return {
|
||||
props: {
|
||||
eventType,
|
||||
|
||||
@@ -767,7 +767,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
}, {});
|
||||
|
||||
if (dirtyFieldExists) {
|
||||
updateMutation.mutate({ ...filteredPayload, id: eventType.id });
|
||||
updateMutation.mutate({ ...filteredPayload, id: eventType.id, hashedLink: values.hashedLink });
|
||||
}
|
||||
}}>
|
||||
<div ref={animationParentRef}>{tabMap[tabName]}</div>
|
||||
|
||||
@@ -39,13 +39,14 @@ export type UserPageProps = {
|
||||
allowSEOIndexing: boolean;
|
||||
username: string | null;
|
||||
};
|
||||
users: (Pick<User, "away" | "name" | "username" | "bio" | "verified" | "avatarUrl"> & {
|
||||
users: (Pick<User, "name" | "username" | "bio" | "verified" | "avatarUrl"> & {
|
||||
profile: UserProfile;
|
||||
})[];
|
||||
themeBasis: string | null;
|
||||
markdownStrippedBio: string;
|
||||
safeBio: string;
|
||||
entity: {
|
||||
logoUrl?: string | null;
|
||||
considerUnpublished: boolean;
|
||||
orgSlug?: string | null;
|
||||
name?: string | null;
|
||||
@@ -98,12 +99,7 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
|
||||
orgSlug: isValidOrgDomain ? currentOrgDomain : null,
|
||||
});
|
||||
|
||||
const usersWithoutAvatar = usersInOrgContext.map((user) => {
|
||||
const { avatar: _1, ...rest } = user;
|
||||
return rest;
|
||||
});
|
||||
|
||||
const isDynamicGroup = usersWithoutAvatar.length > 1;
|
||||
const isDynamicGroup = usersInOrgContext.length > 1;
|
||||
log.debug(safeStringify({ usersInOrgContext, isValidOrgDomain, currentOrgDomain, isDynamicGroup }));
|
||||
|
||||
if (isDynamicGroup) {
|
||||
@@ -114,40 +110,27 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
|
||||
permanent: false,
|
||||
destination: destinationUrl,
|
||||
},
|
||||
} as {
|
||||
redirect: {
|
||||
permanent: false;
|
||||
destination: string;
|
||||
};
|
||||
};
|
||||
} as const;
|
||||
}
|
||||
|
||||
const users = usersWithoutAvatar.map((user) => ({
|
||||
...user,
|
||||
avatar: `/${user.username}/avatar.png`,
|
||||
}));
|
||||
|
||||
const isNonOrgUser = (user: { profile: UserProfile }) => {
|
||||
return !user.profile?.organization;
|
||||
};
|
||||
|
||||
const isThereAnyNonOrgUser = users.some(isNonOrgUser);
|
||||
const isThereAnyNonOrgUser = usersInOrgContext.some(isNonOrgUser);
|
||||
|
||||
if (!users.length || (!isValidOrgDomain && !isThereAnyNonOrgUser)) {
|
||||
if (!usersInOrgContext.length || (!isValidOrgDomain && !isThereAnyNonOrgUser)) {
|
||||
return {
|
||||
notFound: true,
|
||||
} as {
|
||||
notFound: true;
|
||||
};
|
||||
} as const;
|
||||
}
|
||||
|
||||
const [user] = users; //to be used when dealing with single user, not dynamic group
|
||||
const [user] = usersInOrgContext; //to be used when dealing with single user, not dynamic group
|
||||
|
||||
const profile = {
|
||||
name: user.name || user.username || "",
|
||||
image: getUserAvatarUrl({
|
||||
...user,
|
||||
profile: user.profile,
|
||||
avatarUrl: user.avatarUrl,
|
||||
}),
|
||||
theme: user.theme,
|
||||
brandColor: user.brandColor ?? DEFAULT_LIGHT_BRAND_COLOR,
|
||||
@@ -183,20 +166,20 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
|
||||
const safeBio = markdownToSafeHTML(user.bio) || "";
|
||||
|
||||
const markdownStrippedBio = stripMarkdown(user?.bio || "");
|
||||
const org = usersWithoutAvatar[0].profile.organization;
|
||||
const org = usersInOrgContext[0].profile.organization;
|
||||
|
||||
return {
|
||||
props: {
|
||||
users: users.map((user) => ({
|
||||
users: usersInOrgContext.map((user) => ({
|
||||
name: user.name,
|
||||
username: user.username,
|
||||
bio: user.bio,
|
||||
avatarUrl: user.avatarUrl,
|
||||
verified: user.verified,
|
||||
profile: user.profile,
|
||||
away: user.away,
|
||||
})),
|
||||
entity: {
|
||||
...(org?.logoUrl ? { logoUrl: org?.logoUrl } : {}),
|
||||
considerUnpublished: !isARedirectFromNonOrgLink && org?.slug === null,
|
||||
orgSlug: currentOrgDomain,
|
||||
name: org?.name ?? null,
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import classNames from "classnames";
|
||||
import type { InferGetServerSidePropsType } from "next";
|
||||
import Link from "next/link";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
import {
|
||||
@@ -23,7 +22,6 @@ import { type getServerSideProps } from "./users-public-view.getServerSideProps"
|
||||
|
||||
export function UserPage(props: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
const { users, profile, eventTypes, markdownStrippedBio, entity } = props;
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const [user] = users; //To be used when we only have a single user, not dynamic group
|
||||
useTheme(profile.theme);
|
||||
@@ -43,8 +41,6 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
|
||||
...query
|
||||
} = useRouterQuery();
|
||||
|
||||
const isRedirect = searchParams?.get("redirected") === "true" || false;
|
||||
const fromUserNameRedirected = searchParams?.get("username") || "";
|
||||
/*
|
||||
const telemetry = useTelemetry();
|
||||
useEffect(() => {
|
||||
@@ -102,13 +98,13 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
|
||||
{!isOrg && user.verified && (
|
||||
<Icon
|
||||
name="badge-check"
|
||||
className=" mx-1 -mt-1 inline h-6 w-6 fill-blue-500 text-white dark:text-black"
|
||||
className="mx-1 -mt-1 inline h-6 w-6 fill-blue-500 text-white dark:text-black"
|
||||
/>
|
||||
)}
|
||||
{isOrg && (
|
||||
<Icon
|
||||
name="badge-check"
|
||||
className=" mx-1 -mt-1 inline h-6 w-6 fill-yellow-500 text-white dark:text-black"
|
||||
className="mx-1 -mt-1 inline h-6 w-6 fill-yellow-500 text-white dark:text-black"
|
||||
/>
|
||||
)}
|
||||
</h1>
|
||||
@@ -125,47 +121,38 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
|
||||
<div
|
||||
className={classNames("rounded-md ", !isEventListEmpty && "border-subtle border")}
|
||||
data-testid="event-types">
|
||||
{user.away ? (
|
||||
<div className="overflow-hidden rounded-sm border ">
|
||||
<div className="text-muted p-8 text-center">
|
||||
<h2 className="font-cal text-default mb-2 text-3xl">😴{` ${t("user_away")}`}</h2>
|
||||
<p className="mx-auto max-w-md">{t("user_away_description") as string}</p>
|
||||
{eventTypes.map((type) => (
|
||||
<div
|
||||
key={type.id}
|
||||
style={{ display: "flex", ...eventTypeListItemEmbedStyles }}
|
||||
className="bg-default border-subtle dark:bg-muted dark:hover:bg-emphasis hover:bg-muted group relative border-b first:rounded-t-md last:rounded-b-md last:border-b-0">
|
||||
<Icon
|
||||
name="arrow-right"
|
||||
className="text-emphasis absolute right-4 top-4 h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
/>
|
||||
{/* Don't prefetch till the time we drop the amount of javascript in [user][type] page which is impacting score for [user] page */}
|
||||
<div className="block w-full p-5">
|
||||
<Link
|
||||
prefetch={false}
|
||||
href={{
|
||||
pathname: `/${user.profile.username}/${type.slug}`,
|
||||
query,
|
||||
}}
|
||||
passHref
|
||||
onClick={async () => {
|
||||
sdkActionManager?.fire("eventTypeSelected", {
|
||||
eventType: type,
|
||||
});
|
||||
}}
|
||||
data-testid="event-type-link">
|
||||
<div className="flex flex-wrap items-center">
|
||||
<h2 className="text-default pr-2 text-sm font-semibold">{type.title}</h2>
|
||||
</div>
|
||||
<EventTypeDescription eventType={type} isPublic={true} shortenDescription />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
eventTypes.map((type) => (
|
||||
<div
|
||||
key={type.id}
|
||||
style={{ display: "flex", ...eventTypeListItemEmbedStyles }}
|
||||
className="bg-default border-subtle dark:bg-muted dark:hover:bg-emphasis hover:bg-muted group relative border-b first:rounded-t-md last:rounded-b-md last:border-b-0">
|
||||
<Icon
|
||||
name="arrow-right"
|
||||
className="text-emphasis absolute right-4 top-4 h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
/>
|
||||
{/* Don't prefetch till the time we drop the amount of javascript in [user][type] page which is impacting score for [user] page */}
|
||||
<div className="block w-full p-5">
|
||||
<Link
|
||||
prefetch={false}
|
||||
href={{
|
||||
pathname: `/${user.profile.username}/${type.slug}`,
|
||||
query,
|
||||
}}
|
||||
passHref
|
||||
onClick={async () => {
|
||||
sdkActionManager?.fire("eventTypeSelected", {
|
||||
eventType: type,
|
||||
});
|
||||
}}
|
||||
data-testid="event-type-link">
|
||||
<div className="flex flex-wrap items-center">
|
||||
<h2 className=" text-default pr-2 text-sm font-semibold">{type.title}</h2>
|
||||
</div>
|
||||
<EventTypeDescription eventType={type} isPublic={true} shortenDescription />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
|
||||
{isEventListEmpty && <EmptyPage name={profile.name || "User"} />}
|
||||
|
||||
@@ -86,7 +86,6 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
||||
booking,
|
||||
user: usernames.join("+"),
|
||||
slug,
|
||||
away: false,
|
||||
trpcState: ssr.dehydrate(),
|
||||
isBrandingHidden: false,
|
||||
isSEOIndexable: true,
|
||||
@@ -165,7 +164,6 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||
},
|
||||
user: username,
|
||||
slug,
|
||||
away: false,
|
||||
trpcState: ssr.dehydrate(),
|
||||
isBrandingHidden: user?.hideBranding,
|
||||
isSEOIndexable: user?.allowSEOIndexing,
|
||||
|
||||
@@ -23,7 +23,6 @@ export default function Type({
|
||||
user,
|
||||
isEmbed,
|
||||
booking,
|
||||
away,
|
||||
isBrandingHidden,
|
||||
isSEOIndexable,
|
||||
rescheduleUid,
|
||||
@@ -47,7 +46,6 @@ export default function Type({
|
||||
username={user}
|
||||
eventSlug={slug}
|
||||
bookingData={booking}
|
||||
isAway={away}
|
||||
hideBranding={isBrandingHidden}
|
||||
entity={eventData.entity}
|
||||
durationConfig={eventData.metadata?.multipleDuration}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { useTranscription } from "@daily-co/daily-react";
|
||||
import { useDaily, useDailyEvent } from "@daily-co/daily-react";
|
||||
import React, { Fragment, useCallback, useRef, useState, useLayoutEffect, useEffect } from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { showToast } from "@calcom/ui";
|
||||
|
||||
export const CalAiTransctibe = () => {
|
||||
const daily = useDaily();
|
||||
const { t } = useLocale();
|
||||
|
||||
const [transcript, setTranscript] = useState("");
|
||||
|
||||
const [transcriptHeight, setTranscriptHeight] = useState(0);
|
||||
const transcriptRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const transcription = useTranscription();
|
||||
|
||||
useDailyEvent(
|
||||
"app-message",
|
||||
useCallback((ev) => {
|
||||
const data = ev?.data;
|
||||
if (data.user_name && data.text) setTranscript(`${data.user_name}: ${data.text}`);
|
||||
}, [])
|
||||
);
|
||||
|
||||
useDailyEvent("transcription-started", (ev) => {
|
||||
showToast(t("transcription_enabled"), "success");
|
||||
});
|
||||
|
||||
useDailyEvent("transcription-stopped", (ev) => {
|
||||
showToast(t("transcription_stopped"), "success");
|
||||
});
|
||||
|
||||
useDailyEvent("custom-button-click", (ev) => {
|
||||
if (ev?.button_id !== "transcription") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (transcription?.isTranscribing) {
|
||||
daily?.stopTranscription();
|
||||
} else {
|
||||
daily?.startTranscription();
|
||||
}
|
||||
});
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
setTranscriptHeight(entry.target.scrollHeight);
|
||||
}
|
||||
});
|
||||
|
||||
if (transcriptRef.current) {
|
||||
observer.observe(transcriptRef.current);
|
||||
}
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
transcriptRef.current?.scrollTo({
|
||||
top: transcriptRef.current?.scrollHeight,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}, [transcriptHeight]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toaster position="bottom-right" />
|
||||
<div
|
||||
id="cal-ai-thing"
|
||||
ref={transcriptRef}
|
||||
className="max-h-full overflow-x-hidden overflow-y-scroll p-2 text-center text-white">
|
||||
{transcript
|
||||
? transcript.split("\n").map((line, i) => (
|
||||
<Fragment key={`transcript-${i}`}>
|
||||
{i > 0 && <br />}
|
||||
{line}
|
||||
</Fragment>
|
||||
))
|
||||
: ""}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,22 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import type { DailyCall } from "@daily-co/daily-js";
|
||||
import DailyIframe from "@daily-co/daily-js";
|
||||
import { DailyProvider } from "@daily-co/daily-react";
|
||||
import Head from "next/head";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { APP_NAME, SEO_IMG_OGIMG_VIDEO, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { APP_NAME, SEO_IMG_OGIMG_VIDEO, WEBSITE_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { formatToLocalizedDate, formatToLocalizedTime } from "@calcom/lib/date-fns";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import { Icon } from "@calcom/ui";
|
||||
|
||||
import { CalAiTransctibe } from "~/videos/ai/ai-transcribe";
|
||||
|
||||
import { type PageProps } from "./videos-single-view.getServerSideProps";
|
||||
|
||||
export default function JoinCall(props: PageProps) {
|
||||
const { t } = useLocale();
|
||||
const { meetingUrl, meetingPassword, booking } = props;
|
||||
const [daily, setDaily] = useState<DailyCall | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const callFrame = DailyIframe.createFrame({
|
||||
@@ -42,8 +47,20 @@ export default function JoinCall(props: PageProps) {
|
||||
},
|
||||
url: meetingUrl,
|
||||
...(typeof meetingPassword === "string" && { token: meetingPassword }),
|
||||
customTrayButtons: {
|
||||
transcription: {
|
||||
label: "Cal.ai",
|
||||
tooltip: "Enable transcription powered by AI",
|
||||
iconPath: `${WEBAPP_URL}/sparkles.svg`,
|
||||
iconPathDarkMode: `${WEBAPP_URL}/sparkles.svg`,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
setDaily(callFrame);
|
||||
|
||||
callFrame.join();
|
||||
|
||||
return () => {
|
||||
callFrame.destroy();
|
||||
};
|
||||
@@ -67,30 +84,35 @@ export default function JoinCall(props: PageProps) {
|
||||
<meta property="twitter:title" content={`${APP_NAME} Video`} />
|
||||
<meta property="twitter:description" content={t("quick_video_meeting")} />
|
||||
</Head>
|
||||
<div style={{ zIndex: 2, position: "relative" }}>
|
||||
{booking?.user?.organization?.calVideoLogo ? (
|
||||
<img
|
||||
className="min-w-16 min-h-16 fixed z-10 hidden aspect-square h-16 w-16 rounded-full sm:inline-block"
|
||||
src={booking.user.organization.calVideoLogo}
|
||||
alt="My Org Logo"
|
||||
style={{
|
||||
top: 32,
|
||||
left: 32,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
className="fixed z-10 hidden sm:inline-block"
|
||||
src={`${WEBSITE_URL}/cal-logo-word-dark.svg`}
|
||||
alt="Logo"
|
||||
style={{
|
||||
top: 32,
|
||||
left: 32,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<VideoMeetingInfo booking={booking} />
|
||||
<DailyProvider callObject={daily}>
|
||||
<div className="mx-auto" style={{ zIndex: 2, position: "absolute", bottom: 60, width: "100%" }}>
|
||||
<CalAiTransctibe />
|
||||
</div>
|
||||
<div style={{ zIndex: 2, position: "relative" }}>
|
||||
{booking?.user?.organization?.calVideoLogo ? (
|
||||
<img
|
||||
className="min-w-16 min-h-16 fixed z-10 hidden aspect-square h-16 w-16 rounded-full sm:inline-block"
|
||||
src={booking.user.organization.calVideoLogo}
|
||||
alt="My Org Logo"
|
||||
style={{
|
||||
top: 32,
|
||||
left: 32,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
className="fixed z-10 hidden sm:inline-block"
|
||||
src={`${WEBSITE_URL}/cal-logo-word-dark.svg`}
|
||||
alt="Logo"
|
||||
style={{
|
||||
top: 32,
|
||||
left: 32,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<VideoMeetingInfo booking={booking} />
|
||||
</DailyProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
@@ -38,7 +38,8 @@
|
||||
"@calcom/trpc": "*",
|
||||
"@calcom/tsconfig": "*",
|
||||
"@calcom/ui": "*",
|
||||
"@daily-co/daily-js": "^0.37.0",
|
||||
"@daily-co/daily-js": "^0.59.0",
|
||||
"@daily-co/daily-react": "^0.17.2",
|
||||
"@formkit/auto-animate": "1.0.0-beta.5",
|
||||
"@glidejs/glide": "^3.5.2",
|
||||
"@hookform/error-message": "^2.0.0",
|
||||
@@ -121,6 +122,7 @@
|
||||
"react-timezone-select": "^1.4.0",
|
||||
"react-turnstile": "^1.1.3",
|
||||
"react-use-intercom": "1.5.1",
|
||||
"recoil": "^0.7.7",
|
||||
"remove-markdown": "^0.5.0",
|
||||
"rrule": "^2.7.1",
|
||||
"sanitize-html": "^2.10.0",
|
||||
|
||||
@@ -286,7 +286,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
in: userIds as number[],
|
||||
},
|
||||
},
|
||||
select: { id: true, name: true, email: true, avatar: true, username: true },
|
||||
select: { id: true, name: true, email: true, avatarUrl: true, username: true },
|
||||
});
|
||||
|
||||
const userHashMap = new Map();
|
||||
|
||||
@@ -12,6 +12,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
|
||||
res.setHeader("Content-Type", "text/html");
|
||||
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
|
||||
|
||||
res.write(
|
||||
await renderEmail("MonthlyDigestEmail", {
|
||||
language: t,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { z } from "zod";
|
||||
import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/core/videoClient";
|
||||
import { sendDailyVideoRecordingEmails } from "@calcom/emails";
|
||||
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
|
||||
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
|
||||
import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload";
|
||||
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
@@ -15,7 +15,7 @@ import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["recorded-daily-video"] });
|
||||
const log = logger.getSubLogger({ prefix: ["daily-video-webhook-handler"] });
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
@@ -116,7 +116,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const response = schema.safeParse(req.body);
|
||||
|
||||
log.debug(
|
||||
"Recording Request Body:",
|
||||
"Daily video recording webhook Request Body:",
|
||||
safeStringify({
|
||||
response,
|
||||
})
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import { createHmac } from "crypto";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/core/videoClient";
|
||||
import { sendDailyVideoTranscriptEmails } from "@calcom/emails";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
const testRequestSchema = z.object({
|
||||
test: z.enum(["test"]),
|
||||
});
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["send-daily-video-transcript-handler"] });
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
version: z.string(),
|
||||
type: z.string(),
|
||||
id: z.string(),
|
||||
payload: z
|
||||
.object({
|
||||
meeting_id: z.string(),
|
||||
end_ts: z.number().optional(),
|
||||
room: z.string(),
|
||||
start_ts: z.number().optional(),
|
||||
})
|
||||
.passthrough(),
|
||||
event_ts: z.number().optional(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (testRequestSchema.safeParse(req.body).success) {
|
||||
return res.status(200).json({ message: "Test request successful" });
|
||||
}
|
||||
|
||||
const hmacSecret = process.env.DAILY_MEETING_ENDED_WEBHOOK_SECRET;
|
||||
if (!hmacSecret) {
|
||||
return res.status(405).json({ message: "No Daily Webhook Secret" });
|
||||
}
|
||||
|
||||
const signature = `${req.headers["x-webhook-timestamp"]}.${JSON.stringify(req.body)}`;
|
||||
const base64DecodedSecret = Buffer.from(hmacSecret, "base64");
|
||||
const hmac = createHmac("sha256", base64DecodedSecret);
|
||||
const computed_signature = hmac.update(signature).digest("base64");
|
||||
|
||||
if (req.headers["x-webhook-signature"] !== computed_signature) {
|
||||
return res.status(403).json({ message: "Signature does not match" });
|
||||
}
|
||||
|
||||
const response = schema.safeParse(req.body);
|
||||
|
||||
log.debug(
|
||||
"Daily video transcript webhook Request Body:",
|
||||
safeStringify({
|
||||
response,
|
||||
})
|
||||
);
|
||||
|
||||
if (!response.success || response.data.type !== "meeting.ended") {
|
||||
return res.status(400).send({
|
||||
message: "Invalid Payload",
|
||||
});
|
||||
}
|
||||
|
||||
const { room, meeting_id } = response.data.payload;
|
||||
|
||||
try {
|
||||
const bookingReference = await prisma.bookingReference.findFirst({
|
||||
where: { type: "daily_video", uid: room, meetingId: room },
|
||||
select: { bookingId: true },
|
||||
});
|
||||
|
||||
if (!bookingReference || !bookingReference.bookingId) {
|
||||
log.error(
|
||||
"bookingReference Not found:",
|
||||
safeStringify({
|
||||
bookingReference,
|
||||
requestBody: req.body,
|
||||
})
|
||||
);
|
||||
return res.status(404).send({ message: "Booking reference not found" });
|
||||
}
|
||||
|
||||
const booking = await prisma.booking.findUniqueOrThrow({
|
||||
where: {
|
||||
id: bookingReference.bookingId,
|
||||
},
|
||||
select: {
|
||||
...bookingMinimalSelect,
|
||||
uid: true,
|
||||
location: true,
|
||||
isRecorded: true,
|
||||
eventTypeId: true,
|
||||
eventType: {
|
||||
select: {
|
||||
teamId: true,
|
||||
parentId: true,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
timeZone: true,
|
||||
email: true,
|
||||
name: true,
|
||||
locale: true,
|
||||
destinationCalendar: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!booking) {
|
||||
log.error(
|
||||
"Booking Not Found:",
|
||||
safeStringify({
|
||||
booking,
|
||||
requestBody: req.body,
|
||||
})
|
||||
);
|
||||
|
||||
return res.status(404).send({
|
||||
message: `Booking of room_name ${room} does not exist or does not contain daily video as location`,
|
||||
});
|
||||
}
|
||||
|
||||
const transcripts = await getAllTranscriptsAccessLinkFromRoomName(room);
|
||||
|
||||
if (!transcripts || !transcripts.length)
|
||||
return res.status(200).json({ message: `No Transcripts found for room name ${room}` });
|
||||
|
||||
const t = await getTranslation(booking?.user?.locale ?? "en", "common");
|
||||
const attendeesListPromises = booking.attendees.map(async (attendee) => {
|
||||
return {
|
||||
id: attendee.id,
|
||||
name: attendee.name,
|
||||
email: attendee.email,
|
||||
timeZone: attendee.timeZone,
|
||||
language: {
|
||||
translate: await getTranslation(attendee.locale ?? "en", "common"),
|
||||
locale: attendee.locale ?? "en",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const attendeesList = await Promise.all(attendeesListPromises);
|
||||
|
||||
// Send emails
|
||||
const evt: CalendarEvent = {
|
||||
type: booking.title,
|
||||
title: booking.title,
|
||||
description: booking.description || undefined,
|
||||
startTime: booking.startTime.toISOString(),
|
||||
endTime: booking.endTime.toISOString(),
|
||||
organizer: {
|
||||
email: booking?.userPrimaryEmail || booking.user?.email || "Email-less",
|
||||
name: booking.user?.name || "Nameless",
|
||||
timeZone: booking.user?.timeZone || "Europe/London",
|
||||
language: { translate: t, locale: booking?.user?.locale ?? "en" },
|
||||
},
|
||||
attendees: attendeesList,
|
||||
uid: booking.uid,
|
||||
};
|
||||
|
||||
await sendDailyVideoTranscriptEmails(evt, transcripts);
|
||||
|
||||
return res.status(200).json({ message: "Success" });
|
||||
} catch (err) {
|
||||
console.error("Error in /send-daily-video-transcript", err);
|
||||
return res.status(500).json({ message: "something went wrong" });
|
||||
}
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
POST: Promise.resolve({ default: handler }),
|
||||
});
|
||||
@@ -1,154 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
orgDomainConfig,
|
||||
whereClauseForOrgWithSlugOrRequestedSlug,
|
||||
} from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { AVATAR_FALLBACK } from "@calcom/lib/constants";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["team/[slug]"] });
|
||||
const querySchema = z
|
||||
.object({
|
||||
username: z.string(),
|
||||
teamname: z.string(),
|
||||
/**
|
||||
* Passed when we want to fetch avatar of a particular organization
|
||||
*/
|
||||
orgSlug: z.string(),
|
||||
/**
|
||||
* Allow fetching avatar of a particular organization
|
||||
* Avatars being public, we need not worry about others accessing it.
|
||||
*/
|
||||
orgId: z.string().transform((s) => Number(s)),
|
||||
})
|
||||
.partial();
|
||||
|
||||
async function getIdentityData(req: NextApiRequest) {
|
||||
const { username, teamname, orgId, orgSlug } = querySchema.parse(req.query);
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(req);
|
||||
|
||||
const org = isValidOrgDomain ? currentOrgDomain : null;
|
||||
|
||||
const orgQuery = orgId
|
||||
? {
|
||||
id: orgId,
|
||||
}
|
||||
: org
|
||||
? whereClauseForOrgWithSlugOrRequestedSlug(org)
|
||||
: null;
|
||||
|
||||
if (username) {
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
...(orgQuery
|
||||
? {
|
||||
profiles: {
|
||||
some: {
|
||||
username,
|
||||
organization: orgQuery,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
username,
|
||||
// If a user is moved, it isn't actually available outside of the organization. So, for non-org domain check for movedToProfileId
|
||||
movedToProfileId: null,
|
||||
}),
|
||||
},
|
||||
select: { avatar: true, email: true },
|
||||
});
|
||||
|
||||
if (users.length > 1) {
|
||||
throw new Error(`More than one user found for username "${username}"`);
|
||||
}
|
||||
const [user] = users;
|
||||
return {
|
||||
name: username,
|
||||
email: user?.email,
|
||||
avatar: user?.avatar,
|
||||
org,
|
||||
};
|
||||
}
|
||||
|
||||
if (teamname) {
|
||||
const team = await prisma.team.findFirst({
|
||||
where: {
|
||||
slug: teamname,
|
||||
parent: orgQuery,
|
||||
},
|
||||
select: { logo: true },
|
||||
});
|
||||
|
||||
return {
|
||||
org,
|
||||
name: teamname,
|
||||
email: null,
|
||||
avatar: getPlaceholderAvatar(team?.logo, teamname),
|
||||
};
|
||||
}
|
||||
|
||||
if (orgSlug) {
|
||||
const orgs = await prisma.team.findMany({
|
||||
where: {
|
||||
...whereClauseForOrgWithSlugOrRequestedSlug(orgSlug),
|
||||
},
|
||||
select: {
|
||||
slug: true,
|
||||
logo: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (orgs.length > 1) {
|
||||
// This should never happen, but instead of throwing error, we are just logging to be able to observe when it happens.
|
||||
log.error("More than one organization found for slug", orgSlug);
|
||||
}
|
||||
|
||||
const org = orgs[0];
|
||||
return {
|
||||
org: org?.slug,
|
||||
name: org?.name,
|
||||
email: null,
|
||||
avatar: getPlaceholderAvatar(org?.logo, org?.name),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const identity = await getIdentityData(req);
|
||||
const img = identity?.avatar;
|
||||
// If image isn't set or links to this route itself, use default avatar
|
||||
if (!img) {
|
||||
if (identity?.org) {
|
||||
res.setHeader("x-cal-org", identity.org);
|
||||
}
|
||||
res.writeHead(302, {
|
||||
Location: AVATAR_FALLBACK,
|
||||
});
|
||||
|
||||
return res.end();
|
||||
}
|
||||
|
||||
if (!img.includes("data:image")) {
|
||||
if (identity.org) {
|
||||
res.setHeader("x-cal-org", identity.org);
|
||||
}
|
||||
res.writeHead(302, { Location: img });
|
||||
return res.end();
|
||||
}
|
||||
|
||||
const decoded = img.toString().replace("data:image/png;base64,", "").replace("data:image/jpeg;base64,", "");
|
||||
const imageResp = Buffer.from(decoded, "base64");
|
||||
if (identity.org) {
|
||||
res.setHeader("x-cal-org", identity.org);
|
||||
}
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "image/png",
|
||||
"Content-Length": imageResp.length,
|
||||
});
|
||||
res.end(imageResp);
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { GetStaticPropsContext } from "next";
|
||||
import Link from "next/link";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import z from "zod";
|
||||
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, Icon } from "@calcom/ui";
|
||||
|
||||
@@ -17,11 +17,9 @@ const querySchema = z.object({
|
||||
|
||||
export default function Error() {
|
||||
const { t } = useLocale();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const { error } = querySchema.parse(searchParams);
|
||||
const isTokenVerificationError = error?.toLowerCase() === "verification";
|
||||
const errorMsg = isTokenVerificationError ? t("token_invalid_expired") : t("error_during_login");
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
const { error } = querySchema.parse({ error: searchParams?.get("error") || undefined });
|
||||
const errorMsg = error || t("error_during_login");
|
||||
return (
|
||||
<AuthContainer title="" description="">
|
||||
<div>
|
||||
@@ -30,11 +28,8 @@ export default function Error() {
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
<h3 className="text-emphasis text-lg font-medium leading-6" id="modal-title">
|
||||
{error}
|
||||
{errorMsg}
|
||||
</h3>
|
||||
<div className="mt-2">
|
||||
<p className="text-subtle text-sm">{errorMsg}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 sm:mt-6">
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ export default function Type({
|
||||
isEmbed,
|
||||
user,
|
||||
booking,
|
||||
away,
|
||||
isBrandingHidden,
|
||||
isTeamEvent,
|
||||
entity,
|
||||
@@ -33,7 +32,6 @@ export default function Type({
|
||||
username={user}
|
||||
eventSlug={slug}
|
||||
bookingData={booking}
|
||||
isAway={away}
|
||||
hideBranding={isBrandingHidden}
|
||||
isTeamEvent={isTeamEvent}
|
||||
entity={entity}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -21,7 +21,6 @@ export default function Type({
|
||||
slug,
|
||||
user,
|
||||
booking,
|
||||
away,
|
||||
isEmbed,
|
||||
isBrandingHidden,
|
||||
entity,
|
||||
@@ -42,7 +41,6 @@ export default function Type({
|
||||
username={user}
|
||||
eventSlug={slug}
|
||||
bookingData={booking}
|
||||
isAway={away}
|
||||
hideBranding={isBrandingHidden}
|
||||
isTeamEvent
|
||||
isInstantMeeting
|
||||
@@ -110,7 +108,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
eventData.length
|
||||
),
|
||||
booking: null,
|
||||
away: false,
|
||||
user: teamSlug,
|
||||
teamId: team.id,
|
||||
slug: meetingSlug,
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
|
||||
import { SMSLockState } from "@calcom/prisma/client";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { IconName } from "@calcom/ui";
|
||||
import { Avatar, DropdownActions, Table } from "@calcom/ui";
|
||||
|
||||
const { Cell, ColumnTitle, Header, Row } = Table;
|
||||
|
||||
type Props = {
|
||||
setSMSLockState: (param: { userId?: number; teamId?: number; lock: boolean }) => void;
|
||||
};
|
||||
|
||||
type User = {
|
||||
id: number;
|
||||
username: string | null;
|
||||
name: string | null;
|
||||
email: string;
|
||||
smsLockState: SMSLockState;
|
||||
avatarUrl: string | null;
|
||||
};
|
||||
|
||||
type Team = {
|
||||
id: number;
|
||||
name: string;
|
||||
smsLockState: SMSLockState;
|
||||
slug: string | null;
|
||||
logoUrl?: string | null;
|
||||
};
|
||||
|
||||
function UsersTable({ setSMSLockState }: Props) {
|
||||
const { data: usersAndTeams } = trpc.viewer.admin.getSMSLockStateTeamsUsers.useQuery();
|
||||
|
||||
if (!usersAndTeams) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const users = usersAndTeams.users.locked.concat(usersAndTeams.users.reviewNeeded);
|
||||
const teams = usersAndTeams.teams.locked.concat(usersAndTeams.teams.reviewNeeded);
|
||||
|
||||
return <LockStatusTable users={users} teams={teams} setSMSLockState={setSMSLockState} />;
|
||||
}
|
||||
|
||||
const LockStatusTable = ({
|
||||
users = [],
|
||||
teams = [],
|
||||
setSMSLockState,
|
||||
}: {
|
||||
users?: User[];
|
||||
teams?: Team[];
|
||||
setSMSLockState: (param: { userId?: number; teamId?: number; lock: boolean }) => void;
|
||||
}) => {
|
||||
function getActions({ user, team }: { user?: User; team?: Team }) {
|
||||
const smsLockState = user?.smsLockState ?? team?.smsLockState;
|
||||
if (!smsLockState) return [];
|
||||
|
||||
const actions = [
|
||||
{
|
||||
id: "unlock-sms",
|
||||
label: smsLockState === SMSLockState.LOCKED ? "Unlock SMS sending" : "Lock SMS sending",
|
||||
onClick: () =>
|
||||
setSMSLockState({
|
||||
userId: user ? user.id : undefined,
|
||||
teamId: team ? team.id : undefined,
|
||||
lock: smsLockState !== SMSLockState.LOCKED,
|
||||
}),
|
||||
icon: "lock" as IconName,
|
||||
},
|
||||
];
|
||||
if (smsLockState === SMSLockState.REVIEW_NEEDED) {
|
||||
actions.push({
|
||||
id: "reviewed",
|
||||
label: "Mark as Reviewed",
|
||||
onClick: () =>
|
||||
setSMSLockState({
|
||||
userId: user ? user.id : undefined,
|
||||
teamId: team ? team.id : undefined,
|
||||
lock: false,
|
||||
}),
|
||||
icon: "pencil" as IconName,
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table>
|
||||
<Header>
|
||||
<ColumnTitle widthClassNames="w-auto">User/Team</ColumnTitle>
|
||||
<ColumnTitle>Status</ColumnTitle>
|
||||
<ColumnTitle widthClassNames="w-auto">
|
||||
<span className="sr-only">Edit</span>
|
||||
</ColumnTitle>
|
||||
</Header>
|
||||
|
||||
<tbody className="divide-subtle divide-y rounded-md">
|
||||
{users.map((user) => (
|
||||
<Row key={`user-${user.id}`}>
|
||||
<Cell widthClassNames="w-auto">
|
||||
<div className="min-h-10 flex items-center">
|
||||
{" "}
|
||||
<Avatar
|
||||
size="md"
|
||||
alt={`Avatar of ${user.username || "Nameless"}`}
|
||||
imageSrc={getUserAvatarUrl(user)}
|
||||
/>
|
||||
<div className="text-subtle ml-4 font-medium">
|
||||
<span className="text-default">{user.name}</span>
|
||||
<span className="ml-3">/{user.username}</span>
|
||||
<br />
|
||||
<span className="break-all">{user.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Cell>
|
||||
|
||||
<Cell>{user.smsLockState}</Cell>
|
||||
<Cell widthClassNames="w-auto">
|
||||
<DropdownActions actions={getActions({ user })} />
|
||||
</Cell>
|
||||
</Row>
|
||||
))}
|
||||
{teams.map((team) => (
|
||||
<Row key={`team-${team.id}`}>
|
||||
<Cell widthClassNames="w-auto">
|
||||
<div className="min-h-10 flex items-center">
|
||||
<Avatar
|
||||
size="md"
|
||||
alt={`Avatar of ${team.name}`}
|
||||
imageSrc={getPlaceholderAvatar(team.logoUrl, team.name)}
|
||||
/>
|
||||
<div className="text-subtle ml-4 font-medium">
|
||||
<span className="text-default">{team.name}</span>
|
||||
<span className="ml-3 break-all">/team/{team.slug}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Cell>
|
||||
<Cell>{team.smsLockState}</Cell>
|
||||
<Cell widthClassNames="w-auto">
|
||||
<DropdownActions actions={getActions({ team })} />
|
||||
</Cell>
|
||||
</Row>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default UsersTable;
|
||||
@@ -0,0 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
import { getLayout } from "@components/auth/layouts/AdminLayout";
|
||||
|
||||
import LockedSMSView from "./lockedSMSView";
|
||||
|
||||
const LockedSMSPage = () => <LockedSMSView />;
|
||||
|
||||
LockedSMSPage.getLayout = getLayout;
|
||||
LockedSMSPage.PageWrapper = PageWrapper;
|
||||
|
||||
export default LockedSMSPage;
|
||||
@@ -0,0 +1,82 @@
|
||||
"use client";
|
||||
|
||||
import UsersTable from "@pages/settings/admin/lockedSMS/UsersTable";
|
||||
import { useState } from "react";
|
||||
|
||||
import { trpc } from "@calcom/trpc";
|
||||
import { Button, Meta, TextField, showToast } from "@calcom/ui";
|
||||
|
||||
export default function LockedSMSView() {
|
||||
const [username, setUsername] = useState("");
|
||||
const [teamSlug, setTeamSlug] = useState("");
|
||||
|
||||
const utils = trpc.useContext();
|
||||
|
||||
const mutation = trpc.viewer.admin.setSMSLockState.useMutation({
|
||||
onSuccess: (data) => {
|
||||
if (data) {
|
||||
showToast(`${data.name} successfully ${data.locked ? "locked" : "unlocked"}`, "success");
|
||||
}
|
||||
utils.viewer.admin.getSMSLockStateTeamsUsers.invalidate();
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast(`${error}`, "error");
|
||||
utils.viewer.admin.getSMSLockStateTeamsUsers.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
function setSMSLockState({ userId, teamId, lock }: { userId?: number; teamId?: number; lock: boolean }) {
|
||||
mutation.mutate({
|
||||
userId,
|
||||
teamId,
|
||||
lock,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Meta title="lockedSMS" description="Lock or unlock SMS sending for users" />
|
||||
<div className="mb-4 flex w-full items-center justify-between space-x-2 rtl:space-x-reverse">
|
||||
<div className="flex">
|
||||
<TextField
|
||||
name="Lock User"
|
||||
placeholder="username"
|
||||
defaultValue=""
|
||||
onChange={(event) => setUsername(event.target.value)}
|
||||
value={username}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
className="ml-2 mt-5"
|
||||
onClick={() => {
|
||||
mutation.mutate({ username, lock: true });
|
||||
utils.viewer.admin.getSMSLockStateTeamsUsers.invalidate();
|
||||
}}>
|
||||
Lock User
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<TextField
|
||||
name="Lock Team"
|
||||
placeholder="team slug"
|
||||
defaultValue=""
|
||||
onChange={(event) => {
|
||||
setTeamSlug(event.target.value);
|
||||
}}
|
||||
value={teamSlug}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
className="ml-2 mt-5"
|
||||
onClick={() => {
|
||||
mutation.mutate({ teamSlug, lock: true });
|
||||
utils.viewer.admin.getSMSLockStateTeamsUsers.invalidate();
|
||||
}}>
|
||||
Lock Team
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<UsersTable setSMSLockState={setSMSLockState} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -22,7 +22,6 @@ import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||
import type { Ensure } from "@calcom/types/utils";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
@@ -85,7 +84,7 @@ type Email = {
|
||||
|
||||
export type FormValues = {
|
||||
username: string;
|
||||
avatar: string;
|
||||
avatarUrl: string | null;
|
||||
name: string;
|
||||
email: string;
|
||||
bio: string;
|
||||
@@ -98,15 +97,10 @@ const ProfileView = () => {
|
||||
const { update } = useSession();
|
||||
const { data: user, isPending } = trpc.viewer.me.useQuery({ includePasswordAdded: true });
|
||||
|
||||
const { data: avatarData } = trpc.viewer.avatar.useQuery(undefined, {
|
||||
enabled: !isPending && !user?.avatarUrl,
|
||||
});
|
||||
|
||||
const updateProfileMutation = trpc.viewer.updateProfile.useMutation({
|
||||
onSuccess: async (res) => {
|
||||
await update(res);
|
||||
utils.viewer.me.invalidate();
|
||||
utils.viewer.avatar.invalidate();
|
||||
utils.viewer.shouldVerifyEmail.invalidate();
|
||||
|
||||
if (res.hasEmailBeenChanged && res.sendEmailVerification) {
|
||||
@@ -250,10 +244,7 @@ const ProfileView = () => {
|
||||
const userEmail = user.email || "";
|
||||
const defaultValues = {
|
||||
username: user.username || "",
|
||||
avatar: getUserAvatarUrl({
|
||||
...user,
|
||||
profile: user.profile,
|
||||
}),
|
||||
avatarUrl: user.avatarUrl,
|
||||
name: user.name || "",
|
||||
email: userEmail,
|
||||
bio: user.bio || "",
|
||||
@@ -284,7 +275,7 @@ const ProfileView = () => {
|
||||
key={JSON.stringify(defaultValues)}
|
||||
defaultValues={defaultValues}
|
||||
isPending={updateProfileMutation.isPending}
|
||||
isFallbackImg={!user.avatarUrl && !avatarData?.avatar}
|
||||
isFallbackImg={!user.avatarUrl}
|
||||
user={user}
|
||||
userOrganization={user.organization}
|
||||
onSubmit={(values) => {
|
||||
@@ -529,7 +520,7 @@ const ProfileForm = ({
|
||||
|
||||
const profileFormSchema = z.object({
|
||||
username: z.string(),
|
||||
avatar: z.string(),
|
||||
avatarUrl: z.string().nullable(),
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
@@ -622,17 +613,9 @@ const ProfileForm = ({
|
||||
<div className="flex items-center">
|
||||
<Controller
|
||||
control={formMethods.control}
|
||||
name="avatar"
|
||||
render={({ field: { value } }) => {
|
||||
const showRemoveAvatarButton = value === null ? false : !isFallbackImg;
|
||||
const organization =
|
||||
userOrganization && userOrganization.id
|
||||
? {
|
||||
...(userOrganization as Ensure<NonNullable<typeof user.organization>, "id">),
|
||||
slug: userOrganization.slug || null,
|
||||
requestedSlug: userOrganization.metadata?.requestedSlug || null,
|
||||
}
|
||||
: null;
|
||||
name="avatarUrl"
|
||||
render={({ field: { value, onChange } }) => {
|
||||
const showRemoveAvatarButton = value !== null;
|
||||
return (
|
||||
<>
|
||||
<UserAvatar data-testid="profile-upload-avatar" previewSrc={value} size="lg" user={user} />
|
||||
@@ -644,9 +627,9 @@ const ProfileForm = ({
|
||||
id="avatar-upload"
|
||||
buttonMsg={t("upload_avatar")}
|
||||
handleAvatarChange={(newAvatar) => {
|
||||
formMethods.setValue("avatar", newAvatar, { shouldDirty: true });
|
||||
onChange(newAvatar);
|
||||
}}
|
||||
imageSrc={value}
|
||||
imageSrc={getUserAvatarUrl({ avatarUrl: value })}
|
||||
triggerButtonColor={showRemoveAvatarButton ? "secondary" : "primary"}
|
||||
/>
|
||||
|
||||
@@ -654,7 +637,7 @@ const ProfileForm = ({
|
||||
<Button
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
formMethods.setValue("avatar", "", { shouldDirty: true });
|
||||
onChange(null);
|
||||
}}>
|
||||
{t("remove")}
|
||||
</Button>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useEffect } from "react";
|
||||
|
||||
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||
import EventTypeDescription from "@calcom/features/eventtypes/components/EventTypeDescription";
|
||||
import { getOrgAvatarUrl, getTeamAvatarUrl } from "@calcom/lib/getAvatarUrl";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
@@ -67,6 +67,7 @@ function TeamPage({
|
||||
<div className="flex h-full min-h-[100dvh] items-center justify-center">
|
||||
<UnpublishedEntity
|
||||
{...{ [slugPropertyName]: team.parent ? parentSlug : teamSlug }}
|
||||
logoUrl={team.parent?.logoUrl || team.logoUrl}
|
||||
name={team.parent ? team.parent.name : team.name}
|
||||
/>
|
||||
</div>
|
||||
@@ -161,10 +162,7 @@ function TeamPage({
|
||||
</div>
|
||||
);
|
||||
|
||||
const profileImageSrc =
|
||||
isValidOrgDomain || team.isOrganization
|
||||
? getOrgAvatarUrl({ slug: currentOrgDomain, logoUrl: team.logoUrl })
|
||||
: getTeamAvatarUrl({ slug: team.slug, logoUrl: team.logoUrl, organizationId: team.parent?.id });
|
||||
const profileImageSrc = getPlaceholderAvatar(team.logoUrl || team.parent?.logoUrl, team.name);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -30,7 +30,6 @@ export default function Type({
|
||||
slug,
|
||||
user,
|
||||
booking,
|
||||
away,
|
||||
isEmbed,
|
||||
isBrandingHidden,
|
||||
eventData,
|
||||
@@ -54,7 +53,6 @@ export default function Type({
|
||||
username={user}
|
||||
eventSlug={slug}
|
||||
bookingData={booking}
|
||||
isAway={away}
|
||||
isInstantMeeting={isInstantMeeting}
|
||||
hideBranding={isBrandingHidden}
|
||||
isTeamEvent
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -74,10 +74,6 @@ test.describe("Change username on settings", () => {
|
||||
});
|
||||
|
||||
expect(updatedUser.username).toBe("demo.username");
|
||||
|
||||
// Check if user avatar can be accessed and response headers contain 'image/' in the content type
|
||||
const response = await page.goto("/demo.username/avatar.png");
|
||||
expect(response?.headers()?.["content-type"]).toContain("image/");
|
||||
});
|
||||
|
||||
test("User can update to PREMIUM username", async ({ page, users }, testInfo) => {
|
||||
|
||||
@@ -3,12 +3,17 @@ import type { Page } from "@playwright/test";
|
||||
export const createEmbedsFixture = (page: Page) => {
|
||||
return {
|
||||
/**
|
||||
* @deprecated Use gotoPlayground instead
|
||||
* @deprecated
|
||||
* Use 'gotoPlayground' instead, to navigate. It calls `addEmbedListeners` automatically.
|
||||
*/
|
||||
async addEmbedListeners(calNamespace: string) {
|
||||
await page.addInitScript(
|
||||
({ calNamespace }: { calNamespace: string }) => {
|
||||
console.log("PlaywrightTest:", "Adding listener for __iframeReady on namespace:", calNamespace);
|
||||
console.log(
|
||||
"PlaywrightTest - InitScript:",
|
||||
"Adding listener for __iframeReady on namespace:",
|
||||
calNamespace
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
window.eventsFiredStoreForPlaywright = window.eventsFiredStoreForPlaywright || {};
|
||||
@@ -43,14 +48,18 @@ export const createEmbedsFixture = (page: Page) => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
api = window.Cal.ns[calNamespace];
|
||||
console.log("Using api from namespace-", { calNamespace, api });
|
||||
}
|
||||
if (!api) {
|
||||
console.log(`namespace "${calNamespace}" not found yet - Trying again`);
|
||||
setTimeout(tryAddingListener, 500);
|
||||
return;
|
||||
}
|
||||
console.log("PlaywrightTest:", `Adding listener for __iframeReady on namespace:${calNamespace}`);
|
||||
if (!api) {
|
||||
throw new Error(`namespace "${calNamespace}" not found`);
|
||||
}
|
||||
api("on", {
|
||||
action: "*",
|
||||
callback: (e) => {
|
||||
console.log("Playwright Embed Fixture: Received event", e);
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
window.iframeReady = true; // Technically if there are multiple cal embeds, it can be set due to some other iframe. But it works for now. Improve it when it doesn't work
|
||||
|
||||
@@ -10,11 +10,12 @@ const getRandomSlug = () => `org-${Math.random().toString(36).substring(7)}`;
|
||||
export const createOrgsFixture = (page: Page) => {
|
||||
const store = { orgs: [], page } as { orgs: Team[]; page: typeof page };
|
||||
return {
|
||||
create: async (opts: { name: string; slug?: string; requestedSlug?: string }) => {
|
||||
create: async (opts: { name: string; slug?: string; requestedSlug?: string; isPrivate?: boolean }) => {
|
||||
const org = await createOrgInDb({
|
||||
name: opts.name,
|
||||
slug: opts.slug || getRandomSlug(),
|
||||
requestedSlug: opts.requestedSlug,
|
||||
isPrivate: opts.isPrivate,
|
||||
});
|
||||
const orgWithMetadata = {
|
||||
...org,
|
||||
@@ -39,16 +40,19 @@ export async function createOrgInDb({
|
||||
name,
|
||||
slug,
|
||||
requestedSlug,
|
||||
isPrivate,
|
||||
}: {
|
||||
name: string;
|
||||
slug: string | null;
|
||||
requestedSlug?: string;
|
||||
isPrivate?: boolean;
|
||||
}) {
|
||||
return await prisma.team.create({
|
||||
data: {
|
||||
name: name,
|
||||
slug: slug,
|
||||
isOrganization: true,
|
||||
isPrivate: isPrivate,
|
||||
metadata: {
|
||||
...(requestedSlug
|
||||
? {
|
||||
|
||||
@@ -788,7 +788,7 @@ const createUser = (
|
||||
},
|
||||
},
|
||||
accepted: true,
|
||||
role: MembershipRole.ADMIN,
|
||||
role,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@ import { bookTimeSlot, selectFirstAvailableTimeSlotNextMonth } from "./lib/testU
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
// TODO: This test is very flaky. Feels like tossing a coin and hope that it won't fail. Needs to be revisited.
|
||||
test.fixme("hash my url", () => {
|
||||
test.describe("hash my url", () => {
|
||||
test.beforeEach(async ({ users }) => {
|
||||
const user = await users.create();
|
||||
await user.apiLogin();
|
||||
@@ -51,5 +51,17 @@ test.fixme("hash my url", () => {
|
||||
// we wait for the hashedLink setting to load
|
||||
const $newUrl = await page.locator('//*[@data-testid="generated-hash-url"]').inputValue();
|
||||
expect($url !== $newUrl).toBeTruthy();
|
||||
|
||||
// Ensure that private URL is enabled after modifying the event type.
|
||||
// Additionally, if the slug is changed, ensure that the private URL is updated accordingly.
|
||||
await page.getByTestId("vertical-tab-event_setup_tab_title").click();
|
||||
await page.locator("[data-testid=event-title]").fill("somethingrandom");
|
||||
await page.locator("[data-testid=event-slug]").fill("somethingrandom");
|
||||
await page.locator("[data-testid=update-eventtype]").click();
|
||||
await page.getByTestId("toast-success").waitFor();
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.locator(".primary-navigation >> text=Advanced").click();
|
||||
const $url2 = await page.locator('//*[@data-testid="generated-hash-url"]').inputValue();
|
||||
expect($url2.includes("somethingrandom")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { test } from "../lib/fixtures";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.afterEach(async ({ users, orgs }) => {
|
||||
await users.deleteAll();
|
||||
await orgs.deleteAll();
|
||||
});
|
||||
|
||||
test.describe("Organization - Privacy", () => {
|
||||
test(`Private Org \n
|
||||
1) Org Member cannot see members of orgs\n
|
||||
2) Org Owner/Admin can see members`, async ({ page, users, orgs }) => {
|
||||
const org = await orgs.create({
|
||||
name: "TestOrg",
|
||||
isPrivate: true,
|
||||
});
|
||||
const teamMatesObj = [
|
||||
{ name: "teammate-1" },
|
||||
{ name: "teammate-2" },
|
||||
{ name: "teammate-3" },
|
||||
{ name: "teammate-4" },
|
||||
];
|
||||
|
||||
const owner = await users.create(
|
||||
{
|
||||
username: "pro-user",
|
||||
name: "pro-user",
|
||||
organizationId: org.id,
|
||||
roleInOrganization: "OWNER",
|
||||
},
|
||||
{
|
||||
hasTeam: true,
|
||||
teammates: teamMatesObj,
|
||||
}
|
||||
);
|
||||
const memberInOrg = await users.create({
|
||||
username: "org-member-user",
|
||||
name: "org-member-user",
|
||||
organizationId: org.id,
|
||||
roleInOrganization: "MEMBER",
|
||||
});
|
||||
|
||||
await owner.apiLogin();
|
||||
await page.goto("/settings/organizations/members");
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
const tableLocator = await page.getByTestId("user-list-data-table");
|
||||
|
||||
await expect(tableLocator).toBeVisible();
|
||||
|
||||
await memberInOrg.apiLogin();
|
||||
await page.goto("/settings/organizations/members");
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
const userDataTable = await page.getByTestId("user-list-data-table");
|
||||
const membersPrivacyWarning = await page.getByTestId("members-privacy-warning");
|
||||
await expect(userDataTable).toBeHidden();
|
||||
await expect(membersPrivacyWarning).toBeVisible();
|
||||
});
|
||||
test(`Private Org - Private Team\n
|
||||
1) Team Member cannot see members in team\n
|
||||
2) Team Admin/Owner can see members in team`, async ({ page, users, orgs }) => {
|
||||
const org = await orgs.create({
|
||||
name: "TestOrg",
|
||||
isPrivate: true,
|
||||
});
|
||||
const teamMatesObj = [
|
||||
{ name: "teammate-1" },
|
||||
{ name: "teammate-2" },
|
||||
{ name: "teammate-3" },
|
||||
{ name: "teammate-4" },
|
||||
];
|
||||
|
||||
const owner = await users.create(
|
||||
{
|
||||
username: "pro-user",
|
||||
name: "pro-user",
|
||||
organizationId: org.id,
|
||||
roleInOrganization: "OWNER",
|
||||
},
|
||||
{
|
||||
hasTeam: true,
|
||||
teammates: teamMatesObj,
|
||||
}
|
||||
);
|
||||
|
||||
await owner.apiLogin();
|
||||
const membership = await owner.getFirstTeamMembership();
|
||||
const teamId = membership.team.id;
|
||||
|
||||
// Update team to be private
|
||||
await page.goto(`/settings/teams/${teamId}/members`);
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
const togglePrivateSwitch = await page.getByTestId("make-team-private-check");
|
||||
await togglePrivateSwitch.click();
|
||||
|
||||
// As admin/owner we can see the user list
|
||||
const tableLocator = await page.getByTestId("team-member-list-container");
|
||||
await expect(tableLocator).toBeVisible();
|
||||
|
||||
const memberUser = await prisma.membership.findFirst({
|
||||
where: {
|
||||
teamId,
|
||||
role: "MEMBER",
|
||||
},
|
||||
select: {
|
||||
user: {
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(memberUser?.user.email).toBeDefined();
|
||||
// @ts-expect-error expect doesnt assert on a type level
|
||||
const memberOfTeam = await users.set(memberUser?.user.email);
|
||||
await memberOfTeam.apiLogin();
|
||||
|
||||
await page.goto(`/settings/teams/${teamId}/members`);
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
// As a user we can not see the user list when a team is private
|
||||
const hiddenTableLocator = await page.getByTestId("team-member-list-container");
|
||||
await expect(hiddenTableLocator).toBeHidden();
|
||||
});
|
||||
test(`Private Org - Public Team\n
|
||||
1) All team members can see members in team \n
|
||||
2) Team Admin/Owner can see members in team`, async ({ page, users, orgs }) => {
|
||||
const org = await orgs.create({
|
||||
name: "TestOrg",
|
||||
});
|
||||
const teamMatesObj = [
|
||||
{ name: "teammate-1" },
|
||||
{ name: "teammate-2" },
|
||||
{ name: "teammate-3" },
|
||||
{ name: "teammate-4" },
|
||||
];
|
||||
|
||||
const owner = await users.create(
|
||||
{
|
||||
username: "pro-user",
|
||||
name: "pro-user",
|
||||
organizationId: org.id,
|
||||
roleInOrganization: "OWNER",
|
||||
},
|
||||
{
|
||||
hasTeam: true,
|
||||
teammates: teamMatesObj,
|
||||
}
|
||||
);
|
||||
|
||||
await owner.apiLogin();
|
||||
const membership = await owner.getFirstTeamMembership();
|
||||
const teamId = membership.team.id;
|
||||
|
||||
// Update team to be private
|
||||
await page.goto(`/settings/teams/${teamId}/members`);
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
// As admin/owner we can see the user list
|
||||
const tableLocator = await page.getByTestId("team-member-list-container");
|
||||
await expect(tableLocator).toBeVisible();
|
||||
|
||||
const memberUser = await prisma.membership.findFirst({
|
||||
where: {
|
||||
teamId,
|
||||
role: "MEMBER",
|
||||
},
|
||||
select: {
|
||||
user: {
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(memberUser?.user.email).toBeDefined();
|
||||
// @ts-expect-error expect doesnt assert on a type level
|
||||
const memberOfTeam = await users.set(memberUser?.user.email);
|
||||
await memberOfTeam.apiLogin();
|
||||
|
||||
await page.goto(`/settings/teams/${teamId}/members`);
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
// As a user we can not see the user list when a team is private
|
||||
const hiddenTableLocator = await page.getByTestId("team-member-list-container");
|
||||
await expect(hiddenTableLocator).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -1,15 +1,18 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import path from "path";
|
||||
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
import { test } from "../lib/fixtures";
|
||||
|
||||
test.describe("UploadAvatar", async () => {
|
||||
test("can upload an image", async ({ page, users }) => {
|
||||
const user = await users.create({});
|
||||
test.describe("User Avatar", async () => {
|
||||
test("it can upload a user profile image", async ({ page, users }) => {
|
||||
const user = await users.create({ name: "John Doe" });
|
||||
await user.apiLogin();
|
||||
|
||||
let objectKey: string;
|
||||
|
||||
await test.step("Can upload an initial picture", async () => {
|
||||
await page.goto("/settings/my-account/profile");
|
||||
|
||||
@@ -26,8 +29,6 @@ test.describe("UploadAvatar", async () => {
|
||||
|
||||
await page.getByTestId("upload-avatar").click();
|
||||
|
||||
await page.locator("input[name='name']").fill(user.email);
|
||||
|
||||
await page.getByText("Update").click();
|
||||
await page.waitForSelector("text=Settings updated successfully");
|
||||
|
||||
@@ -41,18 +42,165 @@ test.describe("UploadAvatar", async () => {
|
||||
},
|
||||
});
|
||||
|
||||
// Wait for the avatar image with the http src to appear instead of the data uri
|
||||
const avatar = page.getByTestId("profile-upload-avatar").locator("img[src^=http]");
|
||||
objectKey = response.objectKey;
|
||||
|
||||
const src = await avatar.getAttribute("src");
|
||||
const avatarImage = page.getByTestId("profile-upload-avatar").locator("img");
|
||||
|
||||
await expect(src).toContain(response.objectKey);
|
||||
await expect(avatarImage).toHaveAttribute("src", new RegExp(`^\/api\/avatar\/${objectKey}\.png$`));
|
||||
|
||||
const urlResponse = await page.request.get(`/api/avatar/${response.objectKey}.png`, {
|
||||
const urlResponse = await page.request.get((await avatarImage.getAttribute("src")) || "", {
|
||||
maxRedirects: 0,
|
||||
});
|
||||
|
||||
await expect(urlResponse?.status()).toBe(200);
|
||||
});
|
||||
|
||||
await test.step("View avatar on the public page", async () => {
|
||||
await page.goto(`/${user.username}`);
|
||||
|
||||
await expect(page.locator(`img`)).toHaveAttribute(
|
||||
"src",
|
||||
new RegExp(`\/api\/avatar\/${objectKey}\.png$`)
|
||||
);
|
||||
// verify objectKey is passed to the OG image
|
||||
// yes, OG image URI encodes at multiple places.. don't want to mess with that.
|
||||
await expect(page.locator('meta[property="og:image"]')).toHaveAttribute(
|
||||
"content",
|
||||
new RegExp(
|
||||
encodeURIComponent(`meetingImage=${encodeURIComponent(`${CAL_URL}/api/avatar/${objectKey}.png`)}`)
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Team Logo", async () => {
|
||||
test("it can upload a team logo image", async ({ page, users }) => {
|
||||
const user = await users.create(undefined, { hasTeam: true });
|
||||
|
||||
const { team } = await user.getFirstTeamMembership();
|
||||
|
||||
await user.apiLogin();
|
||||
|
||||
await page.goto(`/settings/teams/${team.id}/profile`);
|
||||
|
||||
await test.step("Can upload an initial picture", async () => {
|
||||
await page.getByTestId("open-upload-avatar-dialog").click();
|
||||
|
||||
const [fileChooser] = await Promise.all([
|
||||
// It is important to call waitForEvent before click to set up waiting.
|
||||
page.waitForEvent("filechooser"),
|
||||
// Opens the file chooser.
|
||||
page.getByTestId("open-upload-image-filechooser").click(),
|
||||
]);
|
||||
|
||||
await fileChooser.setFiles(`${path.dirname(__filename)}/../fixtures/cal.png`);
|
||||
|
||||
await page.getByTestId("upload-avatar").click();
|
||||
|
||||
await page.getByText("Update").click();
|
||||
await page.waitForSelector("text=Your team has been updated successfully.");
|
||||
|
||||
const response = await prisma.avatar.findUniqueOrThrow({
|
||||
where: {
|
||||
teamId_userId_isBanner: {
|
||||
userId: 0,
|
||||
teamId: team.id,
|
||||
isBanner: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const avatarImage = page.getByTestId("profile-upload-logo").locator("img");
|
||||
|
||||
await expect(avatarImage).toHaveAttribute(
|
||||
"src",
|
||||
new RegExp(`^\/api\/avatar\/${response.objectKey}\.png$`)
|
||||
);
|
||||
|
||||
const urlResponse = await page.request.get((await avatarImage.getAttribute("src")) || "", {
|
||||
maxRedirects: 0,
|
||||
});
|
||||
|
||||
await expect(urlResponse?.status()).toBe(200);
|
||||
|
||||
await expect(
|
||||
page.getByTestId("tab-teams").locator(`img[src="/api/avatar/${response.objectKey}.png"]`)
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Organization Logo", async () => {
|
||||
test("it can upload a organization logo image", async ({ page, users, orgs }) => {
|
||||
const owner = await users.create(undefined, { hasTeam: true, isUnpublished: true, isOrg: true });
|
||||
const { team: org } = await owner.getOrgMembership();
|
||||
|
||||
await owner.apiLogin();
|
||||
await page.goto("/settings/organizations/profile");
|
||||
|
||||
let objectKey: string;
|
||||
|
||||
await test.step("Can upload an initial picture", async () => {
|
||||
await page.getByTestId("open-upload-avatar-dialog").click();
|
||||
|
||||
const [fileChooser] = await Promise.all([
|
||||
// It is important to call waitForEvent before click to set up waiting.
|
||||
page.waitForEvent("filechooser"),
|
||||
// Opens the file chooser.
|
||||
page.getByTestId("open-upload-image-filechooser").click(),
|
||||
]);
|
||||
|
||||
await fileChooser.setFiles(`${path.dirname(__filename)}/../fixtures/cal.png`);
|
||||
|
||||
await page.getByTestId("upload-avatar").click();
|
||||
|
||||
await page.getByText("Update").click();
|
||||
await page.waitForSelector("text=Your organization updated successfully");
|
||||
|
||||
const response = await prisma.avatar.findUniqueOrThrow({
|
||||
where: {
|
||||
teamId_userId_isBanner: {
|
||||
userId: 0,
|
||||
teamId: org.id,
|
||||
isBanner: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
objectKey = response.objectKey;
|
||||
|
||||
const avatarImage = page.getByTestId("profile-upload-logo").locator("img");
|
||||
|
||||
await expect(avatarImage).toHaveAttribute(
|
||||
"src",
|
||||
new RegExp(`^\/api\/avatar\/${response.objectKey}\.png$`)
|
||||
);
|
||||
|
||||
const urlResponse = await page.request.get((await avatarImage.getAttribute("src")) || "", {
|
||||
maxRedirects: 0,
|
||||
});
|
||||
|
||||
await expect(urlResponse?.status()).toBe(200);
|
||||
|
||||
// TODO: Implement the org logo updating in the sidebar
|
||||
// this should be done in the orgBrandingContext
|
||||
});
|
||||
|
||||
const requestedSlug = org.metadata?.requestedSlug;
|
||||
|
||||
await test.step("it shows the correct logo on the unpublished public page", async () => {
|
||||
await page.goto(`/org/${requestedSlug}`);
|
||||
|
||||
expect(await page.locator('[data-testid="empty-screen"]').count()).toBe(1);
|
||||
|
||||
await expect(page.locator(`img`)).toHaveAttribute(
|
||||
"src",
|
||||
new RegExp(`^\/api\/avatar\/${objectKey}\.png$`)
|
||||
);
|
||||
});
|
||||
|
||||
// TODO: add test for published team.
|
||||
// unpublished works regardless of orgDomain but when it is published it does work
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -9,7 +9,6 @@ test.describe.configure({ mode: "parallel" });
|
||||
const title = (name: string) => `${name} is unpublished`;
|
||||
const description = (entity: string) =>
|
||||
`This ${entity} link is currently not available. Please contact the ${entity} owner or ask them to publish it.`;
|
||||
const avatar = (slug: string, entity = "team") => `/${entity}/${slug}/avatar.png`;
|
||||
|
||||
test.afterAll(async ({ users }) => {
|
||||
await users.deleteAll();
|
||||
@@ -24,7 +23,7 @@ test.describe("Unpublished", () => {
|
||||
expect(await page.locator('[data-testid="empty-screen"]').count()).toBe(1);
|
||||
expect(await page.locator(`h2:has-text("${title(team.name)}")`).count()).toBe(1);
|
||||
expect(await page.locator(`div:text("${description("team")}")`).count()).toBe(1);
|
||||
await expect(page.locator(`img`)).toHaveAttribute("src", avatar(requestedSlug));
|
||||
await expect(page.locator(`img`)).toHaveAttribute("src", /.*/);
|
||||
});
|
||||
|
||||
test("Regular team event type", async ({ page, users }) => {
|
||||
@@ -40,7 +39,7 @@ test.describe("Unpublished", () => {
|
||||
expect(await page.locator('[data-testid="empty-screen"]').count()).toBe(1);
|
||||
expect(await page.locator(`h2:has-text("${title(team.name)}")`).count()).toBe(1);
|
||||
expect(await page.locator(`div:text("${description("team")}")`).count()).toBe(1);
|
||||
await expect(page.locator(`img`)).toHaveAttribute("src", avatar(requestedSlug));
|
||||
await expect(page.locator(`img`)).toHaveAttribute("src", /.*/);
|
||||
});
|
||||
|
||||
test("Organization profile", async ({ users, page }) => {
|
||||
@@ -52,7 +51,7 @@ test.describe("Unpublished", () => {
|
||||
expect(await page.locator('[data-testid="empty-screen"]').count()).toBe(1);
|
||||
expect(await page.locator(`h2:has-text("${title(org.name)}")`).count()).toBe(1);
|
||||
expect(await page.locator(`div:text("${description("organization")}")`).count()).toBe(1);
|
||||
await expect(page.locator(`img`)).toHaveAttribute("src", avatar(requestedSlug, "org"));
|
||||
await expect(page.locator(`img`)).toHaveAttribute("src", /.*/);
|
||||
});
|
||||
|
||||
test("Organization sub-team", async ({ users, page }) => {
|
||||
@@ -70,7 +69,7 @@ test.describe("Unpublished", () => {
|
||||
expect(await page.locator('[data-testid="empty-screen"]').count()).toBe(1);
|
||||
expect(await page.locator(`h2:has-text("${title(org.name)}")`).count()).toBe(1);
|
||||
expect(await page.locator(`div:text("${description("organization")}")`).count()).toBe(1);
|
||||
await expect(page.locator(`img`)).toHaveAttribute("src", avatar(requestedSlug, "org"));
|
||||
await expect(page.locator(`img`)).toHaveAttribute("src", /.*/);
|
||||
});
|
||||
|
||||
test("Organization sub-team event-type", async ({ users, page }) => {
|
||||
@@ -90,7 +89,7 @@ test.describe("Unpublished", () => {
|
||||
expect(await page.locator('[data-testid="empty-screen"]').count()).toBe(1);
|
||||
expect(await page.locator(`h2:has-text("${title(org.name)}")`).count()).toBe(1);
|
||||
expect(await page.locator(`div:text("${description("organization")}")`).count()).toBe(1);
|
||||
await expect(page.locator(`img`)).toHaveAttribute("src", avatar(requestedSlug, "org"));
|
||||
await expect(page.locator(`img`)).toHaveAttribute("src", /.*/);
|
||||
});
|
||||
|
||||
test("Organization user", async ({ users, page }) => {
|
||||
@@ -102,7 +101,7 @@ test.describe("Unpublished", () => {
|
||||
expect(await page.locator('[data-testid="empty-screen"]').count()).toBe(1);
|
||||
expect(await page.locator(`h2:has-text("${title(org.name)}")`).count()).toBe(1);
|
||||
expect(await page.locator(`div:text("${description("organization")}")`).count()).toBe(1);
|
||||
await expect(page.locator(`img`)).toHaveAttribute("src", avatar(requestedSlug, "org"));
|
||||
await expect(page.locator(`img`)).toHaveAttribute("src", /.*/);
|
||||
});
|
||||
|
||||
test("Organization user event-type", async ({ users, page }) => {
|
||||
@@ -115,6 +114,6 @@ test.describe("Unpublished", () => {
|
||||
expect(await page.locator('[data-testid="empty-screen"]').count()).toBe(1);
|
||||
expect(await page.locator(`h2:has-text("${title(org.name)}")`).count()).toBe(1);
|
||||
expect(await page.locator(`div:text("${description("organization")}")`).count()).toBe(1);
|
||||
await expect(page.locator(`img`)).toHaveAttribute("src", avatar(requestedSlug, "org"));
|
||||
await expect(page.locator(`img`)).toHaveAttribute("src", /.*/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,16 +8,20 @@ import { BookingStatus } from "@calcom/prisma/client";
|
||||
import { test } from "./lib/fixtures";
|
||||
import {
|
||||
bookOptinEvent,
|
||||
bookTimeSlot,
|
||||
createHttpServer,
|
||||
selectFirstAvailableTimeSlotNextMonth,
|
||||
gotoRoutingLink,
|
||||
createUserWithSeatedEventAndAttendees,
|
||||
gotoRoutingLink,
|
||||
selectFirstAvailableTimeSlotNextMonth,
|
||||
} from "./lib/testUtils";
|
||||
|
||||
// remove dynamic properties that differs depending on where you run the tests
|
||||
const dynamic = "[redacted/dynamic]";
|
||||
|
||||
test.afterEach(({ users }) => users.deleteAll());
|
||||
test.afterEach(async ({ users }) => {
|
||||
// This also delete forms on cascade
|
||||
await users.deleteAll();
|
||||
});
|
||||
|
||||
async function createWebhookReceiver(page: Page) {
|
||||
const webhookReceiver = createHttpServer();
|
||||
@@ -71,11 +75,7 @@ test.describe("BOOKING_CREATED", async () => {
|
||||
// --- Book the first available day next month in the pro user's "30min"-event
|
||||
await page.goto(`/${user.username}/${eventType.slug}`);
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
|
||||
// --- fill form
|
||||
await page.fill('[name="name"]', "Test Testson");
|
||||
await page.fill('[name="email"]', "test@example.com");
|
||||
await page.press('[name="email"]', "Enter");
|
||||
await bookTimeSlot(page);
|
||||
|
||||
await webhookReceiver.waitForRequestCount(1);
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#f4f4f4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-sparkles"><path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/><path d="M5 3v4"/><path d="M19 17v4"/><path d="M3 5h4"/><path d="M17 19h4"/></svg>
|
||||
|
After Width: | Height: | Size: 467 B |
@@ -19,6 +19,7 @@
|
||||
"verify_email_banner_body": "Verify your email address to guarantee the best email and calendar deliverability",
|
||||
"verify_email_email_header": "Verify your email address",
|
||||
"verify_email_email_button": "Verify email",
|
||||
"cal_ai_assistant":"Cal AI Assistant",
|
||||
"verify_email_change_description": "You have recently requested to change the email address you use to log into your {{appName}} account. Please click the button below to confirm your new email address.",
|
||||
"verify_email_change_success_toast": "Updated your email to {{email}}",
|
||||
"verify_email_change_failure_toast": "Failed to update email.",
|
||||
@@ -110,7 +111,9 @@
|
||||
"event_still_awaiting_approval": "An event is still waiting for your approval",
|
||||
"booking_submitted_subject": "Booking Submitted: {{title}} at {{date}}",
|
||||
"download_recording_subject": "Download Recording: {{title}} at {{date}}",
|
||||
"download_transcript_email_subject":"Download Transcript: {{title}} at {{date}}",
|
||||
"download_your_recording": "Download your recording",
|
||||
"download_your_transcripts":"Download your Transcripts",
|
||||
"your_meeting_has_been_booked": "Your meeting has been booked",
|
||||
"event_type_has_been_rescheduled_on_time_date": "Your {{title}} has been rescheduled to {{date}}.",
|
||||
"event_has_been_rescheduled": "Updated - Your event has been rescheduled",
|
||||
@@ -326,8 +329,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.",
|
||||
@@ -1427,7 +1432,11 @@
|
||||
"download_responses_description": "Download all responses to your form in CSV format.",
|
||||
"download": "Download",
|
||||
"download_recording": "Download Recording",
|
||||
"transcription_enabled": "Transcriptions are enabled now",
|
||||
"transcription_stopped": "Transcriptions are stopped now",
|
||||
"download_transcript": "Download Transcript",
|
||||
"recording_from_your_recent_call": "A recording from your recent call on {{appName}} is ready for download",
|
||||
"transcript_from_previous_call": "Transcript from your recent call on {{appName}} is ready to download. Links are valid only for 1 Hour",
|
||||
"link_valid_for_12_hrs":"Note: The download link is valid only for 12 hours. You can generate new download link by following instructions <1>here</1>.",
|
||||
"create_your_first_form": "Create your first form",
|
||||
"create_your_first_form_description": "With Routing Forms you can ask qualifying questions and route to the correct person or event type.",
|
||||
@@ -2389,9 +2398,9 @@
|
||||
"ooo_user_is_ooo": "{{displayName}} is OOO",
|
||||
"ooo_slots_returning": "<0>{{displayName}}</0> can take their meetings while they are away.",
|
||||
"ooo_slots_book_with": "Book {{displayName}}",
|
||||
"ooo_create_entry_modal": "Create an Out of Office",
|
||||
"ooo_create_entry_modal": "Go Out of Office",
|
||||
"ooo_select_reason": "Select reason",
|
||||
"create_an_out_of_office": "Create an Out of Office",
|
||||
"create_an_out_of_office": "Go Out of Office",
|
||||
"submit_feedback": "Submit Feedback",
|
||||
"host_no_show": "Your host didn't show up",
|
||||
"no_show_description": "You can reschedule another meeting with them",
|
||||
@@ -2404,5 +2413,6 @@
|
||||
"no_show_url_info":"The URL for No Show Feedback",
|
||||
"no_support_needed":"No Support Needed?",
|
||||
"hide_support":"Hide Support",
|
||||
"lockedSMS": "Locked SMS",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2238,15 +2238,15 @@
|
||||
"not_enough_available_seats_error": "La réservation n'a pas assez de places disponibles",
|
||||
"user_redirect_title": "{{username}} est actuellement en absence de courte durée.",
|
||||
"user_redirect_description": "Pendant ce temps, {{profile.username}} sera responsable de tous les rendez-vous programmés de la part de {{username}}.",
|
||||
"out_of_office": "Pas au bureau",
|
||||
"out_of_office": "Absent du bureau",
|
||||
"out_of_office_description": "Configurez des actions dans votre profil pendant votre absence.",
|
||||
"send_request": "Envoyer une demande",
|
||||
"start_date_and_end_date_required": "Les dates de début et de fin sont requises",
|
||||
"start_date_must_be_before_end_date": "La date de début doit être antérieure à la date de fin",
|
||||
"start_date_must_be_in_the_future": "La date de début doit être dans le futur",
|
||||
"user_not_found": "Utilisateur introuvable",
|
||||
"out_of_office_entry_already_exists": "L'entrée Pas au bureau existe déjà",
|
||||
"out_of_office_id_required": "L'id de l'entrée Pas au bureau est requis",
|
||||
"out_of_office_entry_already_exists": "L'entrée absent du bureau existe déjà",
|
||||
"out_of_office_id_required": "L'id de l'entrée absent du bureau est requis",
|
||||
"booking_redirect_infinite_not_allowed": "Une redirection de réservation de cet utilisateur vers vous existe déjà.",
|
||||
"success_entry_created": "Création d'une nouvelle entrée réussie",
|
||||
"booking_redirect_email_subject": "Notification de redirection de réservation",
|
||||
@@ -2257,20 +2257,20 @@
|
||||
"copy_link_booking_redirect_request": "Copier le lien pour partager la demande",
|
||||
"booking_redirect_request_title": "Demande de redirection de réservation",
|
||||
"select_team_member": "Sélectionner un membre de l'équipe",
|
||||
"going_away_title": "Vous partez ? Marquez simplement le lien de votre profil comme étant indisponible pendant un certain temps.",
|
||||
"going_away_title": "Vous partez? Marquez simplement le lien de votre profil comme étant indisponible pendant un certain temps.",
|
||||
"redirect_team_enabled": "Redirigez votre profil vers un autre membre de l'équipe",
|
||||
"redirect_team_disabled": "Redirigez votre profil vers un autre membre de l'équipe (formule Équipe requise)",
|
||||
"out_of_office_unavailable_list": "Liste des indisponibilités Pas au bureau",
|
||||
"out_of_office_unavailable_list": "Liste des indisponibilités absent du bureau",
|
||||
"success_deleted_entry_out_of_office": "Suppression de l'entrée réussie",
|
||||
"temporarily_out_of_office": "Temporairement Pas au bureau ?",
|
||||
"temporarily_out_of_office": "Temporairement absent du bureau?",
|
||||
"add_a_redirect": "Ajouter une redirection",
|
||||
"create_entry": "Créer une entrée",
|
||||
"time_range": "Plage temporelle",
|
||||
"automatically_add_all_team_members": "Ajouter tous les membres de l'équipe, y compris les futurs membres",
|
||||
"redirect_to": "Redirection vers",
|
||||
"having_trouble_finding_time": "Vous avez du mal à trouver une heure ?",
|
||||
"having_trouble_finding_time": "Vous avez du mal à trouver un créneau?",
|
||||
"show_more": "En voir plus",
|
||||
"assignment_description": "Planifiez des rendez-vous lorsque tout le monde est disponible ou faites tourner les membres de votre équipe",
|
||||
"assignment_description": "Planifiez des rendez-vous lorsque tout le monde est disponible ou faites une rotation entre les membres de votre équipe",
|
||||
"lowest": "le plus bas",
|
||||
"low": "bas",
|
||||
"medium": "moyen",
|
||||
@@ -2297,4 +2297,4 @@
|
||||
"confirm_email_description": "Nous avons envoyé un e-mail à <strong>{{email}}</strong>. Cliquez sur le lien dans l'e-mail pour vérifier cette adresse.",
|
||||
"send_event_details_to": "Envoyer les détails de l'événement à",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Ajoutez vos nouvelles chaînes ci-dessus ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -630,8 +630,7 @@ describe("getSchedule", () => {
|
||||
...TestData.users.example,
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
credentials: [],
|
||||
},
|
||||
],
|
||||
bookings: [
|
||||
@@ -643,7 +642,7 @@ describe("getSchedule", () => {
|
||||
status: "ACCEPTED" as BookingStatus,
|
||||
},
|
||||
],
|
||||
apps: [TestData.apps.googleCalendar],
|
||||
// apps: [TestData.apps.googleCalendar],
|
||||
};
|
||||
|
||||
await createBookingScenario(scenarioData);
|
||||
|
||||
@@ -31,7 +31,6 @@ export const getSampleUserInSession = function () {
|
||||
identityProvider: IdentityProvider.CAL,
|
||||
brandColor: "#292929",
|
||||
darkBrandColor: "#fafafa",
|
||||
away: false,
|
||||
metadata: null,
|
||||
role: UserPermissionRole.USER,
|
||||
disableImpersonation: false,
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
{
|
||||
"path": "/api/cron/calendar-cache-cleanup",
|
||||
"schedule": "0 5 * * *"
|
||||
},
|
||||
{
|
||||
"path": "/api/tasks/cron",
|
||||
"schedule": "* * * * *"
|
||||
},
|
||||
{
|
||||
"path": "/api/tasks/cleanup",
|
||||
"schedule": "0 0 * * *"
|
||||
}
|
||||
],
|
||||
"functions": {
|
||||
|
||||
@@ -108,6 +108,8 @@
|
||||
"vitest-mock-extended": "^1.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@daily-co/daily-js": "^0.59.0",
|
||||
"city-timezones": "^1.2.1",
|
||||
"eslint": "^8.34.0",
|
||||
"turbo": "^1.10.1"
|
||||
},
|
||||
|
||||
@@ -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,6 +6,7 @@ import alby_config_json from "./alby/config.json";
|
||||
import amie_config_json from "./amie/config.json";
|
||||
import { metadata as applecalendar__metadata_ts } from "./applecalendar/_metadata";
|
||||
import around_config_json from "./around/config.json";
|
||||
import autocheckin_config_json from "./autocheckin/config.json";
|
||||
import basecamp3_config_json from "./basecamp3/config.json";
|
||||
import cal_ai_config_json from "./cal-ai/config.json";
|
||||
import { metadata as caldavcalendar__metadata_ts } from "./caldavcalendar/_metadata";
|
||||
@@ -91,6 +92,7 @@ export const appStoreMetadata = {
|
||||
amie: amie_config_json,
|
||||
applecalendar: applecalendar__metadata_ts,
|
||||
around: around_config_json,
|
||||
autocheckin: autocheckin_config_json,
|
||||
basecamp3: basecamp3_config_json,
|
||||
"cal-ai": cal_ai_config_json,
|
||||
caldavcalendar: caldavcalendar__metadata_ts,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user