Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7e05aa016 | ||
|
|
f8cbf8b1c4 | ||
|
|
ccc294e567 | ||
|
|
0be344b1ba | ||
|
|
7a525e476a | ||
|
|
fe62f9c6de | ||
|
|
9608f6518f | ||
|
|
2f8fa20ec6 | ||
|
|
6f097b0a74 | ||
|
|
159bbb53b6 | ||
|
|
622160ea17 | ||
|
|
5d543c864f | ||
|
|
ed745623eb | ||
|
|
29bca76e91 | ||
|
|
33d0fee43e | ||
|
|
bb13de4176 | ||
|
|
c854dd092c | ||
|
|
26d64bd93f | ||
|
|
1025238eee | ||
|
|
eec76ecfe2 | ||
|
|
f5876ae6d6 | ||
|
|
ccc8cdc1ff | ||
|
|
a1840c9362 | ||
|
|
22212e7edb | ||
|
|
6ffe7005dd | ||
|
|
4890d72e30 | ||
|
|
a531541273 | ||
|
|
2bced59b47 | ||
|
|
e9554604e7 | ||
|
|
48c51da853 | ||
|
|
cc2164657c | ||
|
|
a35683667b | ||
|
|
cf403bebf1 | ||
|
|
f3e4bbc753 | ||
|
|
4bfbac6649 | ||
|
|
3d7fd7eb01 | ||
|
|
ec42900e25 | ||
|
|
032b243248 | ||
|
|
b70b5a5449 | ||
|
|
cc5dfb06bd |
+6
-1
@@ -345,4 +345,9 @@ SENTRY_DISABLE_SERVER_WEBPACK_PLUGIN=1
|
||||
NEXT_PUBLIC_API_V2_URL="http://localhost:5555/api/v2"
|
||||
|
||||
# Ratelimiting via unkey
|
||||
UNKEY_ROOT_KEY=
|
||||
UNKEY_ROOT_KEY=
|
||||
|
||||
|
||||
# Used for Cal.ai Enterprise Voice AI Agents
|
||||
# https://retellai.com
|
||||
RETELL_AI_KEY=
|
||||
|
||||
@@ -3,7 +3,7 @@ description: "Cache or restore if necessary"
|
||||
inputs:
|
||||
DATABASE_URL:
|
||||
required: false
|
||||
default: "postgresql://postgres:@localhost:5432/calendso"
|
||||
default: "postgresql://postgres:postgres@localhost:5432/calendso"
|
||||
path:
|
||||
required: false
|
||||
default: "backups/backup.sql"
|
||||
|
||||
@@ -10,13 +10,19 @@ jobs:
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:13.13
|
||||
image: postgres:13
|
||||
credentials:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: calendso
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
mailhog:
|
||||
|
||||
@@ -10,13 +10,19 @@ jobs:
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:13.13
|
||||
image: postgres:13
|
||||
credentials:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: calendso
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
strategy:
|
||||
|
||||
@@ -10,13 +10,19 @@ jobs:
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:13.13
|
||||
image: postgres:13
|
||||
credentials:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: calendso
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
mailhog:
|
||||
|
||||
@@ -10,13 +10,19 @@ jobs:
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:13.13
|
||||
image: postgres:13
|
||||
credentials:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: calendso
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
mailhog:
|
||||
|
||||
@@ -30,6 +30,8 @@ env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
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 }}
|
||||
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
@@ -34,6 +34,8 @@ env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
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 }}
|
||||
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -42,13 +44,19 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:13.13
|
||||
image: postgres:13
|
||||
credentials:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: calendso
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
steps:
|
||||
|
||||
@@ -56,7 +56,7 @@ test.describe("Org", () => {
|
||||
];
|
||||
const response = await page.goto(`http://i.cal.com/${users[0].username}+${users[1].username}`);
|
||||
expect(response?.status()).toBe(200);
|
||||
expect(await page.locator('[data-testid="event-title"]').textContent()).toBe("Dynamic");
|
||||
expect(await page.locator('[data-testid="event-title"]').textContent()).toBe("Group Meeting");
|
||||
|
||||
expect(await page.locator('[data-testid="event-meta"]').textContent()).toContain(users[0].name);
|
||||
expect(await page.locator('[data-testid="event-meta"]').textContent()).toContain(users[1].name);
|
||||
|
||||
@@ -4,3 +4,4 @@ NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000
|
||||
|
||||
# Get it in console.cal.com
|
||||
CALCOM_LICENSE_KEY=""
|
||||
NEXT_PUBLIC_API_V2_ROOT_URL=http://localhost:5555
|
||||
@@ -47,6 +47,22 @@ const nextConfig = {
|
||||
source: "/v:version/:rest*",
|
||||
destination: "/api/v:version/:rest*",
|
||||
},
|
||||
{
|
||||
source: "/api/v2",
|
||||
destination: `${process.env.NEXT_PUBLIC_API_V2_ROOT_URL}/health`,
|
||||
},
|
||||
{
|
||||
source: "/api/v2/health",
|
||||
destination: `${process.env.NEXT_PUBLIC_API_V2_ROOT_URL}/health`,
|
||||
},
|
||||
{
|
||||
source: "/api/v2/docs/:path*",
|
||||
destination: `${process.env.NEXT_PUBLIC_API_V2_ROOT_URL}/docs/:path*`,
|
||||
},
|
||||
{
|
||||
source: "/api/v2/:path*",
|
||||
destination: `${process.env.NEXT_PUBLIC_API_V2_ROOT_URL}/api/v2/:path*`,
|
||||
},
|
||||
// This redirects requests to api/v*/ to /api/ passing version as a query parameter.
|
||||
{
|
||||
source: "/api/v:version/:rest*",
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { SchedulesRepository } from "@/ee/schedules/schedules.repository";
|
||||
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
|
||||
import { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { withAccessTokenAuth } from "test/utils/withAccessTokenAuth";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
import {
|
||||
getAllUserBookings,
|
||||
handleNewBooking,
|
||||
getBookingInfo,
|
||||
handleNewRecurringBooking,
|
||||
handleInstantMeeting,
|
||||
} from "@calcom/platform-libraries";
|
||||
import { ApiSuccessResponse, ApiResponse } from "@calcom/platform-types";
|
||||
|
||||
describe("Bookings Endpoints", () => {
|
||||
describe("User Authenticated", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||
|
||||
const userEmail = "bookings-controller-e2e@api.com";
|
||||
let user: User;
|
||||
|
||||
let createdBooking: Awaited<ReturnType<typeof handleNewBooking>>;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withAccessTokenAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, AvailabilitiesModule, UsersModule],
|
||||
providers: [SchedulesRepository, SchedulesService],
|
||||
})
|
||||
).compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
});
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it("should be defined", () => {
|
||||
expect(userRepositoryFixture).toBeDefined();
|
||||
expect(user).toBeDefined();
|
||||
});
|
||||
|
||||
it("should create a booking", async () => {
|
||||
const bookingStart = "2023-05-25T09:30:00.000Z";
|
||||
const bookingEnd = "2023-05-25T10:30:00.000Z";
|
||||
const bookingEventTypeId = 7;
|
||||
const bookingTimeZone = "Europe/Londom";
|
||||
const bookingLanguage = "en";
|
||||
const bookingHashedLink = "";
|
||||
const bookingMetadata = {};
|
||||
|
||||
const body = {
|
||||
start: bookingStart,
|
||||
end: bookingEnd,
|
||||
eventTypeId: bookingEventTypeId,
|
||||
timeZone: bookingTimeZone,
|
||||
language: bookingLanguage,
|
||||
metadata: bookingMetadata,
|
||||
hashedLink: bookingHashedLink,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/api/v2/ee/bookings")
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<Awaited<ReturnType<typeof handleNewBooking>>> =
|
||||
response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.user.email).toBeDefined();
|
||||
expect(responseBody.data.user.email).toEqual(userEmail);
|
||||
expect(responseBody.data.id).toBeDefined();
|
||||
expect(responseBody.data.uid).toBeDefined();
|
||||
expect(responseBody.data.startTime).toEqual(bookingStart);
|
||||
expect(responseBody.data.eventTypeId).toEqual(bookingEventTypeId);
|
||||
expect(responseBody.data.user.timeZone).toEqual(bookingTimeZone);
|
||||
expect(responseBody.data.metadata).toEqual(bookingMetadata);
|
||||
|
||||
createdBooking = responseBody.data;
|
||||
});
|
||||
});
|
||||
|
||||
it("should get bookings", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get("/api/v2/ee/bookings")
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<Awaited<ReturnType<typeof getAllUserBookings>>> =
|
||||
response.body;
|
||||
const fetchedBooking = responseBody.data.bookings[0];
|
||||
|
||||
expect(responseBody.data.bookings.length).toEqual(1);
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(fetchedBooking).toBeDefined();
|
||||
|
||||
expect(fetchedBooking.id).toEqual(createdBooking.id);
|
||||
expect(fetchedBooking.uid).toEqual(createdBooking.uid);
|
||||
expect(fetchedBooking.startTime).toEqual(createdBooking.startTime);
|
||||
expect(fetchedBooking.endTime).toEqual(createdBooking.endTime);
|
||||
expect(fetchedBooking.user?.email).toEqual(createdBooking.user.email);
|
||||
});
|
||||
});
|
||||
|
||||
it("should get booking", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/api/v2/ee/bookings/${createdBooking.uid}`)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<Awaited<ReturnType<typeof getBookingInfo>>> = response.body;
|
||||
const { bookingInfo } = responseBody.data;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(bookingInfo?.id).toBeDefined();
|
||||
expect(bookingInfo?.uid).toBeDefined();
|
||||
expect(bookingInfo?.id).toEqual(createdBooking.id);
|
||||
expect(bookingInfo?.uid).toEqual(createdBooking.uid);
|
||||
expect(bookingInfo?.eventTypeId).toEqual(createdBooking.eventTypeId);
|
||||
expect(bookingInfo?.startTime).toEqual(createdBooking.startTime);
|
||||
});
|
||||
});
|
||||
|
||||
it("should create a recurring booking", async () => {
|
||||
const bookingStart = "2023-05-25T09:30:00.000Z";
|
||||
const bookingEnd = "2023-05-25T10:30:00.000Z";
|
||||
const bookingEventTypeId = 7;
|
||||
const bookingTimeZone = "Europe/Londom";
|
||||
const bookingLanguage = "en";
|
||||
const bookingHashedLink = "";
|
||||
const bookingRecurringCount = 5;
|
||||
const currentBookingRecurringIndex = 0;
|
||||
|
||||
const body = {
|
||||
start: bookingStart,
|
||||
end: bookingEnd,
|
||||
eventTypeId: bookingEventTypeId,
|
||||
timeZone: bookingTimeZone,
|
||||
language: bookingLanguage,
|
||||
metadata: {},
|
||||
hashedLink: bookingHashedLink,
|
||||
recurringCount: bookingRecurringCount,
|
||||
currentRecurringIndex: currentBookingRecurringIndex,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/api/v2/ee/bookings/reccuring")
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then((response) => {
|
||||
const responseBody: ApiResponse<Awaited<ReturnType<typeof handleNewRecurringBooking>>> =
|
||||
response.body;
|
||||
|
||||
expect(responseBody.status).toEqual("recurring");
|
||||
});
|
||||
});
|
||||
|
||||
it("should create an instant booking", async () => {
|
||||
const bookingStart = "2023-05-25T09:30:00.000Z";
|
||||
const bookingEnd = "2023-05-25T10:30:00.000Z";
|
||||
const bookingEventTypeId = 7;
|
||||
const bookingTimeZone = "Europe/Londom";
|
||||
const bookingLanguage = "en";
|
||||
const bookingHashedLink = "";
|
||||
|
||||
const body = {
|
||||
start: bookingStart,
|
||||
end: bookingEnd,
|
||||
eventTypeId: bookingEventTypeId,
|
||||
timeZone: bookingTimeZone,
|
||||
language: bookingLanguage,
|
||||
metadata: {},
|
||||
hashedLink: bookingHashedLink,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/api/v2/ee/bookings/instant")
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then((response) => {
|
||||
const responseBody: ApiResponse<Awaited<ReturnType<typeof handleInstantMeeting>>> = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual("instant");
|
||||
});
|
||||
});
|
||||
|
||||
it("should cancel a booking", async () => {
|
||||
const bookingId = createdBooking.id;
|
||||
|
||||
const body = {
|
||||
allRemainingBookings: false,
|
||||
cancellationReason: "Was fighting some unforseen rescheduling demons",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/api/v2/ee/bookings/${bookingId}/cancel`)
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then((response) => {
|
||||
const responseBody: ApiResponse<{ status: typeof SUCCESS_STATUS | typeof ERROR_STATUS }> =
|
||||
response.body;
|
||||
|
||||
expect(bookingId).toBeDefined();
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,11 @@
|
||||
import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input";
|
||||
import { CreateReccuringBookingInput } from "@/ee/bookings/inputs/create-reccuring-booking.input";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
@@ -11,12 +14,23 @@ import {
|
||||
InternalServerErrorException,
|
||||
Body,
|
||||
HttpException,
|
||||
Param,
|
||||
Get,
|
||||
Query,
|
||||
NotFoundException,
|
||||
UseGuards,
|
||||
} from "@nestjs/common";
|
||||
import { User } from "@prisma/client";
|
||||
import { Request } from "express";
|
||||
import { NextApiRequest } from "next/types";
|
||||
|
||||
import { BOOKING_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import {
|
||||
getAllUserBookings,
|
||||
getBookingInfo,
|
||||
handleCancelBooking,
|
||||
getBookingForReschedule,
|
||||
} from "@calcom/platform-libraries";
|
||||
import {
|
||||
handleNewBooking,
|
||||
BookingResponse,
|
||||
@@ -24,7 +38,9 @@ import {
|
||||
handleNewRecurringBooking,
|
||||
handleInstantMeeting,
|
||||
} from "@calcom/platform-libraries";
|
||||
import { GetBookingsInput, CancelBookingInput } from "@calcom/platform-types";
|
||||
import { ApiResponse } from "@calcom/platform-types";
|
||||
import { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
@Controller({
|
||||
path: "ee/bookings",
|
||||
@@ -34,7 +50,65 @@ import { ApiResponse } from "@calcom/platform-types";
|
||||
export class BookingsController {
|
||||
private readonly logger = new Logger("ee bookings controller");
|
||||
|
||||
constructor(private readonly oAuthFlowService: OAuthFlowService) {}
|
||||
constructor(
|
||||
private readonly oAuthFlowService: OAuthFlowService,
|
||||
private readonly prismaReadService: PrismaReadService
|
||||
) {}
|
||||
|
||||
// note(Rajiv): currently this endpoint is atoms only
|
||||
@Get("/")
|
||||
@UseGuards(AccessTokenGuard)
|
||||
async getBookings(
|
||||
@GetUser() user: User,
|
||||
@Query() queryParams: GetBookingsInput
|
||||
): Promise<ApiResponse<unknown>> {
|
||||
const { filters, cursor, limit } = queryParams;
|
||||
const bookings = await getAllUserBookings({
|
||||
bookingListingByStatus: filters.status,
|
||||
skip: cursor ?? 0,
|
||||
take: limit ?? 10,
|
||||
filters,
|
||||
ctx: {
|
||||
user: { email: user.email, id: user.id },
|
||||
prisma: this.prismaReadService.prisma as unknown as PrismaClient,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: bookings,
|
||||
};
|
||||
}
|
||||
|
||||
// note(Rajiv): currently this endpoint is atoms only
|
||||
@Get("/:bookingUid")
|
||||
async getBooking(@Param("bookingUid") bookingUid: string): Promise<ApiResponse<unknown>> {
|
||||
const { bookingInfo } = await getBookingInfo(bookingUid);
|
||||
|
||||
if (!bookingInfo) {
|
||||
throw new NotFoundException(`Booking with UID=${bookingUid} does not exist.`);
|
||||
}
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: bookingInfo,
|
||||
};
|
||||
}
|
||||
|
||||
// note(Rajiv): currently this endpoint is atoms only
|
||||
@Get("/:bookingUid/reschedule")
|
||||
async getBookingForReschedule(@Param("bookingUid") bookingUid: string): Promise<ApiResponse<unknown>> {
|
||||
const booking = await getBookingForReschedule(bookingUid);
|
||||
|
||||
if (!booking) {
|
||||
throw new NotFoundException(`Booking with UID=${bookingUid} does not exist.`);
|
||||
}
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: booking,
|
||||
};
|
||||
}
|
||||
|
||||
@Post("/")
|
||||
@Permissions([BOOKING_WRITE])
|
||||
@@ -56,6 +130,30 @@ export class BookingsController {
|
||||
throw new InternalServerErrorException("Could not create booking.");
|
||||
}
|
||||
|
||||
@Post("/:bookingId/cancel")
|
||||
@Permissions([BOOKING_WRITE])
|
||||
async cancelBooking(
|
||||
@Req() req: Request & { userId?: number },
|
||||
@Param("bookingId") bookingId: string,
|
||||
@Body() body: CancelBookingInput
|
||||
): Promise<ApiResponse> {
|
||||
if (bookingId) {
|
||||
req.userId = await this.getOwnerId(req);
|
||||
req.body = { ...body, id: parseInt(bookingId) };
|
||||
try {
|
||||
await handleCancelBooking(req as unknown as NextApiRequest & { userId?: number });
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
};
|
||||
} catch (err) {
|
||||
handleBookingErrors(err);
|
||||
}
|
||||
} else {
|
||||
throw new NotFoundException("Booking ID is required.");
|
||||
}
|
||||
throw new InternalServerErrorException("Could not cancel booking.");
|
||||
}
|
||||
|
||||
@Post("/reccuring")
|
||||
@Permissions([BOOKING_WRITE])
|
||||
async createReccuringBooking(
|
||||
|
||||
@@ -82,16 +82,16 @@ describe("Platform Gcal Endpoints", () => {
|
||||
expect(user).toBeDefined();
|
||||
});
|
||||
|
||||
it(`/GET/platform/gcal/oauth/auth-url: it should respond 401 with invalid access token`, async () => {
|
||||
it(`/GET/ee/gcal/oauth/auth-url: it should respond 401 with invalid access token`, async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(`/api/v2/platform/gcal/oauth/auth-url`)
|
||||
.get(`/api/v2/ee/gcal/oauth/auth-url`)
|
||||
.set("Authorization", `Bearer invalid_access_token`)
|
||||
.expect(401);
|
||||
});
|
||||
|
||||
it(`/GET/platform/gcal/oauth/auth-url: it should auth-url to google oauth with valid access token `, async () => {
|
||||
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/platform/gcal/oauth/auth-url`)
|
||||
.get(`/api/v2/ee/gcal/oauth/auth-url`)
|
||||
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||
.set("Origin", CLIENT_REDIRECT_URI)
|
||||
.expect(200);
|
||||
@@ -99,50 +99,50 @@ describe("Platform Gcal Endpoints", () => {
|
||||
expect(data.authUrl).toBeDefined();
|
||||
});
|
||||
|
||||
it(`/GET/platform/gcal/oauth/save: without oauth code`, async () => {
|
||||
it(`/GET/ee/gcal/oauth/save: without oauth code`, async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(
|
||||
`/api/v2/platform/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`
|
||||
`/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`
|
||||
)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it(`/GET/platform/gcal/oauth/save: without access token`, async () => {
|
||||
it(`/GET/ee/gcal/oauth/save: without access token`, async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(
|
||||
`/api/v2/platform/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`
|
||||
`/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`
|
||||
)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it(`/GET/platform/gcal/oauth/save: without origin`, async () => {
|
||||
it(`/GET/ee/gcal/oauth/save: without origin`, async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(
|
||||
`/api/v2/platform/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`
|
||||
`/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`
|
||||
)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it(`/GET/platform/gcal/check with access token but without origin`, async () => {
|
||||
it(`/GET/ee/gcal/check with access token but without origin`, async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(`/api/v2/platform/gcal/check`)
|
||||
.get(`/api/v2/ee/gcal/check`)
|
||||
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it(`/GET/platform/gcal/check without access token`, async () => {
|
||||
await request(app.getHttpServer()).get(`/api/v2/platform/gcal/check`).expect(401);
|
||||
it(`/GET/ee/gcal/check without access token`, async () => {
|
||||
await request(app.getHttpServer()).get(`/api/v2/ee/gcal/check`).expect(401);
|
||||
});
|
||||
|
||||
it(`/GET/platform/gcal/check with access token and origin but no credentials`, async () => {
|
||||
it(`/GET/ee/gcal/check with access token and origin but no credentials`, async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(`/api/v2/platform/gcal/check`)
|
||||
.get(`/api/v2/ee/gcal/check`)
|
||||
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||
.set("Origin", CLIENT_REDIRECT_URI)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it(`/GET/platform/gcal/check with access token and origin and gcal credentials`, async () => {
|
||||
it(`/GET/ee/gcal/check with access token and origin and gcal credentials`, async () => {
|
||||
gcalCredentials = await credentialsRepositoryFixture.create(
|
||||
"google_calendar",
|
||||
{},
|
||||
@@ -150,7 +150,7 @@ describe("Platform Gcal Endpoints", () => {
|
||||
"google-calendar"
|
||||
);
|
||||
await request(app.getHttpServer())
|
||||
.get(`/api/v2/platform/gcal/check`)
|
||||
.get(`/api/v2/ee/gcal/check`)
|
||||
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||
.set("Origin", CLIENT_REDIRECT_URI)
|
||||
.expect(200);
|
||||
|
||||
@@ -40,7 +40,7 @@ const CALENDAR_SCOPES = [
|
||||
];
|
||||
|
||||
@Controller({
|
||||
path: "platform/gcal",
|
||||
path: "ee/gcal",
|
||||
version: "2",
|
||||
})
|
||||
export class GcalController {
|
||||
@@ -55,7 +55,7 @@ export class GcalController {
|
||||
private readonly gcalService: GcalService
|
||||
) {}
|
||||
|
||||
private redirectUri = `${this.config.get("api.url")}/platform/gcal/oauth/save`;
|
||||
private redirectUri = `${this.config.get("api.url")}/ee/gcal/oauth/save`;
|
||||
|
||||
@Get("/oauth/auth-url")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
|
||||
@@ -65,7 +65,7 @@ describe("Me Endpoints", () => {
|
||||
|
||||
it("should get user associated with access token", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get("/api/v2/me")
|
||||
.get("/api/v2/ee/me")
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<UserResponse> = response.body;
|
||||
@@ -84,7 +84,7 @@ describe("Me Endpoints", () => {
|
||||
const body: UpdateManagedPlatformUserInput = { timeZone: "Europe/Rome" };
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch("/api/v2/me")
|
||||
.patch("/api/v2/ee/me")
|
||||
.send(body)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
@@ -108,19 +108,22 @@ describe("Me Endpoints", () => {
|
||||
it("should not update user associated with access token given invalid timezone", async () => {
|
||||
const bodyWithIncorrectTimeZone: UpdateManagedPlatformUserInput = { timeZone: "Narnia/Woods" };
|
||||
|
||||
return request(app.getHttpServer()).patch("/api/v2/me").send(bodyWithIncorrectTimeZone).expect(400);
|
||||
return request(app.getHttpServer()).patch("/api/v2/ee/me").send(bodyWithIncorrectTimeZone).expect(400);
|
||||
});
|
||||
|
||||
it("should not update user associated with access token given invalid time format", async () => {
|
||||
const bodyWithIncorrectTimeFormat: UpdateManagedPlatformUserInput = { timeFormat: 100 };
|
||||
|
||||
return request(app.getHttpServer()).patch("/api/v2/me").send(bodyWithIncorrectTimeFormat).expect(400);
|
||||
return request(app.getHttpServer())
|
||||
.patch("/api/v2/ee/me")
|
||||
.send(bodyWithIncorrectTimeFormat)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should not update user associated with access token given invalid week start", async () => {
|
||||
const bodyWithIncorrectWeekStart: UpdateManagedPlatformUserInput = { weekStart: "waba luba dub dub" };
|
||||
|
||||
return request(app.getHttpServer()).patch("/api/v2/me").send(bodyWithIncorrectWeekStart).expect(400);
|
||||
return request(app.getHttpServer()).patch("/api/v2/ee/me").send(bodyWithIncorrectWeekStart).expect(400);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
|
||||
@@ -12,7 +12,7 @@ import { UserResponse, userSchemaResponse } from "@calcom/platform-types";
|
||||
import { ApiResponse } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "me",
|
||||
path: "ee/me",
|
||||
version: "2",
|
||||
})
|
||||
@UseGuards(AccessTokenGuard, PermissionsGuard)
|
||||
|
||||
@@ -20,7 +20,7 @@ import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { ApiResponse } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "platform/provider",
|
||||
path: "ee/provider",
|
||||
version: "2",
|
||||
})
|
||||
export class CalProviderController {
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
|
||||
import { SCHEDULE_READ, SCHEDULE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { ScheduleWithAvailabilitiesForWeb } from "@calcom/platform-libraries";
|
||||
import type { CityTimezones } from "@calcom/platform-libraries";
|
||||
import { updateScheduleHandler } from "@calcom/platform-libraries";
|
||||
import type { UpdateScheduleOutputType } from "@calcom/platform-libraries";
|
||||
import { ApiSuccessResponse, ScheduleResponse, UpdateScheduleInput } from "@calcom/platform-types";
|
||||
@@ -73,16 +72,6 @@ export class SchedulesController {
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/time-zones")
|
||||
async getTimeZones(): Promise<ApiResponse<CityTimezones>> {
|
||||
const timeZones = await this.schedulesService.getSchedulePossibleTimeZones();
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: timeZones,
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/:scheduleId")
|
||||
@Permissions([SCHEDULE_READ])
|
||||
async getSchedule(
|
||||
|
||||
@@ -6,8 +6,6 @@ import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { Schedule } from "@prisma/client";
|
||||
|
||||
import { cityTimezonesHandler } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class SchedulesService {
|
||||
constructor(
|
||||
@@ -96,8 +94,4 @@ export class SchedulesService {
|
||||
throw new ForbiddenException(`User with ID=${userId} does not own schedule with ID=${schedule.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getSchedulePossibleTimeZones() {
|
||||
return cityTimezonesHandler();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { PlatformEndpointsModule } from "@/ee/platform-endpoints-module";
|
||||
import { EventsModule } from "@/modules/events/events.module";
|
||||
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
|
||||
import { TimezoneModule } from "@/modules/timezones/timezones.module";
|
||||
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [EventsModule, OAuthClientModule, PlatformEndpointsModule],
|
||||
imports: [EventsModule, OAuthClientModule, PlatformEndpointsModule, TimezoneModule],
|
||||
})
|
||||
export class EndpointsModule implements NestModule {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { TimezonesService } from "@/modules/timezones/services/timezones.service";
|
||||
import { Controller, Get } from "@nestjs/common";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { CityTimezones } from "@calcom/platform-libraries";
|
||||
import { ApiResponse } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "timezones",
|
||||
version: "2",
|
||||
})
|
||||
export class TimezonesController {
|
||||
constructor(private readonly timezonesService: TimezonesService) {}
|
||||
|
||||
@Get("/")
|
||||
async getTimeZones(): Promise<ApiResponse<CityTimezones>> {
|
||||
const timeZones = await this.timezonesService.getCityTimeZones();
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: timeZones,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { cityTimezonesHandler } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class TimezonesService {
|
||||
async getCityTimeZones() {
|
||||
return cityTimezonesHandler();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { TimezonesController } from "@/modules/timezones/controllers/timezones.controller";
|
||||
import { TimezonesService } from "@/modules/timezones/services/timezones.service";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
providers: [TimezonesService],
|
||||
controllers: [TimezonesController],
|
||||
exports: [TimezonesService],
|
||||
})
|
||||
export class TimezoneModule {}
|
||||
@@ -22,7 +22,40 @@
|
||||
"/api/v2/events/public": {
|
||||
"get": {
|
||||
"operationId": "EventsController_getPublicEvent",
|
||||
"parameters": [],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "username",
|
||||
"required": true,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "eventSlug",
|
||||
"required": true,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "isTeamEvent",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "org",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
@@ -55,7 +88,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateUserInput"
|
||||
"$ref": "#/components/schemas/CreateManagedPlatformUserInput"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,7 +166,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateUserInput"
|
||||
"$ref": "#/components/schemas/UpdateManagedPlatformUserInput"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -578,7 +611,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/platform/gcal/oauth/auth-url": {
|
||||
"/api/v2/ee/gcal/oauth/auth-url": {
|
||||
"get": {
|
||||
"operationId": "GcalController_redirect",
|
||||
"parameters": [
|
||||
@@ -605,7 +638,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/platform/gcal/oauth/save": {
|
||||
"/api/v2/ee/gcal/oauth/save": {
|
||||
"get": {
|
||||
"operationId": "GcalController_save",
|
||||
"parameters": [
|
||||
@@ -640,7 +673,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/platform/gcal/check": {
|
||||
"/api/v2/ee/gcal/check": {
|
||||
"get": {
|
||||
"operationId": "GcalController_check",
|
||||
"parameters": [],
|
||||
@@ -658,7 +691,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/platform/provider/{clientId}": {
|
||||
"/api/v2/ee/provider/{clientId}": {
|
||||
"get": {
|
||||
"operationId": "CalProviderController_verifyClientId",
|
||||
"parameters": [
|
||||
@@ -685,7 +718,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/platform/provider/{clientId}/access-token": {
|
||||
"/api/v2/ee/provider/{clientId}/access-token": {
|
||||
"get": {
|
||||
"operationId": "CalProviderController_verifyAccessToken",
|
||||
"parameters": [
|
||||
@@ -774,24 +807,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/schedules/time-zones": {
|
||||
"get": {
|
||||
"operationId": "SchedulesController_getTimeZones",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/schedules/{scheduleId}": {
|
||||
"get": {
|
||||
"operationId": "SchedulesController_getSchedule",
|
||||
@@ -870,7 +885,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/me": {
|
||||
"/api/v2/ee/me": {
|
||||
"get": {
|
||||
"operationId": "MeController_getMe",
|
||||
"parameters": [],
|
||||
@@ -895,7 +910,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateUserInput"
|
||||
"$ref": "#/components/schemas/UpdateManagedPlatformUserInput"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -951,6 +966,22 @@
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/bookings": {
|
||||
"get": {
|
||||
"operationId": "BookingsController_getBookings",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"operationId": "BookingsController_createBooking",
|
||||
"parameters": [],
|
||||
@@ -978,6 +1009,97 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/bookings/{bookingUid}": {
|
||||
"get": {
|
||||
"operationId": "BookingsController_getBooking",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "bookingUid",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/bookings/{bookingUid}/reschedule": {
|
||||
"get": {
|
||||
"operationId": "BookingsController_getBookingForReschedule",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "bookingUid",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/bookings/{bookingId}/cancel": {
|
||||
"post": {
|
||||
"operationId": "BookingsController_cancelBooking",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "bookingId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CancelBookingInput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/bookings/reccuring": {
|
||||
"post": {
|
||||
"operationId": "BookingsController_createReccuringBooking",
|
||||
@@ -1100,6 +1222,24 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/timezones": {
|
||||
"get": {
|
||||
"operationId": "TimezonesController_getTimeZones",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"info": {
|
||||
@@ -1112,7 +1252,7 @@
|
||||
"servers": [],
|
||||
"components": {
|
||||
"schemas": {
|
||||
"CreateUserInput": {
|
||||
"CreateManagedPlatformUserInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
@@ -1124,9 +1264,6 @@
|
||||
"timeFormat": {
|
||||
"type": "number"
|
||||
},
|
||||
"defaultScheduleId": {
|
||||
"type": "number"
|
||||
},
|
||||
"weekStart": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1138,7 +1275,7 @@
|
||||
"email"
|
||||
]
|
||||
},
|
||||
"UpdateUserInput": {
|
||||
"UpdateManagedPlatformUserInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
@@ -1526,6 +1663,10 @@
|
||||
"hashedLink"
|
||||
]
|
||||
},
|
||||
"CancelBookingInput": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
},
|
||||
"ReserveSlotInput": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { TestingModule } from "@nestjs/testing";
|
||||
import { Booking, User } from "@prisma/client";
|
||||
|
||||
export class BookingsRepositoryFixture {
|
||||
private primaReadClient: PrismaReadService["prisma"];
|
||||
private prismaWriteClient: PrismaWriteService["prisma"];
|
||||
|
||||
constructor(private readonly module: TestingModule) {
|
||||
this.primaReadClient = module.get(PrismaReadService).prisma;
|
||||
this.prismaWriteClient = module.get(PrismaWriteService).prisma;
|
||||
}
|
||||
|
||||
async getById(bookingId: Booking["id"]) {
|
||||
return this.primaReadClient.booking.findFirst({ where: { id: bookingId } });
|
||||
}
|
||||
|
||||
async deleteById(bookingId: Booking["id"]) {
|
||||
return this.prismaWriteClient.booking.delete({ where: { id: bookingId } });
|
||||
}
|
||||
|
||||
async deleteAllBookings(userId: User["id"], userEmail: User["email"]) {
|
||||
return this.prismaWriteClient.booking.deleteMany({ where: { userId, userPrimaryEmail: userEmail } });
|
||||
}
|
||||
}
|
||||
@@ -122,7 +122,7 @@ export const InstallAppButtonChild = ({
|
||||
data-testid={team.isUser ? "install-app-button-personal" : "anything else"}
|
||||
key={team.id}
|
||||
disabled={isInstalled}
|
||||
StartIcon={(props) => (
|
||||
StartIcon={(props: { className?: string }) => (
|
||||
<Avatar
|
||||
alt={team.logo || ""}
|
||||
imageSrc={team.logo || `${WEBAPP_URL}/${team.logo}/avatar.png`} // if no image, use default avatar
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
import { useSession } from "next-auth/react";
|
||||
import type { EventTypeSetup } from "pages/event-types/[type]";
|
||||
import { useState } from "react";
|
||||
import { useFormContext, Controller } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { AIPhoneSettingSchema } from "@calcom/prisma/zod-utils";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import {
|
||||
Button,
|
||||
Label,
|
||||
EmptyScreen,
|
||||
SettingsToggle,
|
||||
Divider,
|
||||
TextField,
|
||||
TextAreaField,
|
||||
PhoneInput,
|
||||
showToast,
|
||||
} from "@calcom/ui";
|
||||
import { Sparkles } from "@calcom/ui/components/icon";
|
||||
|
||||
type AIEventControllerProps = {
|
||||
eventType: EventTypeSetup;
|
||||
isTeamEvent: boolean;
|
||||
};
|
||||
|
||||
export default function AIEventController({ eventType, isTeamEvent }: AIEventControllerProps) {
|
||||
const { t } = useLocale();
|
||||
const session = useSession();
|
||||
const [aiEventState, setAIEventState] = useState<boolean>(eventType?.aiPhoneCallConfig?.enabled ?? false);
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
|
||||
const isOrg = !!session.data?.user?.org?.id;
|
||||
|
||||
if (session.status === "loading") return <></>;
|
||||
|
||||
return (
|
||||
<LicenseRequired>
|
||||
<div className="block items-start sm:flex">
|
||||
{!isOrg || !isTeamEvent ? (
|
||||
<EmptyScreen
|
||||
headline={t("Cal.ai")}
|
||||
Icon={Sparkles}
|
||||
description={t("upgrade_to_cal_ai_phone_number_description")}
|
||||
buttonRaw={<Button href="/enterprise">{t("upgrade")}</Button>}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full">
|
||||
<SettingsToggle
|
||||
labelClassName="text-sm"
|
||||
toggleSwitchAtTheEnd={true}
|
||||
switchContainerClassName={classNames(
|
||||
"border-subtle rounded-lg border py-6 px-4 sm:px-6",
|
||||
aiEventState && "rounded-b-none"
|
||||
)}
|
||||
childrenClassName="lg:ml-0"
|
||||
title={t("Cal.ai")}
|
||||
description={t("use_cal_ai_to_make_call_description")}
|
||||
checked={aiEventState}
|
||||
data-testid="instant-event-check"
|
||||
onCheckedChange={(e) => {
|
||||
if (!e) {
|
||||
formMethods.setValue("aiPhoneCallConfig.enabled", false, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
setAIEventState(false);
|
||||
} else {
|
||||
formMethods.setValue("aiPhoneCallConfig.enabled", true, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
setAIEventState(true);
|
||||
}
|
||||
}}>
|
||||
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
||||
{aiEventState && <AISettings eventType={eventType} />}
|
||||
</div>
|
||||
</SettingsToggle>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</LicenseRequired>
|
||||
);
|
||||
}
|
||||
|
||||
const AISettings = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
const [calApiKey, setCalApiKey] = useState("");
|
||||
|
||||
const createCallMutation = trpc.viewer.organizations.createPhoneCall.useMutation({
|
||||
onSuccess: (data) => {
|
||||
if (!!data?.call_id) {
|
||||
showToast("Phone Call Created successfully", "success");
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
showToast(t("something_went_wrong"), "error");
|
||||
},
|
||||
});
|
||||
|
||||
// const [createModalOpen, setCreateModalOpen] = useState(false);
|
||||
|
||||
// const NewPhoneButton = () => {
|
||||
// const { t } = useLocale();
|
||||
// return (
|
||||
// <Button
|
||||
// color="primary"
|
||||
// data-testid="new_phone_number"
|
||||
// StartIcon={Plus}
|
||||
// onClick={() => setCreateModalOpen(true)}>
|
||||
// {t("New Phone number")}
|
||||
// </Button>
|
||||
// );
|
||||
// };
|
||||
|
||||
// v1 will require the user to log in to Retellai.com to create a phone number, and an agent and
|
||||
// authorize it with the Cal.com API key / OAuth
|
||||
// const retellAuthorized = true; // TODO: call retellAPI here
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = formMethods.getValues("aiPhoneCallConfig");
|
||||
|
||||
const data = await AIPhoneSettingSchema.parseAsync({
|
||||
...values,
|
||||
eventTypeId: eventType.id,
|
||||
calApiKey,
|
||||
});
|
||||
|
||||
createCallMutation.mutate(data);
|
||||
} catch (err) {
|
||||
if (err instanceof z.ZodError) {
|
||||
const fieldName = err.issues?.[0]?.path?.[0];
|
||||
const message = err.issues?.[0]?.message;
|
||||
showToast(`Error on ${fieldName}: ${message} `, "error");
|
||||
} else {
|
||||
showToast(t("something_went_wrong"), "error");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="space-y-4">
|
||||
<>
|
||||
<Label>{t("your_phone_number")}</Label>
|
||||
<Controller
|
||||
name="aiPhoneCallConfig.yourPhoneNumber"
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<PhoneInput
|
||||
required
|
||||
placeholder={t("your_phone_number")}
|
||||
id="aiPhoneCallConfig.yourPhoneNumber"
|
||||
name="aiPhoneCallConfig.yourPhoneNumber"
|
||||
value={value}
|
||||
onChange={(val) => {
|
||||
onChange(val);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Label>{t("number_to_call")}</Label>
|
||||
<Controller
|
||||
name="aiPhoneCallConfig.numberToCall"
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<PhoneInput
|
||||
required
|
||||
placeholder={t("phone_number")}
|
||||
id="aiPhoneCallConfig.numberToCall"
|
||||
name="aiPhoneCallConfig.numberToCall"
|
||||
value={value}
|
||||
onChange={(val) => {
|
||||
onChange(val);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
</>
|
||||
|
||||
<TextField
|
||||
type="text"
|
||||
hint="Variable: {name}"
|
||||
label={t("guest_name")}
|
||||
placeholder="Jane Doe"
|
||||
{...formMethods.register("aiPhoneCallConfig.guestName")}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
type="text"
|
||||
hint="For eg:- cal_live_0123.."
|
||||
label={t("provide_api_key")}
|
||||
name="calApiKey"
|
||||
placeholder="Cal API Key"
|
||||
value={calApiKey}
|
||||
onChange={(e) => {
|
||||
setCalApiKey(e.target.value);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
<TextAreaField
|
||||
rows={3}
|
||||
required
|
||||
placeholder={t("general_prompt")}
|
||||
label={t("general_prompt")}
|
||||
{...formMethods.register("aiPhoneCallConfig.generalPrompt")}
|
||||
onChange={(e) => {
|
||||
formMethods.setValue("aiPhoneCallConfig.generalPrompt", e.target.value, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextAreaField
|
||||
rows={3}
|
||||
placeholder={t("begin_message")}
|
||||
label={t("begin_message")}
|
||||
{...formMethods.register("aiPhoneCallConfig.beginMessage")}
|
||||
onChange={(e) => {
|
||||
formMethods.setValue("aiPhoneCallConfig.beginMessage", e.target.value, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
disabled={createCallMutation.isPending}
|
||||
loading={createCallMutation.isPending}
|
||||
onClick={handleSubmit}>
|
||||
{t("make_a_call")}
|
||||
</Button>
|
||||
|
||||
{/* TODO:<small className="block opacity-60">
|
||||
Want to automate outgoing phone calls? Read our{" "}
|
||||
<Link className="underline" href="https://cal.com/docs">
|
||||
API docs
|
||||
</Link>{" "}
|
||||
and learn how to build workflows.
|
||||
</small> */}
|
||||
</div>
|
||||
|
||||
{/* TODO:
|
||||
<>
|
||||
<EmptyScreen
|
||||
Icon={Phone}
|
||||
headline={t("Create your phone number")}
|
||||
description={t(
|
||||
"This phone number can be called by guests but can also do proactive outbound calls by the AI agent."
|
||||
)}
|
||||
buttonRaw={
|
||||
<div className="flex justify-between gap-2">
|
||||
<NewPhoneButton />
|
||||
<Button color="secondary">{t("learn_more")}</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<Dialog open={createModalOpen} onOpenChange={(isOpen) => !isOpen && setCreateModalOpen(false)}>
|
||||
<DialogContent
|
||||
enableOverflow
|
||||
title={t("Create phone number")}
|
||||
description={t("This number can later be called or can do proactive outbound calls")}>
|
||||
<div className="mb-12 mt-4">
|
||||
<TextField placeholder="+415" hint="Area Code" />
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
*/}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||
|
||||
import AIEventController from "./AIEventController";
|
||||
|
||||
export const EventAITab = ({
|
||||
eventType,
|
||||
isTeamEvent,
|
||||
}: Pick<EventTypeSetupProps, "eventType"> & { isTeamEvent: boolean }) => {
|
||||
return <AIEventController eventType={eventType} isTeamEvent={isTeamEvent} />;
|
||||
};
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
Repeat,
|
||||
Grid,
|
||||
Zap,
|
||||
Sparkles,
|
||||
Users,
|
||||
ExternalLink,
|
||||
Code,
|
||||
@@ -283,6 +284,15 @@ function EventTypeSingleLayout({
|
||||
info: `${activeWebhooksNumber} ${t("active")}`,
|
||||
});
|
||||
}
|
||||
const hidden = true; // hidden while in alpha trial. you can access it with tabName=ai
|
||||
if (team && hidden) {
|
||||
navigation.push({
|
||||
name: "Cal.ai",
|
||||
href: `/event-types/${eventType.id}?tabName=ai`,
|
||||
icon: Sparkles,
|
||||
info: "cal_ai_event_tab_description", // todo `cal_ai_event_tab_description`,
|
||||
});
|
||||
}
|
||||
return navigation;
|
||||
}, [
|
||||
t,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React from "react";
|
||||
|
||||
import type { LucideIcon } from "@calcom/ui/components/icon";
|
||||
|
||||
import type { SVGComponent } from "@lib/types/SVGComponent";
|
||||
|
||||
interface LinkIconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
Icon: SVGComponent;
|
||||
Icon: SVGComponent | LucideIcon;
|
||||
}
|
||||
|
||||
export default function LinkIconButton(props: LinkIconButtonProps) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { GetServerSidePropsContext } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { getBookingWithResponses } from "@calcom/features/bookings/lib/get-booking";
|
||||
import getBookingInfo from "@calcom/features/bookings/lib/getBookingInfo";
|
||||
import { parseRecurringEvent } from "@calcom/lib";
|
||||
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
|
||||
import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat";
|
||||
@@ -62,58 +62,9 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const maybeBookingUidFromSeat = await maybeGetBookingUidFromSeat(prisma, uid);
|
||||
if (maybeBookingUidFromSeat.uid) uid = maybeBookingUidFromSeat.uid;
|
||||
if (maybeBookingUidFromSeat.seatReferenceUid) seatReferenceUid = maybeBookingUidFromSeat.seatReferenceUid;
|
||||
const bookingInfoRaw = await prisma.booking.findFirst({
|
||||
where: {
|
||||
uid: uid,
|
||||
},
|
||||
select: {
|
||||
title: true,
|
||||
id: true,
|
||||
uid: true,
|
||||
description: true,
|
||||
customInputs: true,
|
||||
smsReminderNumber: true,
|
||||
recurringEventId: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
location: true,
|
||||
status: true,
|
||||
metadata: true,
|
||||
cancellationReason: true,
|
||||
responses: true,
|
||||
rejectionReason: true,
|
||||
userPrimaryEmail: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
username: true,
|
||||
timeZone: true,
|
||||
},
|
||||
},
|
||||
attendees: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
timeZone: true,
|
||||
},
|
||||
},
|
||||
eventTypeId: true,
|
||||
eventType: {
|
||||
select: {
|
||||
eventName: true,
|
||||
slug: true,
|
||||
timeZone: true,
|
||||
},
|
||||
},
|
||||
seatsReferences: {
|
||||
select: {
|
||||
referenceUid: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { bookingInfoRaw, bookingInfo } = await getBookingInfo(uid);
|
||||
|
||||
if (!bookingInfoRaw) {
|
||||
return {
|
||||
notFound: true,
|
||||
@@ -133,7 +84,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
requiresLoginToUpdate = true;
|
||||
}
|
||||
|
||||
const bookingInfo = getBookingWithResponses(bookingInfoRaw);
|
||||
// @NOTE: had to do this because Server side cant return [Object objects]
|
||||
// probably fixable with json.stringify -> json.parse
|
||||
bookingInfo["startTime"] = (bookingInfo?.startTime as Date)?.toISOString() as unknown as Date;
|
||||
|
||||
@@ -34,6 +34,11 @@ import { EventTypeSingleLayout } from "@components/eventtype/EventTypeSingleLayo
|
||||
|
||||
import { type PageProps } from "~/event-types/views/event-types-single-view.getServerSideProps";
|
||||
|
||||
const DEFAULT_PROMPT_VALUE =
|
||||
"## You are helping user set up a call with the support team. The appointment is 15 min long. You are a pleasant and friendly.\n\n## Style Guardrails\nBe Concise: Respond succinctly, addressing one topic at most.\nEmbrace Variety: Use diverse language and rephrasing to enhance clarity without repeating content.\nBe Conversational: Use everyday language, making the chat feel like talking to a friend.\nBe Proactive: Lead the conversation, often wrapping up with a question or next-step suggestion.\nAvoid multiple questions in a single response.\nGet clarity: If the user only partially answers a question, or if the answer is unclear, keep asking to get clarity.\nUse a colloquial way of referring to the date (like Friday, Jan 14th, or Tuesday, Jan 12th, 2024 at 8am).\nIf you are saying a time like 8:00 AM, just say 8 AM and emit the trailing zeros.\n\n## Response Guideline\nAdapt and Guess: Try to understand transcripts that may contain transcription errors. Avoid mentioning \"transcription error\" in the response.\nStay in Character: Keep conversations within your role'''s scope, guiding them back creatively without repeating.\nEnsure Fluid Dialogue: Respond in a role-appropriate, direct manner to maintain a smooth conversation flow.\n\n## Schedule Rule\nCurrent time is {{current_time}}. You only schedule time in current calendar year, you cannot schedule time that'''s in the past.\n\n## Task Steps\n1. I am here to learn more about your issue and help schedule an appointment with our support team.\n2. Ask for user name and email. Confirm the name and email with user by reading it back to user.\n3. Ask user for \"When would you want to meet with one of our representive\".\n4. Call function check_availability to check for availability in the user provided time range.\n - if availability exists, inform user about the availability range (do not repeat the detailed available slot) and ask user to choose from it. Make sure user chose a slot within detailed available slot.\n - if availability does not exist, ask user to select another time range for the appointment, repeat this step 3.\n4. Confirm the date and time selected by user: \"Just to confirm, you want to book the appointment at ...\".\n6. Once confirmed, call function book_appointment to book the appointment.\n - if booking returned booking detail, it means booking is successful, proceed to step 7.\n - if booking returned error message, let user know why the booking was not successful, and maybe start over with step 3.\n7. Inform the user booking is successful, and ask if user have any questions. Answer them if there are any.\n8. After all questions answered, call function end_call to hang up.";
|
||||
|
||||
const DEFAULT_BEGIN_MESSAGE = "Hi. How are you doing?";
|
||||
|
||||
// These can't really be moved into calcom/ui due to the fact they use infered getserverside props typings;
|
||||
const EventSetupTab = dynamic(() =>
|
||||
import("@components/eventtype/EventSetupTab").then((mod) => mod.EventSetupTab)
|
||||
@@ -73,6 +78,8 @@ const EventWebhooksTab = dynamic(() =>
|
||||
import("@components/eventtype/EventWebhooksTab").then((mod) => mod.EventWebhooksTab)
|
||||
);
|
||||
|
||||
const EventAITab = dynamic(() => import("@components/eventtype/EventAITab").then((mod) => mod.EventAITab));
|
||||
|
||||
const ManagedEventTypeDialog = dynamic(() => import("@components/eventtype/ManagedEventDialog"));
|
||||
|
||||
export type Host = { isFixed: boolean; userId: number; priority: number };
|
||||
@@ -92,6 +99,7 @@ const querySchema = z.object({
|
||||
"advanced",
|
||||
"workflows",
|
||||
"webhooks",
|
||||
"ai",
|
||||
])
|
||||
.optional()
|
||||
.default("setup"),
|
||||
@@ -243,6 +251,14 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
})),
|
||||
seatsPerTimeSlotEnabled: eventType.seatsPerTimeSlot,
|
||||
assignAllTeamMembers: eventType.assignAllTeamMembers,
|
||||
aiPhoneCallConfig: {
|
||||
generalPrompt: eventType.aiPhoneCallConfig?.generalPrompt ?? DEFAULT_PROMPT_VALUE,
|
||||
enabled: eventType.aiPhoneCallConfig?.enabled,
|
||||
beginMessage: eventType.aiPhoneCallConfig?.beginMessage ?? DEFAULT_BEGIN_MESSAGE,
|
||||
guestName: eventType.aiPhoneCallConfig?.guestName,
|
||||
yourPhoneNumber: eventType.aiPhoneCallConfig?.yourPhoneNumber,
|
||||
numberToCall: eventType.aiPhoneCallConfig?.numberToCall,
|
||||
},
|
||||
};
|
||||
}, [eventType, periodDates, metadata]);
|
||||
const formMethods = useForm<FormValues>({
|
||||
@@ -375,6 +391,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
/>
|
||||
),
|
||||
webhooks: <EventWebhooksTab eventType={eventType} />,
|
||||
ai: <EventAITab eventType={eventType} isTeamEvent={!!team} />,
|
||||
} as const;
|
||||
const isObject = <T,>(value: T): boolean => {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "3.9.2",
|
||||
"version": "3.9.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
||||
@@ -11,7 +11,7 @@ test.describe.configure({ mode: "parallel" });
|
||||
test.afterEach(({ users }) => users.deleteAll());
|
||||
|
||||
test.describe("Out of office", () => {
|
||||
test("User can create out of office entry", async ({ page, users }) => {
|
||||
test.skip("User can create out of office entry", async ({ page, users }) => {
|
||||
const user = await users.create({ name: "userOne" });
|
||||
|
||||
await user.apiLogin();
|
||||
@@ -23,7 +23,7 @@ test.describe("Out of office", () => {
|
||||
await expect(page.locator(`data-testid=table-redirect-n-a`)).toBeVisible();
|
||||
});
|
||||
|
||||
test("User can configure booking redirect", async ({ page, users }) => {
|
||||
test.skip("User can configure booking redirect", async ({ page, users }) => {
|
||||
const user = await users.create({ name: "userOne" });
|
||||
const userTo = await users.create({ name: "userTwo" });
|
||||
|
||||
|
||||
@@ -182,6 +182,7 @@
|
||||
"2fa_confirm_current_password": "قم بتأكيد كلمة المرور الحالية للبدء.",
|
||||
"2fa_scan_image_or_use_code": "امسح الصورة أدناه ضوئيًا باستخدام تطبيق المصادقة على هاتفك، أو أدخل الرمز النصي يدويًا.",
|
||||
"text": "النص",
|
||||
"your_phone_number": "رقم هاتفك",
|
||||
"multiline_text": "النص متعدد السطور",
|
||||
"number": "الرقم",
|
||||
"checkbox": "مربع الاختيار",
|
||||
|
||||
@@ -182,6 +182,7 @@
|
||||
"2fa_confirm_current_password": "Pro pokračování zadejte vaše současné heslo.",
|
||||
"2fa_scan_image_or_use_code": "Načtěte obrázek níž autentifikační aplikací na vašem telefonu nebo opište kód ručně.",
|
||||
"text": "Text",
|
||||
"your_phone_number": "Vaše telefonní číslo",
|
||||
"multiline_text": "Víceřádkový text",
|
||||
"number": "Číslo",
|
||||
"checkbox": "Zaškrtávací pole",
|
||||
|
||||
@@ -151,6 +151,7 @@
|
||||
"2fa_confirm_current_password": "Bekræft din nuværende adgangskode for at komme i gang.",
|
||||
"2fa_scan_image_or_use_code": "Scan billedet nedenfor med autentificerings-appen på din telefon, eller indtast tekstkoden manuelt i stedet.",
|
||||
"text": "Tekst",
|
||||
"your_phone_number": "Dit Telefonnummer",
|
||||
"multiline_text": "Flerlinje Tekst",
|
||||
"number": "Nummer",
|
||||
"checkbox": "Afkrydsningsfelt",
|
||||
|
||||
@@ -206,6 +206,7 @@
|
||||
"2fa_confirm_current_password": "Bestätigen Sie Ihr aktuelles Passwort, um loszulegen.",
|
||||
"2fa_scan_image_or_use_code": "Scannen Sie das Bild mit der Authentifizierungs-App auf Ihrem Telefon oder geben Sie stattdessen den Textcode manuell ein.",
|
||||
"text": "Text",
|
||||
"your_phone_number": "Ihre Telefonnummer",
|
||||
"multiline_text": "Mehrzeiliger Text",
|
||||
"number": "Nummer",
|
||||
"checkbox": "Checkbox",
|
||||
@@ -1513,7 +1514,7 @@
|
||||
"report_app": "App melden",
|
||||
"limit_booking_frequency": "Buchungsfrequenz begrenzen",
|
||||
"limit_booking_frequency_description": "Begrenzen, wie oft dieses Ereignis gebucht werden kann",
|
||||
"limit_booking_only_first_slot": "Buchun auf den ersten Platz beschränken",
|
||||
"limit_booking_only_first_slot": "Buchung auf den ersten Platz beschränken",
|
||||
"limit_booking_only_first_slot_description": "Nur die Buchung des ersten Platzes am Tag erlauben",
|
||||
"limit_total_booking_duration": "Gesamtbuchungsdauer beschränken",
|
||||
"limit_total_booking_duration_description": "Begrenzt die Gesamtanzahl, die dieser Termin gebucht werden kann",
|
||||
|
||||
@@ -90,6 +90,7 @@
|
||||
"copy_public_page_link": "Αντιγραφή συνδέσμου δημόσιας σελίδας",
|
||||
"sign_out": "Αποσύνδεση",
|
||||
"2fa_confirm_current_password": "Επιβεβαιώστε τον κωδικό πρόσβασης σας για να ξεκινήσετε.",
|
||||
"your_phone_number": "Ο αριθμός τηλεφώνου σας",
|
||||
"required": "Υποχρεωτικό",
|
||||
"optional": "Προαιρετικό",
|
||||
"guests": "Επισκέπτες",
|
||||
|
||||
@@ -81,6 +81,8 @@
|
||||
"payment": "Payment",
|
||||
"missing_card_fields": "Missing card fields",
|
||||
"pay_now": "Pay now",
|
||||
"general_prompt": "General Prompt",
|
||||
"begin_message":"Begin Message",
|
||||
"codebase_has_to_stay_opensource": "The codebase has to stay open source, whether it was modified or not",
|
||||
"cannot_repackage_codebase": "You can not repackage or sell the codebase",
|
||||
"acquire_license": "Acquire a commercial license to remove these terms by emailing",
|
||||
@@ -207,6 +209,7 @@
|
||||
"2fa_confirm_current_password": "Confirm your current password to get started.",
|
||||
"2fa_scan_image_or_use_code": "Scan the image below with the authenticator app on your phone or manually enter the text code instead.",
|
||||
"text": "Text",
|
||||
"your_phone_number":"Your Phone Number",
|
||||
"multiline_text": "Multiline Text",
|
||||
"number": "Number",
|
||||
"checkbox": "Checkbox",
|
||||
@@ -693,6 +696,7 @@
|
||||
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
|
||||
"multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)",
|
||||
"minutes": "Minutes",
|
||||
"use_cal_ai_to_make_call_description": "Use Cal.ai to get an AI powered phone number or make calls to guests.",
|
||||
"round_robin": "Round Robin",
|
||||
"round_robin_description": "Cycle meetings between multiple team members.",
|
||||
"managed_event": "Managed Event",
|
||||
@@ -1277,6 +1281,7 @@
|
||||
"upgrade": "Upgrade",
|
||||
"upgrade_to_access_recordings_title": "Upgrade to access recordings",
|
||||
"upgrade_to_access_recordings_description": "Recordings are only available as part of our teams plan. Upgrade to start recording your calls",
|
||||
"upgrade_to_cal_ai_phone_number_description":"Upgrade to Enterprise to generate an AI Agent phone number that can call guests to schedule calls",
|
||||
"recordings_are_part_of_the_teams_plan": "Recordings are part of the teams plan",
|
||||
"team_feature_teams": "This is a Team feature. Upgrade to Team to see your team's availability.",
|
||||
"team_feature_workflows": "This is a Team feature. Upgrade to Team to automate your event notifications and reminders with Workflows.",
|
||||
@@ -2249,6 +2254,8 @@
|
||||
"troubleshooter_tooltip": "Open the troubleshooter and figure out what is wrong with your schedule",
|
||||
"need_help": "Need help?",
|
||||
"troubleshooter": "Troubleshooter",
|
||||
"number_to_call": "Number to Call",
|
||||
"guest_name": "Guest Name",
|
||||
"please_install_a_calendar": "Please install a calendar",
|
||||
"instant_tab_title": "Instant Booking",
|
||||
"instant_event_tab_description": "Let people book immediately",
|
||||
@@ -2256,6 +2263,7 @@
|
||||
"dont_want_to_wait": "Don't want to wait?",
|
||||
"meeting_started": "Meeting Started",
|
||||
"pay_and_book": "Pay to book",
|
||||
"cal_ai_event_tab_description":"Let AI Agents book you",
|
||||
"booking_not_found_error": "Could not find booking",
|
||||
"booking_seats_full_error": "Booking seats are full",
|
||||
"missing_payment_credential_error": "Missing payment credentials",
|
||||
@@ -2330,5 +2338,6 @@
|
||||
"lock_org_users_eventtypes": "Lock individual event type creation",
|
||||
"lock_org_users_eventtypes_description": "Prevent members from creating their own event types.",
|
||||
"cookie_consent_checkbox": "I consent to our privacy policy and cookie usage",
|
||||
"make_a_call": "Make a Call",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -182,6 +182,7 @@
|
||||
"2fa_confirm_current_password": "Confirma tu contraseña actual para empezar.",
|
||||
"2fa_scan_image_or_use_code": "Escanee la imagen a continuación con la aplicación de autenticación en su teléfono o ingrese manualmente el código de texto.",
|
||||
"text": "Texto",
|
||||
"your_phone_number": "Su número de teléfono",
|
||||
"multiline_text": "Texto de varias líneas",
|
||||
"number": "Número",
|
||||
"checkbox": "Casilla",
|
||||
|
||||
@@ -1 +1,25 @@
|
||||
{}
|
||||
{
|
||||
"identity_provider": "Identiteedi pakkuja",
|
||||
"trial_days_left": "Teil on $t(day, {\"count\": {{days}} }) jäänud PRO prooviversiooni",
|
||||
"day_one": "{{count}} päev",
|
||||
"day_other": "{{count}} päeva",
|
||||
"second_one": "{{count}} sekundit",
|
||||
"second_other": "{{count}} sekundit",
|
||||
"upgrade_now": "Uuenda nüüd",
|
||||
"accept_invitation": "Võta kutse vastu",
|
||||
"calcom_explained": "{{appName}} pakub ajakava infrastruktuuri absoluutselt kõigile.",
|
||||
"reset_password_subject": "{{appName}}: Parooli taastamise juhend",
|
||||
"verify_email_subject": "{{appName}}: Kinnita oma konto",
|
||||
"check_your_email": "Kontrolli enda e-posti",
|
||||
"old_email_address": "Vana e-posti aadress",
|
||||
"new_email_address": "Uus e-posti aadress",
|
||||
"verify_email_page_body": "Me saatsime e-kirja meiliaadressile: {{email}}. Oluline on kinnitada oma e-posti aadress, et tagada parim e-posti ja kalendri ühendus {{appName}}.",
|
||||
"verify_email_banner_body": "Parima meili ja kalendri edastamise tagamiseks kinnitage oma e-posti aadress",
|
||||
"verify_email_email_header": "Kinnita oma e-posti aadress",
|
||||
"verify_email_email_button": "Kinnita e-posti aadress",
|
||||
"verify_email_change_description": "Olete hiljuti taotlenud oma {{appName}} kontole sisselogimiseks kasutatava e-posti aadressi muutmist. Uue e-posti aadressi kinnitamiseks klõpsake alloleval nupul.",
|
||||
"verify_email_change_success_toast": "Teie e-posti aadress on värskendatud: {{email}}",
|
||||
"verify_email_change_failure_toast": "E-posti aadressi värskendamine ebaõnnestus.",
|
||||
"change_of_email": "Kinnitage oma uus rakenduse {{appName}} e-posti aadress",
|
||||
"change_of_email_toast": "Saatsime kinnituslingi aadressile {{email}}. Värskendame teie e-posti aadressi, kui klõpsate sellel lingil."
|
||||
}
|
||||
|
||||
@@ -206,6 +206,7 @@
|
||||
"2fa_confirm_current_password": "Confirmez votre mot de passe actuel pour commencer.",
|
||||
"2fa_scan_image_or_use_code": "Scannez l'image ci-dessous avec votre application d'authentification ou saisissez manuellement le code.",
|
||||
"text": "Texte",
|
||||
"your_phone_number": "Votre numéro de téléphone",
|
||||
"multiline_text": "Zone de texte",
|
||||
"number": "Nombre",
|
||||
"checkbox": "Case à cocher",
|
||||
|
||||
@@ -207,6 +207,7 @@
|
||||
"2fa_confirm_current_password": "כדי להתחיל, יש לאשר את הסיסמה הנוכחית.",
|
||||
"2fa_scan_image_or_use_code": "סרוק את התמונה הבאה באמצעות אפליקציית האימות (authenticator) בטלפון או הזן את קוד הטקסט ידנית.",
|
||||
"text": "טקסט",
|
||||
"your_phone_number": "מספר הטלפון שלך",
|
||||
"multiline_text": "טקסט מרובה שורות",
|
||||
"number": "מספר",
|
||||
"checkbox": "תיבת סימון",
|
||||
@@ -2230,6 +2231,7 @@
|
||||
"troubleshooter_tooltip": "אפשר לפתוח את פותר הבעיות ולנסות לפענח מה לא תקין בלוח הזמנים שלך",
|
||||
"need_help": "צריך עזרה?",
|
||||
"troubleshooter": "פותר בעיות",
|
||||
"number_to_call": "מספר לחיוג",
|
||||
"please_install_a_calendar": "נא להתקין לוח שנה",
|
||||
"instant_tab_title": "הזמנה מיידית",
|
||||
"instant_event_tab_description": "לאפשר לאנשים ליצור הזמנות מיידית",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -206,6 +206,7 @@
|
||||
"2fa_confirm_current_password": "Conferma la tua password attuale per iniziare.",
|
||||
"2fa_scan_image_or_use_code": "Scansiona l'immagine qui sotto con l'app di autenticazione sul tuo telefono o inserisci manualmente il codice di testo.",
|
||||
"text": "Testo",
|
||||
"your_phone_number": "Il tuo numero di telefono",
|
||||
"multiline_text": "Testo Multilinea",
|
||||
"number": "Numero",
|
||||
"checkbox": "Casella di controllo",
|
||||
|
||||
@@ -182,6 +182,7 @@
|
||||
"2fa_confirm_current_password": "現在のパスワードを確認して開始します。",
|
||||
"2fa_scan_image_or_use_code": "携帯電話の認証アプリで下の画像をスキャンするか、手動でテキストコードを入力してください。",
|
||||
"text": "テキスト",
|
||||
"your_phone_number": "あなたの電話番号",
|
||||
"multiline_text": "複数行のテキスト",
|
||||
"number": "番号",
|
||||
"checkbox": "チェックボックス",
|
||||
|
||||
@@ -182,6 +182,7 @@
|
||||
"2fa_confirm_current_password": "시작하려면 현재 비밀번호를 확인하세요.",
|
||||
"2fa_scan_image_or_use_code": "휴대폰의 인증 앱으로 아래 이미지를 스캔하거나 텍스트 코드를 수동으로 입력하세요.",
|
||||
"text": "텍스트",
|
||||
"your_phone_number": "본인 전화 번호",
|
||||
"multiline_text": "멀티라인 텍스트",
|
||||
"number": "숫자",
|
||||
"checkbox": "체크박스",
|
||||
|
||||
@@ -182,6 +182,7 @@
|
||||
"2fa_confirm_current_password": "Bevestig uw huidige wachtwoord om aan de slag te gaan.",
|
||||
"2fa_scan_image_or_use_code": "Scan de afbeelding hieronder met de authenticator-app op uw telefoon of voer de tekstcode handmatig in.",
|
||||
"text": "Tekst",
|
||||
"your_phone_number": "Uw telefoonnummer",
|
||||
"multiline_text": "Meerdere regels tekst",
|
||||
"number": "Nummer",
|
||||
"checkbox": "Selectievakje",
|
||||
|
||||
@@ -148,6 +148,7 @@
|
||||
"2fa_confirm_current_password": "Bekreft ditt nåværende passord for å komme i gang.",
|
||||
"2fa_scan_image_or_use_code": "Skann bildet nedenfor med autentiseringsappen på telefonen eller skriv inn tekstkoden manuelt i stedet.",
|
||||
"text": "Tekst",
|
||||
"your_phone_number": "Ditt Telefonnummer",
|
||||
"multiline_text": "Flerlinjetekst",
|
||||
"number": "Tall",
|
||||
"checkbox": "Avmerkingsboks",
|
||||
|
||||
@@ -182,6 +182,7 @@
|
||||
"2fa_confirm_current_password": "Potwierdź aktualne hasło, aby rozpocząć.",
|
||||
"2fa_scan_image_or_use_code": "Zeskanuj poniższy obraz za pomocą aplikacji uwierzytelniającej na telefonie lub ręcznie wprowadź kod tekstowy.",
|
||||
"text": "Tekst",
|
||||
"your_phone_number": "Twój numer telefonu",
|
||||
"multiline_text": "Tekst wieloliniowy",
|
||||
"number": "Numer",
|
||||
"checkbox": "Pole wyboru",
|
||||
|
||||
@@ -207,6 +207,7 @@
|
||||
"2fa_confirm_current_password": "Insira a sua senha atual para começar.",
|
||||
"2fa_scan_image_or_use_code": "Digitalize a imagem abaixo com o aplicativo autenticador no seu telefone, ou insira o código manualmente.",
|
||||
"text": "Texto",
|
||||
"your_phone_number": "Seu número de telefone",
|
||||
"multiline_text": "Múltiplas linhas de texto",
|
||||
"number": "Número",
|
||||
"checkbox": "Caixa de seleção",
|
||||
|
||||
@@ -206,6 +206,7 @@
|
||||
"2fa_confirm_current_password": "Confirme a sua senha atual para começar.",
|
||||
"2fa_scan_image_or_use_code": "Digitalize a imagem abaixo com a aplicação de autenticação no seu telefone, ou insira o código de texto manualmente.",
|
||||
"text": "Texto",
|
||||
"your_phone_number": "O seu número de telefone",
|
||||
"multiline_text": "Múltiplas linhas de texto",
|
||||
"number": "Número",
|
||||
"checkbox": "Caixa de seleção",
|
||||
|
||||
@@ -182,6 +182,7 @@
|
||||
"2fa_confirm_current_password": "Confirmați parola curentă pentru a începe.",
|
||||
"2fa_scan_image_or_use_code": "Scanați imaginea de mai jos cu aplicația de autentificare de pe telefon sau introduceți manual codul de text.",
|
||||
"text": "Textul",
|
||||
"your_phone_number": "Numărul dvs. de telefon",
|
||||
"multiline_text": "Text Multiline",
|
||||
"number": "Număr",
|
||||
"checkbox": "Caseta de bifat",
|
||||
|
||||
@@ -183,6 +183,7 @@
|
||||
"2fa_confirm_current_password": "Подтвердите свой текущий пароль.",
|
||||
"2fa_scan_image_or_use_code": "Отсканируйте изображение ниже с помощью приложения-аутентификатора на вашем телефоне или введите текстовый код вручную.",
|
||||
"text": "Текст",
|
||||
"your_phone_number": "Ваш номер телефона",
|
||||
"multiline_text": "Многострочный текст",
|
||||
"number": "Число",
|
||||
"checkbox": "Чекбокс",
|
||||
|
||||
@@ -182,6 +182,7 @@
|
||||
"2fa_confirm_current_password": "Potvrdite vašu trenutnu lozinku da bi počeli.",
|
||||
"2fa_scan_image_or_use_code": "Skenirajte sliku ispod telefonom sa aplikacijom za autentikaciju ili manuelno unesite tekst kod.",
|
||||
"text": "Tekst",
|
||||
"your_phone_number": "Vaš broj telefona",
|
||||
"multiline_text": "Višelinijski Tekst",
|
||||
"number": "Broj",
|
||||
"checkbox": "Polje za potvrdu",
|
||||
|
||||
@@ -182,6 +182,7 @@
|
||||
"2fa_confirm_current_password": "Bekräfta ditt nuvarande lösenord för att komma igång.",
|
||||
"2fa_scan_image_or_use_code": "Skanna bilden nedan med autentiseringsappen på din telefon eller ange textkoden manuellt.",
|
||||
"text": "Text",
|
||||
"your_phone_number": "Ditt telefonnummer",
|
||||
"multiline_text": "Text på flera rader",
|
||||
"number": "Nummer",
|
||||
"checkbox": "Kryssruta",
|
||||
|
||||
@@ -182,6 +182,7 @@
|
||||
"2fa_confirm_current_password": "Başlamak için mevcut şifrenizi onaylayın.",
|
||||
"2fa_scan_image_or_use_code": "Aşağıdaki resmi telefonunuzdaki kimlik doğrulama uygulamasını kullanarak tarayın veya bunun yerine metin kodunu manuel olarak girin.",
|
||||
"text": "Metin",
|
||||
"your_phone_number": "Telefon Numaranız",
|
||||
"multiline_text": "Çok Satırlı Metin",
|
||||
"number": "Sayı",
|
||||
"checkbox": "Onay kutusu",
|
||||
|
||||
@@ -206,6 +206,7 @@
|
||||
"2fa_confirm_current_password": "Підтвердьте свій поточний пароль, щоб почати.",
|
||||
"2fa_scan_image_or_use_code": "Проскануйте зображення нижче за допомогою застосунка для автентифікації на телефоні або введіть текстовий код вручну.",
|
||||
"text": "Текст",
|
||||
"your_phone_number": "Ваш номер телефону",
|
||||
"multiline_text": "Багаторядковий текст",
|
||||
"number": "Число",
|
||||
"checkbox": "Прапорець",
|
||||
|
||||
@@ -183,6 +183,7 @@
|
||||
"2fa_confirm_current_password": "Xác nhận lại mật khẩu hiện tại của bạn để bắt đầu.",
|
||||
"2fa_scan_image_or_use_code": "Quét hình ảnh dưới đây với ứng dụng \bxác thực hai yếu tố trên điện thoại bạn hoặc nhập mã code.",
|
||||
"text": "Chữ",
|
||||
"your_phone_number": "Số điện thoại của bạn",
|
||||
"multiline_text": "Chữ nhiều dòng",
|
||||
"number": "Số",
|
||||
"checkbox": "Checkbox",
|
||||
|
||||
@@ -203,6 +203,7 @@
|
||||
"2fa_confirm_current_password": "输入您当前的密码来开始设置。",
|
||||
"2fa_scan_image_or_use_code": "请用您的手机上的身份验证器扫描下面的图像,或者手动输入验证码。",
|
||||
"text": "文本",
|
||||
"your_phone_number": "您的电话号码",
|
||||
"multiline_text": "多行文本",
|
||||
"number": "数字",
|
||||
"checkbox": "复选框",
|
||||
|
||||
@@ -13,10 +13,16 @@
|
||||
"reset_password_subject": "{{appName}}: 重新設置密碼說明",
|
||||
"verify_email_subject": "{{appName}}:驗證您的帳號",
|
||||
"check_your_email": "檢查您的電子郵件",
|
||||
"old_email_address": "舊電子郵件",
|
||||
"new_email_address": "新電子郵件地址",
|
||||
"verify_email_page_body": "我們已傳送電子郵件至 {{email}}。請務必驗證您的電子郵件地址,以確保能準確收到 {{appName}} 遞送的電子郵件和行事曆。",
|
||||
"verify_email_banner_body": "驗證您的電子郵件地址,以確保能準確收到遞送的電子郵件和行事曆",
|
||||
"verify_email_email_header": "驗證您的電子郵件地址",
|
||||
"verify_email_email_button": "驗證電子郵件",
|
||||
"verify_email_change_description": "您最近要求更改您用於登入 {{appName}} 帳戶的電子郵件地址。請點擊下方按鈕以確認您的新電子郵件地址。",
|
||||
"verify_email_change_success_toast": "將您的電子郵件更新為 {{email}}",
|
||||
"verify_email_change_failure_toast": "更新電子郵件失敗。",
|
||||
"change_of_email": "確認您的新 {{appName}} 電子郵件地址",
|
||||
"copy_somewhere_safe": "請將此 API 金鑰記在安全的地方。您之後將無法再次查看此金鑰。",
|
||||
"verify_email_email_body": "請按一下下方的按鈕來驗證電子郵件地址。",
|
||||
"verify_email_by_code_email_body": "請使用下方驗證碼來驗證電子郵件地址。",
|
||||
@@ -182,6 +188,7 @@
|
||||
"2fa_confirm_current_password": "一開始,請確認目前的密碼。",
|
||||
"2fa_scan_image_or_use_code": "使用手機的認證應用程式掃描下方的圖片,或者,手動輸入文字碼也可以。",
|
||||
"text": "文字",
|
||||
"your_phone_number": "您的電話號碼",
|
||||
"multiline_text": "多行文字",
|
||||
"number": "數字",
|
||||
"checkbox": "勾選",
|
||||
@@ -847,6 +854,7 @@
|
||||
"next_step": "跳過步驟",
|
||||
"prev_step": "上個步驟",
|
||||
"install": "安裝",
|
||||
"start_paid_trial": "開始免費試用",
|
||||
"installed": "已安裝",
|
||||
"active_install_one": "{{count}} 個有效安裝",
|
||||
"active_install_other": "{{count}} 個有效安裝",
|
||||
@@ -2075,6 +2083,7 @@
|
||||
"access_bookings": "讀取、編輯、刪除您的預約",
|
||||
"allow_client_to_do": "允許 {{clientName}} 這麼做嗎?",
|
||||
"oauth_access_information": "按一下「允許」後,即表示此應用程式可根據他們的服務和隱私政策使用您的資訊。您可在 {{appName}} App Store 中移除存取權限。",
|
||||
"oauth_form_description": "這是建立新的 OAuth 用戶端的表單。",
|
||||
"allow": "允許",
|
||||
"view_only_edit_availability_not_onboarded": "此使用者尚未完成入門導覽。在對方完成入門導覽前,您將無法設定對方的可預約時間。",
|
||||
"view_only_edit_availability": "您正在查看此使用者的可預約時間。您只能編輯自己的可預約時間。",
|
||||
@@ -2094,8 +2103,11 @@
|
||||
"lock_timezone_toggle_on_booking_page": "鎖定預約頁面的時區",
|
||||
"description_lock_timezone_toggle_on_booking_page": "鎖定預約頁面的時區,安排實體活動時非常實用。",
|
||||
"extensive_whitelabeling": "專屬的入門和工程支援",
|
||||
"create_your_org": "建立你的組織",
|
||||
"need_help": "需要協助?",
|
||||
"user_redirect_description": "在此期間,{{profile.username}} 將代表 {{username}} 負責所有新安排的會議。",
|
||||
"show_more": "顯示更多",
|
||||
"privacy": "隱私權",
|
||||
"team_will_be_under_org": "新的團隊將會在您的組織下。",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 請在此處新增您的字串 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -26,7 +26,9 @@
|
||||
"../../packages/types/next-auth.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts"
|
||||
".next/types/**/*.ts",
|
||||
"../../packages/features/bookings/lib/getBookingInfo.ts",
|
||||
"../../packages/features/bookings/lib/getUserBooking.ts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
+1
-1
@@ -109,7 +109,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"eslint": "^8.34.0",
|
||||
"lucide-react": "^0.171.0",
|
||||
"lucide-react": "^0.363.0",
|
||||
"turbo": "^1.10.1"
|
||||
},
|
||||
"resolutions": {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { SVGComponent } from "@calcom/types/SVGComponent";
|
||||
import { Alert, Button, Dialog, DialogClose, DialogContent, DialogFooter, Input } from "@calcom/ui";
|
||||
import { Link, Search } from "@calcom/ui/components/icon";
|
||||
import type { LucideIcon } from "@calcom/ui/components/icon";
|
||||
|
||||
interface ISearchDialog {
|
||||
isOpenDialog: boolean;
|
||||
@@ -85,7 +86,7 @@ export const SearchDialog = (props: ISearchDialog) => {
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderTab = (Icon: SVGComponent, text: string, mode: Mode) => (
|
||||
const renderTab = (Icon: SVGComponent | LucideIcon, text: string, mode: Mode) => (
|
||||
<div
|
||||
className={classNames(
|
||||
"flex cursor-pointer items-center border-b-2 p-2 text-sm ",
|
||||
|
||||
@@ -24,7 +24,11 @@ export default function ICSFeedSetup() {
|
||||
<div className="flex flex-col space-y-5 md:flex-row md:space-x-5 md:space-y-0">
|
||||
<div>
|
||||
{/* eslint-disable @next/next/no-img-element */}
|
||||
<img src="/api/app-store/ics-feed/icon.svg" alt="ICS Feed" className="h-12 w-12 max-w-2xl" />
|
||||
<img
|
||||
src="/api/app-store/ics-feedcalendar/icon.svg"
|
||||
alt="ICS Feed"
|
||||
className="h-12 w-12 max-w-2xl"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-10/12 flex-col">
|
||||
<h1 className="text-default">{t("connect_ics_feed")}</h1>
|
||||
@@ -34,7 +38,7 @@ export default function ICSFeedSetup() {
|
||||
form={form}
|
||||
handleSubmit={async (_) => {
|
||||
setErrorMessage("");
|
||||
const res = await fetch("/api/integrations/ics-feed/add", {
|
||||
const res = await fetch("/api/integrations/ics-feedcalendar/add", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ urls }),
|
||||
headers: {
|
||||
|
||||
@@ -26,6 +26,11 @@ vi.mock("@calcom/atoms/monorepo", () => ({
|
||||
return false;
|
||||
},
|
||||
}));
|
||||
vi.mock("@calcom/lib", () => ({
|
||||
classNames: (...args: string[]) => {
|
||||
return args.filter(Boolean).join(" ");
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/lib/event-types/getEventTypesByViewer", () => ({}));
|
||||
vi.mock("@calcom/lib/event-types/getEventTypesPublic", () => ({}));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import EnvironmentPlugin from "vite-plugin-environment";
|
||||
|
||||
import viteBaseConfig from "../vite.config";
|
||||
import viteBaseConfig, { embedCoreEnvVars } from "../vite.config";
|
||||
|
||||
const path = require("path");
|
||||
const { defineConfig } = require("vite");
|
||||
@@ -9,6 +9,13 @@ module.exports = defineConfig((configEnv) => {
|
||||
const config = {
|
||||
...viteBaseConfig,
|
||||
base: "/embed/",
|
||||
plugins: [
|
||||
EnvironmentPlugin({
|
||||
EMBED_PUBLIC_EMBED_FINGER_PRINT: embedCoreEnvVars.EMBED_PUBLIC_EMBED_FINGER_PRINT,
|
||||
EMBED_PUBLIC_VERCEL_URL: embedCoreEnvVars.EMBED_PUBLIC_VERCEL_URL,
|
||||
EMBED_PUBLIC_WEBAPP_URL: embedCoreEnvVars.EMBED_PUBLIC_WEBAPP_URL,
|
||||
}),
|
||||
],
|
||||
build: {
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
@@ -17,11 +24,6 @@ module.exports = defineConfig((configEnv) => {
|
||||
embed: path.resolve(__dirname, "src/embed.ts"),
|
||||
},
|
||||
plugins: [
|
||||
EnvironmentPlugin([
|
||||
"EMBED_PUBLIC_EMBED_FINGER_PRINT",
|
||||
"EMBED_PUBLIC_VERCEL_URL",
|
||||
"EMBED_PUBLIC_WEBAPP_URL",
|
||||
]),
|
||||
{
|
||||
generateBundle: (code, bundle) => {
|
||||
// Note: banner/footer doesn't work because it doesn't enclose the entire library code, some variables are still left out.
|
||||
|
||||
@@ -2,13 +2,17 @@ const path = require("path");
|
||||
require("dotenv").config({ path: path.join(__dirname, "..", "..", ".env") });
|
||||
process.env.EMBED_PUBLIC_VERCEL_URL = process.env.VERCEL_URL;
|
||||
process.env.EMBED_PUBLIC_WEBAPP_URL = process.env.NEXT_PUBLIC_WEBAPP_URL;
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
process.env.EMBED_PUBLIC_EMBED_LIB_URL = process.env.NEXT_PUBLIC_EMBED_LIB_URL;
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
process.env.EMBED_PUBLIC_EMBED_FINGER_PRINT = process.env.NEXT_PUBLIC_EMBED_FINGER_PRINT;
|
||||
|
||||
// Problem: typeof process.env.EMBED_PUBLIC_EMBED_LIB_URL is "undefined"(truthy) if process.env.NEXT_PUBLIC_EMBED_LIB_URL is undefined(falsy)
|
||||
// This is probably because environment variables are always string, so this weird automatic conversion to string happens
|
||||
// HACKY Solution
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
if (process.env.EMBED_PUBLIC_EMBED_LIB_URL === "undefined") {
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
delete process.env.EMBED_PUBLIC_EMBED_LIB_URL;
|
||||
}
|
||||
|
||||
@@ -24,3 +28,12 @@ const viteBaseConfig = {
|
||||
envPrefix: "EMBED_PUBLIC_",
|
||||
};
|
||||
export default viteBaseConfig;
|
||||
export const embedCoreEnvVars = {
|
||||
EMBED_PUBLIC_EMBED_FINGER_PRINT: process.env.EMBED_PUBLIC_EMBED_FINGER_PRINT ?? "",
|
||||
EMBED_PUBLIC_VERCEL_URL: process.env.EMBED_PUBLIC_VERCEL_URL ?? "http://localhost:3000",
|
||||
EMBED_PUBLIC_WEBAPP_URL:
|
||||
process.env.EMBED_PUBLIC_WEBAPP_URL ??
|
||||
(process.env.EMBED_PUBLIC_VERCEL_URL
|
||||
? `https://${process.env.EMBED_PUBLIC_VERCEL_URL}`
|
||||
: "http://localhost:3000"),
|
||||
};
|
||||
|
||||
@@ -118,6 +118,8 @@ export type BookerStore = {
|
||||
rescheduleUid: string | null;
|
||||
bookingUid: string | null;
|
||||
bookingData: GetBookingType | null;
|
||||
setBookingData: (bookingData: GetBookingType | null | undefined) => void;
|
||||
|
||||
/**
|
||||
* Method called by booker component to set initial data.
|
||||
*/
|
||||
@@ -317,6 +319,9 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
|
||||
set({ selectedDuration });
|
||||
updateQueryParam("duration", selectedDuration ?? "");
|
||||
},
|
||||
setBookingData: (bookingData: GetBookingType | null | undefined) => {
|
||||
set({ bookingData: bookingData ?? null });
|
||||
},
|
||||
recurringEventCount: null,
|
||||
setRecurringEventCount: (recurringEventCount: number | null) => set({ recurringEventCount }),
|
||||
occurenceCount: null,
|
||||
|
||||
@@ -6,6 +6,7 @@ import classNames from "@calcom/lib/classNames";
|
||||
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Clock, CheckSquare, RefreshCcw } from "@calcom/ui/components/icon";
|
||||
import type { LucideIcon } from "@calcom/ui/components/icon";
|
||||
|
||||
import type { PublicEvent } from "../../types";
|
||||
import { EventDetailBlocks } from "../../types";
|
||||
@@ -34,7 +35,7 @@ type EventDetailCustomBlock = {
|
||||
type EventDetailsProps = EventDetailsPropsBase & (EventDetailDefaultBlock | EventDetailCustomBlock);
|
||||
|
||||
interface EventMetaProps {
|
||||
icon?: React.FC<{ className: string }> | string;
|
||||
icon?: React.FC<{ className: string }> | string | LucideIcon;
|
||||
children: React.ReactNode;
|
||||
// Emphasises the text in the block. For now only
|
||||
// applying in dark mode.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { CreditCard, Zap } from "lucide-react";
|
||||
|
||||
export function getPayIcon(currency: string): React.FC<{ className: string }> | string {
|
||||
import type { LucideIcon } from "@calcom/ui/components/icon";
|
||||
|
||||
export function getPayIcon(currency: string): React.FC<{ className: string }> | string | LucideIcon {
|
||||
return currency !== "BTC" ? CreditCard : Zap;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { CreditCard } from "lucide-react";
|
||||
|
||||
import type { LucideIcon } from "@calcom/ui/components/icon";
|
||||
import { SatSymbol } from "@calcom/ui/components/icon/SatSymbol";
|
||||
|
||||
export function getPriceIcon(currency: string): React.FC<{ className: string }> | string {
|
||||
export function getPriceIcon(currency: string): React.FC<{ className: string }> | string | LucideIcon {
|
||||
return currency !== "BTC" ? CreditCard : (SatSymbol as React.FC<{ className: string }>);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { getBookingWithResponses } from "@calcom/features/bookings/lib/get-booking";
|
||||
|
||||
import getUserBooking from "./getUserBooking";
|
||||
|
||||
const getBookingInfo = async (uid: string) => {
|
||||
const bookingInfoRaw = await getUserBooking(uid);
|
||||
|
||||
if (!bookingInfoRaw) {
|
||||
return { bookingInfoRaw: undefined, bookingInfo: undefined };
|
||||
}
|
||||
|
||||
const bookingInfo = getBookingWithResponses(bookingInfoRaw);
|
||||
|
||||
return { bookingInfoRaw, bookingInfo };
|
||||
};
|
||||
|
||||
export default getBookingInfo;
|
||||
@@ -0,0 +1,60 @@
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
const getUserBooking = async (uid: string) => {
|
||||
const bookingInfo = await prisma.booking.findFirst({
|
||||
where: {
|
||||
uid: uid,
|
||||
},
|
||||
select: {
|
||||
title: true,
|
||||
id: true,
|
||||
uid: true,
|
||||
description: true,
|
||||
customInputs: true,
|
||||
smsReminderNumber: true,
|
||||
recurringEventId: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
location: true,
|
||||
status: true,
|
||||
metadata: true,
|
||||
cancellationReason: true,
|
||||
responses: true,
|
||||
rejectionReason: true,
|
||||
userPrimaryEmail: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
username: true,
|
||||
timeZone: true,
|
||||
},
|
||||
},
|
||||
attendees: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
timeZone: true,
|
||||
},
|
||||
},
|
||||
eventTypeId: true,
|
||||
eventType: {
|
||||
select: {
|
||||
eventName: true,
|
||||
slug: true,
|
||||
timeZone: true,
|
||||
},
|
||||
},
|
||||
seatsReferences: {
|
||||
select: {
|
||||
referenceUid: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return bookingInfo;
|
||||
};
|
||||
|
||||
export default getUserBooking;
|
||||
@@ -6,9 +6,10 @@ import { trpc } from "@calcom/trpc/react";
|
||||
import type { SVGComponent } from "@calcom/types/SVGComponent";
|
||||
import { CreateButtonWithTeamsList, EmptyScreen as ClassicEmptyScreen, showToast } from "@calcom/ui";
|
||||
import { Mail, Smartphone, Zap } from "@calcom/ui/components/icon";
|
||||
import type { LucideIcon } from "@calcom/ui/components/icon";
|
||||
|
||||
type WorkflowExampleType = {
|
||||
Icon: SVGComponent;
|
||||
Icon: SVGComponent | LucideIcon;
|
||||
text: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ const publicEventSelect = Prisma.validator<Prisma.EventTypeSelect>()({
|
||||
eventName: true,
|
||||
slug: true,
|
||||
isInstantEvent: true,
|
||||
aiPhoneCallConfig: true,
|
||||
schedulingType: true,
|
||||
length: true,
|
||||
locations: true,
|
||||
@@ -302,6 +303,7 @@ export const getPublicEvent = async (
|
||||
},
|
||||
isDynamic: false,
|
||||
isInstantEvent: eventWithUserProfiles.isInstantEvent,
|
||||
aiPhoneCallConfig: eventWithUserProfiles.aiPhoneCallConfig,
|
||||
assignAllTeamMembers: event.assignAllTeamMembers,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -58,6 +58,14 @@ export type FormValues = {
|
||||
credentialId?: number;
|
||||
teamName?: string;
|
||||
}[];
|
||||
aiPhoneCallConfig: {
|
||||
generalPrompt: string;
|
||||
enabled: boolean;
|
||||
beginMessage: string;
|
||||
yourPhoneNumber: string;
|
||||
numberToCall: string;
|
||||
guestName: string;
|
||||
};
|
||||
customInputs: CustomInputParsed[];
|
||||
schedule: number | null;
|
||||
periodType: PeriodType;
|
||||
|
||||
@@ -51,7 +51,7 @@ export const TeamsFilter = ({
|
||||
if (selectedTeamsNames) {
|
||||
checkedOptions.push(...selectedTeamsNames);
|
||||
}
|
||||
return `${t("team")}: ${checkedOptions.join(",")}`;
|
||||
return `${checkedOptions.join(",")}`;
|
||||
}
|
||||
if (users) {
|
||||
return t("yours");
|
||||
|
||||
@@ -8,11 +8,12 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
Tooltip,
|
||||
} from "@calcom/ui";
|
||||
import type { LucideIcon } from "@calcom/ui/components/icon";
|
||||
import { Plus, Link, User, Check } from "@calcom/ui/components/icon";
|
||||
|
||||
import { useFilterContext } from "../context/provider";
|
||||
|
||||
type Option = { value: "event-type" | "user"; label: string; StartIcon?: SVGComponent };
|
||||
type Option = { value: "event-type" | "user"; label: string; StartIcon?: SVGComponent | LucideIcon };
|
||||
|
||||
export const FilterType = () => {
|
||||
const { t } = useLocale();
|
||||
|
||||
@@ -79,6 +79,7 @@ import {
|
||||
Tooltip,
|
||||
useCalcomTheme,
|
||||
} from "@calcom/ui";
|
||||
import type { LucideIcon } from "@calcom/ui/components/icon";
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
@@ -100,6 +101,7 @@ import {
|
||||
Settings,
|
||||
User as UserIcon,
|
||||
Users,
|
||||
Sparkles,
|
||||
Zap,
|
||||
Check,
|
||||
} from "@calcom/ui/components/icon";
|
||||
@@ -504,7 +506,7 @@ function UserDropdown({ small }: UserDropdownProps) {
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem
|
||||
type="button"
|
||||
StartIcon={(props) => (
|
||||
StartIcon={(props: { className?: string }) => (
|
||||
<UserIcon className={classNames("text-default", props.className)} aria-hidden="true" />
|
||||
)}
|
||||
href="/settings/my-account/profile">
|
||||
@@ -514,7 +516,7 @@ function UserDropdown({ small }: UserDropdownProps) {
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem
|
||||
type="button"
|
||||
StartIcon={(props) => (
|
||||
StartIcon={(props: { className?: string }) => (
|
||||
<Settings className={classNames("text-default", props.className)} aria-hidden="true" />
|
||||
)}
|
||||
href="/settings/my-account/general">
|
||||
@@ -524,7 +526,7 @@ function UserDropdown({ small }: UserDropdownProps) {
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem
|
||||
type="button"
|
||||
StartIcon={(props) => (
|
||||
StartIcon={(props: { className?: string }) => (
|
||||
<Moon className={classNames("text-default", props.className)} aria-hidden="true" />
|
||||
)}
|
||||
href="/settings/my-account/out-of-office">
|
||||
@@ -549,7 +551,9 @@ function UserDropdown({ small }: UserDropdownProps) {
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem
|
||||
type="button"
|
||||
StartIcon={(props) => <HelpCircle aria-hidden="true" {...props} />}
|
||||
StartIcon={(props: { className?: string }) => (
|
||||
<HelpCircle aria-hidden="true" {...props} />
|
||||
)}
|
||||
onClick={() => setHelpOpen(true)}>
|
||||
{t("help")}
|
||||
</DropdownItem>
|
||||
@@ -565,7 +569,7 @@ function UserDropdown({ small }: UserDropdownProps) {
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem
|
||||
type="button"
|
||||
StartIcon={(props) => <LogOut aria-hidden="true" {...props} />}
|
||||
StartIcon={(props: { className?: string }) => <LogOut aria-hidden="true" {...props} />}
|
||||
onClick={() => signOut({ callbackUrl: "/auth/logout" })}>
|
||||
{t("sign_out")}
|
||||
</DropdownItem>
|
||||
@@ -585,7 +589,7 @@ export type NavigationItemType = {
|
||||
onClick?: React.MouseEventHandler<HTMLAnchorElement | HTMLButtonElement>;
|
||||
target?: HTMLAnchorElement["target"];
|
||||
badge?: React.ReactNode;
|
||||
icon?: SVGComponent;
|
||||
icon?: SVGComponent | LucideIcon;
|
||||
child?: NavigationItemType[];
|
||||
pro?: true;
|
||||
onlyMobile?: boolean;
|
||||
@@ -670,6 +674,11 @@ const navigation: NavigationItemType[] = [
|
||||
icon: FileText,
|
||||
isCurrent: ({ pathname }) => pathname?.startsWith("/apps/routing-forms/") ?? false,
|
||||
},
|
||||
{
|
||||
name: "Cal.ai",
|
||||
href: "/ai",
|
||||
icon: Sparkles,
|
||||
},
|
||||
{
|
||||
name: "workflows",
|
||||
href: "/workflows",
|
||||
|
||||
@@ -2,7 +2,7 @@ import { keepPreviousData } from "@tanstack/react-query";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Plus } from "lucide-react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useCallback, useEffect, useMemo, useReducer, useRef } from "react";
|
||||
import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -115,11 +115,12 @@ export function UserListTable() {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
const { t } = useLocale();
|
||||
const orgBranding = useOrgBranding();
|
||||
|
||||
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
|
||||
const { data, isPending, fetchNextPage, isFetching } =
|
||||
trpc.viewer.organizations.listMembers.useInfiniteQuery(
|
||||
{
|
||||
limit: 10,
|
||||
searchTerm: debouncedSearchTerm,
|
||||
},
|
||||
{
|
||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||
@@ -310,7 +311,7 @@ export function UserListTable() {
|
||||
return (
|
||||
<>
|
||||
<DataTable
|
||||
searchKey="member"
|
||||
onSearch={(value) => setDebouncedSearchTerm(value)}
|
||||
selectionOptions={[
|
||||
{
|
||||
type: "render",
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import { getBookings } from "@calcom/trpc/server/routers/viewer/bookings/get.handler";
|
||||
|
||||
type InputByStatus = "upcoming" | "recurring" | "past" | "cancelled" | "unconfirmed";
|
||||
type GetOptions = {
|
||||
ctx: {
|
||||
user: { id: number; email: string };
|
||||
prisma: PrismaClient;
|
||||
};
|
||||
bookingListingByStatus: InputByStatus;
|
||||
take: number;
|
||||
skip: number;
|
||||
filters: {
|
||||
status: InputByStatus;
|
||||
teamIds?: number[] | undefined;
|
||||
userIds?: number[] | undefined;
|
||||
eventTypeIds?: number[] | undefined;
|
||||
};
|
||||
};
|
||||
|
||||
const getAllUserBookings = async ({ ctx, filters, bookingListingByStatus, take, skip }: GetOptions) => {
|
||||
const { prisma, user } = ctx;
|
||||
|
||||
const bookingListingFilters: Record<typeof bookingListingByStatus, Prisma.BookingWhereInput> = {
|
||||
upcoming: {
|
||||
endTime: { gte: new Date() },
|
||||
// These changes are needed to not show confirmed recurring events,
|
||||
// as rescheduling or cancel for recurring event bookings should be
|
||||
// handled separately for each occurrence
|
||||
OR: [
|
||||
{
|
||||
recurringEventId: { not: null },
|
||||
status: { equals: BookingStatus.ACCEPTED },
|
||||
},
|
||||
{
|
||||
recurringEventId: { equals: null },
|
||||
status: { notIn: [BookingStatus.CANCELLED, BookingStatus.REJECTED] },
|
||||
},
|
||||
],
|
||||
},
|
||||
recurring: {
|
||||
endTime: { gte: new Date() },
|
||||
AND: [
|
||||
{ NOT: { recurringEventId: { equals: null } } },
|
||||
{ status: { notIn: [BookingStatus.CANCELLED, BookingStatus.REJECTED] } },
|
||||
],
|
||||
},
|
||||
past: {
|
||||
endTime: { lte: new Date() },
|
||||
AND: [
|
||||
{ NOT: { status: { equals: BookingStatus.CANCELLED } } },
|
||||
{ NOT: { status: { equals: BookingStatus.REJECTED } } },
|
||||
],
|
||||
},
|
||||
cancelled: {
|
||||
OR: [{ status: { equals: BookingStatus.CANCELLED } }, { status: { equals: BookingStatus.REJECTED } }],
|
||||
},
|
||||
unconfirmed: {
|
||||
endTime: { gte: new Date() },
|
||||
status: { equals: BookingStatus.PENDING },
|
||||
},
|
||||
};
|
||||
const bookingListingOrderby: Record<
|
||||
typeof bookingListingByStatus,
|
||||
Prisma.BookingOrderByWithAggregationInput
|
||||
> = {
|
||||
upcoming: { startTime: "asc" },
|
||||
recurring: { startTime: "asc" },
|
||||
past: { startTime: "desc" },
|
||||
cancelled: { startTime: "desc" },
|
||||
unconfirmed: { startTime: "asc" },
|
||||
};
|
||||
|
||||
const passedBookingsStatusFilter = bookingListingFilters[bookingListingByStatus];
|
||||
const orderBy = bookingListingOrderby[bookingListingByStatus];
|
||||
|
||||
const { bookings, recurringInfo } = await getBookings({
|
||||
user,
|
||||
prisma,
|
||||
passedBookingsStatusFilter,
|
||||
filters: filters,
|
||||
orderBy,
|
||||
take,
|
||||
skip,
|
||||
});
|
||||
|
||||
const bookingsFetched = bookings.length;
|
||||
let nextCursor: typeof skip | null = skip;
|
||||
if (bookingsFetched > take) {
|
||||
nextCursor += bookingsFetched;
|
||||
} else {
|
||||
nextCursor = null;
|
||||
}
|
||||
|
||||
return {
|
||||
bookings,
|
||||
recurringInfo,
|
||||
nextCursor,
|
||||
};
|
||||
};
|
||||
|
||||
export default getAllUserBookings;
|
||||
@@ -84,6 +84,7 @@ export const getEventTypeById = async ({
|
||||
description: true,
|
||||
length: true,
|
||||
isInstantEvent: true,
|
||||
aiPhoneCallConfig: true,
|
||||
offsetStart: true,
|
||||
hidden: true,
|
||||
locations: true,
|
||||
|
||||
@@ -7,3 +7,4 @@ export * from "./validateIntervalLimitOrder";
|
||||
export * from "./schedules";
|
||||
export * from "./event-types/getEventTypesByViewer";
|
||||
export * from "./event-types/getEventTypesPublic";
|
||||
export * from "./bookings/getAllUserBookings";
|
||||
|
||||
@@ -40,6 +40,7 @@ export const eventTypeSelect = Prisma.validator<Prisma.EventTypeSelect>()({
|
||||
slotInterval: true,
|
||||
successRedirectUrl: true,
|
||||
isInstantEvent: true,
|
||||
aiPhoneCallConfig: true,
|
||||
assignAllTeamMembers: true,
|
||||
recurringEvent: true,
|
||||
});
|
||||
|
||||
@@ -25,13 +25,13 @@ export async function getTeamWithMembers(args: {
|
||||
orgSlug?: string | null;
|
||||
includeTeamLogo?: boolean;
|
||||
isTeamView?: boolean;
|
||||
currentOrg?: Team | null;
|
||||
currentOrg?: Pick<Team, "id"> | null;
|
||||
/**
|
||||
* If true, means that you are fetching an organization and not a team
|
||||
*/
|
||||
isOrgView?: boolean;
|
||||
}) {
|
||||
const { id, slug, currentOrg, userId, orgSlug, isTeamView, isOrgView, includeTeamLogo } = args;
|
||||
const { id, slug, currentOrg: _currentOrg, userId, orgSlug, isTeamView, isOrgView, includeTeamLogo } = args;
|
||||
|
||||
// This should improve performance saving already app data found.
|
||||
const appDataMap = new Map();
|
||||
|
||||
@@ -349,7 +349,15 @@ export class ProfileRepository {
|
||||
},
|
||||
movedFromUser: true,
|
||||
organization: {
|
||||
include: {
|
||||
select: {
|
||||
calVideoLogo: true,
|
||||
id: true,
|
||||
logoUrl: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
metadata: true,
|
||||
bannerUrl: true,
|
||||
isPrivate: true,
|
||||
organizationSettings: {
|
||||
select: {
|
||||
lockEventTypeCreationForUsers: true,
|
||||
|
||||
@@ -27,6 +27,7 @@ import { useConnectedCalendars } from "../hooks/useConnectedCalendars";
|
||||
import { useCreateBooking } from "../hooks/useCreateBooking";
|
||||
import { useCreateInstantBooking } from "../hooks/useCreateInstantBooking";
|
||||
import { useCreateRecurringBooking } from "../hooks/useCreateRecurringBooking";
|
||||
import { useGetBookingForReschedule } from "../hooks/useGetBookingForReschedule";
|
||||
import { useHandleBookEvent } from "../hooks/useHandleBookEvent";
|
||||
import { useMe } from "../hooks/useMe";
|
||||
import { usePublicEvent } from "../hooks/usePublicEvent";
|
||||
@@ -54,7 +55,15 @@ type BookerPlatformWrapperAtomProps = Omit<BookerProps, "entity"> & {
|
||||
export const BookerPlatformWrapper = (props: BookerPlatformWrapperAtomProps) => {
|
||||
const [bookerState, setBookerState] = useBookerStore((state) => [state.state, state.setState], shallow);
|
||||
const setSelectedDate = useBookerStore((state) => state.setSelectedDate);
|
||||
const setBookingData = useBookerStore((state) => state.setBookingData);
|
||||
|
||||
const setSelectedTimeslot = useBookerStore((state) => state.setSelectedTimeslot);
|
||||
const { data: booking } = useGetBookingForReschedule({
|
||||
uid: props.rescheduleUid ?? props.bookingUid ?? "",
|
||||
onSuccess: (data) => {
|
||||
setBookingData(data);
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// reset booker whenever it's unmounted
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { ApiResponse, ApiErrorResponse } from "@calcom/platform-types";
|
||||
import type { schemaBookingCancelParams } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import http from "../lib/http";
|
||||
|
||||
interface IUseCancelBooking {
|
||||
onSuccess?: () => void;
|
||||
onError?: (err: ApiErrorResponse | Error) => void;
|
||||
}
|
||||
|
||||
type inputParams = z.infer<typeof schemaBookingCancelParams>;
|
||||
|
||||
export const useCancelBooking = (
|
||||
{ onSuccess, onError }: IUseCancelBooking = {
|
||||
onSuccess: () => {
|
||||
return;
|
||||
},
|
||||
onError: () => {
|
||||
return;
|
||||
},
|
||||
}
|
||||
) => {
|
||||
const cancelBooking = useMutation<ApiResponse, Error, inputParams>({
|
||||
mutationFn: (data) => {
|
||||
return http.post<ApiResponse>(`/ee/bookings/${data.id}/cancel`, data).then((res) => {
|
||||
if (res.data.status === SUCCESS_STATUS) {
|
||||
return res.data;
|
||||
}
|
||||
throw new Error(res.data.error.message);
|
||||
});
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
if (data.status === SUCCESS_STATUS) {
|
||||
onSuccess?.();
|
||||
} else {
|
||||
onError?.(data);
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
onError?.(err);
|
||||
},
|
||||
});
|
||||
return cancelBooking;
|
||||
};
|
||||
@@ -12,7 +12,7 @@ export const useGcal = ({ isAuth }: useGcalProps) => {
|
||||
|
||||
const redirectToGcalOAuth = () => {
|
||||
http
|
||||
?.get("/platform/gcal/oauth/auth-url")
|
||||
?.get("/ee/gcal/oauth/auth-url")
|
||||
.then(({ data: responseBody }) => {
|
||||
if (responseBody.data?.authUrl) {
|
||||
window.location.href = responseBody.data.authUrl;
|
||||
@@ -24,7 +24,7 @@ export const useGcal = ({ isAuth }: useGcalProps) => {
|
||||
useEffect(() => {
|
||||
if (isAuth) {
|
||||
http
|
||||
?.get("/platform/gcal/check")
|
||||
?.get("/ee/gcal/check")
|
||||
.then(() => setAllowConnect(false))
|
||||
.catch(() => setAllowConnect(true))
|
||||
.finally(() => setChecked(true));
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { BASE_URL, API_VERSION, V2_ENDPOINTS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { getBookingInfo } from "@calcom/platform-libraries";
|
||||
import type { ApiResponse, ApiSuccessResponse } from "@calcom/platform-types";
|
||||
|
||||
import http from "../lib/http";
|
||||
|
||||
export const QUERY_KEY = "user-booking";
|
||||
|
||||
export const useGetBooking = (uid = "") => {
|
||||
const endpoint = new URL(BASE_URL);
|
||||
|
||||
endpoint.pathname = `api/${API_VERSION}/${V2_ENDPOINTS.bookings}/${uid}`;
|
||||
|
||||
const bookingQuery = useQuery({
|
||||
queryKey: [QUERY_KEY, uid],
|
||||
queryFn: () => {
|
||||
return http
|
||||
.get<ApiResponse<Awaited<ReturnType<typeof getBookingInfo>>["bookingInfo"]>>(endpoint.toString())
|
||||
.then((res) => {
|
||||
if (res.data.status === SUCCESS_STATUS) {
|
||||
return (res.data as ApiSuccessResponse<Awaited<ReturnType<typeof getBookingInfo>>["bookingInfo"]>)
|
||||
.data;
|
||||
}
|
||||
throw new Error(res.data.error.message);
|
||||
});
|
||||
},
|
||||
enabled: !!uid,
|
||||
});
|
||||
|
||||
return bookingQuery;
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { BASE_URL, API_VERSION, V2_ENDPOINTS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { getBookingForReschedule } from "@calcom/platform-libraries";
|
||||
import type { ApiResponse, ApiSuccessResponse } from "@calcom/platform-types";
|
||||
|
||||
import http from "../lib/http";
|
||||
|
||||
export const QUERY_KEY = "user-booking";
|
||||
|
||||
interface IUseGetBookingForReschedule {
|
||||
onSuccess?: (res: ApiSuccessResponse<Awaited<ReturnType<typeof getBookingForReschedule>>>["data"]) => void;
|
||||
onError?: (err: Error) => void;
|
||||
uid?: string;
|
||||
}
|
||||
export const useGetBookingForReschedule = (
|
||||
props: IUseGetBookingForReschedule = {
|
||||
onSuccess: () => {
|
||||
return;
|
||||
},
|
||||
onError: () => {
|
||||
return;
|
||||
},
|
||||
uid: "",
|
||||
}
|
||||
) => {
|
||||
const endpoint = new URL(BASE_URL);
|
||||
|
||||
endpoint.pathname = `api/${API_VERSION}/${V2_ENDPOINTS.bookings}/${props.uid}/reschedule`;
|
||||
|
||||
const bookingQuery = useQuery({
|
||||
queryKey: [QUERY_KEY, props.uid],
|
||||
queryFn: () => {
|
||||
return http
|
||||
.get<ApiResponse<Awaited<ReturnType<typeof getBookingForReschedule>>>>(endpoint.toString())
|
||||
.then((res) => {
|
||||
if (res.data.status === SUCCESS_STATUS) {
|
||||
props.onSuccess?.(
|
||||
(res.data as ApiSuccessResponse<Awaited<ReturnType<typeof getBookingForReschedule>>>).data
|
||||
);
|
||||
return (res.data as ApiSuccessResponse<Awaited<ReturnType<typeof getBookingForReschedule>>>).data;
|
||||
}
|
||||
const error = new Error(res.data.error.message);
|
||||
props.onError?.(error);
|
||||
throw error;
|
||||
})
|
||||
.catch((err) => {
|
||||
props.onError?.(err);
|
||||
});
|
||||
},
|
||||
enabled: !!props?.uid,
|
||||
});
|
||||
|
||||
return bookingQuery;
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { BASE_URL, SUCCESS_STATUS, API_VERSION, V2_ENDPOINTS } from "@calcom/platform-constants";
|
||||
import type { getAllUserBookings } from "@calcom/platform-libraries";
|
||||
import type { ApiResponse } from "@calcom/platform-types";
|
||||
import type { GetBookingsInput } from "@calcom/platform-types/bookings";
|
||||
|
||||
import http from "../lib/http";
|
||||
|
||||
export const QUERY_KEY = "user-bookings";
|
||||
|
||||
export const useGetBookings = (input: GetBookingsInput) => {
|
||||
const endpoint = new URL(BASE_URL);
|
||||
|
||||
endpoint.pathname = `api/${API_VERSION}/${V2_ENDPOINTS.bookings}`;
|
||||
|
||||
const bookingsQuery = useQuery({
|
||||
queryKey: [QUERY_KEY, input?.limit ?? 50, input?.cursor ?? 0, input?.filters?.status ?? "upcoming"],
|
||||
queryFn: () => {
|
||||
return http
|
||||
.get<ApiResponse<Awaited<ReturnType<typeof getAllUserBookings>>>>(endpoint.toString(), {
|
||||
params: input,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.data.status === SUCCESS_STATUS) {
|
||||
return res.data.data;
|
||||
}
|
||||
throw new Error(res.data.error.message);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return bookingsQuery;
|
||||
};
|
||||
@@ -7,7 +7,7 @@ import http from "../lib/http";
|
||||
const useGetCityTimezones = () => {
|
||||
const endpoint = new URL(BASE_URL);
|
||||
|
||||
endpoint.pathname = `api/${API_VERSION}/schedules/time-zones`;
|
||||
endpoint.pathname = `api/${API_VERSION}/timezones`;
|
||||
|
||||
const { isLoading, data } = useQuery({
|
||||
queryKey: ["city-timezones"],
|
||||
|
||||
@@ -30,7 +30,7 @@ export const useOAuthClient = ({ clientId, apiUrl, refreshUrl, onError, onSucces
|
||||
useEffect(() => {
|
||||
if (clientId && http.getUrl() && prevClientId !== clientId) {
|
||||
try {
|
||||
http.get<ApiResponse>(`/platform/provider/${clientId}`).catch((err: AxiosError) => {
|
||||
http.get<ApiResponse>(`/ee/provider/${clientId}`).catch((err: AxiosError) => {
|
||||
if (err.response?.status === 401) {
|
||||
onError("Invalid oAuth Client.");
|
||||
} else {
|
||||
|
||||
@@ -58,7 +58,7 @@ export const useOAuthFlow = ({ accessToken, refreshUrl, clientId, onError, onSuc
|
||||
http.setAuthorizationHeader(accessToken);
|
||||
try {
|
||||
http
|
||||
.get<ApiResponse>(`/platform/provider/${clientId}/access-token`)
|
||||
.get<ApiResponse>(`/ee/provider/${clientId}/access-token`)
|
||||
.catch(async (err: AxiosError) => {
|
||||
if ((err.response?.status === 498 || err.response?.status === 401) && refreshUrl) {
|
||||
setIsRefreshing(true);
|
||||
|
||||
@@ -6,3 +6,6 @@ export { useIsPlatform } from "./hooks/useIsPlatform";
|
||||
export { useAtomsContext } from "./hooks/useAtomsContext";
|
||||
export { useConnectedCalendars } from "./hooks/useConnectedCalendars";
|
||||
export { useEventTypesPublic } from "./hooks/event-types/useEventTypesPublic";
|
||||
export { useCancelBooking } from "./hooks/useCancelBooking";
|
||||
export { useGetBooking } from "./hooks/useGetBooking";
|
||||
export { useGetBookings } from "./hooks/useGetBookings";
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
"@tanstack/react-query": "^5.17.15",
|
||||
"class-variance-authority": "^0.4.0",
|
||||
"clsx": "^2.0.0",
|
||||
"lucide-react": "^0.171.0",
|
||||
"lucide-react": "^0.363.0",
|
||||
"react-use": "^17.4.2",
|
||||
"tailwind-merge": "^1.13.2",
|
||||
"tailwindcss": "^3.3.3",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user