Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90f55670dc | ||
|
|
9c53f392ca | ||
|
|
cc19a81875 | ||
|
|
67032ddaf1 | ||
|
|
2fe71c3695 | ||
|
|
21422fe027 | ||
|
|
95575291ff | ||
|
|
3e61a0633d | ||
|
|
85c9a790f2 | ||
|
|
e17d55457f | ||
|
|
d8d8bc4d00 | ||
|
|
55cc1aea59 | ||
|
|
ae383164bc | ||
|
|
0d8863fe8e | ||
|
|
51f6654635 | ||
|
|
fa11e10bcb | ||
|
|
a8f5bd6fe9 | ||
|
|
a1c725bb35 | ||
|
|
e4eeb11a61 | ||
|
|
3b0bb25251 | ||
|
|
d27490e6f2 | ||
|
|
b3c191b553 | ||
|
|
0b977e615d |
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
undecided:
|
||||
- "@calcom/prisma"
|
||||
Vendored
Vendored
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
@@ -38,8 +38,7 @@
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.231",
|
||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.236",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
"@calcom/prisma": "*",
|
||||
|
||||
+24
-5
@@ -18,6 +18,7 @@ import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import {
|
||||
@@ -33,13 +34,19 @@ import {
|
||||
HttpStatus,
|
||||
Delete,
|
||||
Query,
|
||||
Headers,
|
||||
InternalServerErrorException,
|
||||
ParseIntPipe,
|
||||
} from "@nestjs/common";
|
||||
import { ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
|
||||
|
||||
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { getPublicEvent, getEventTypesByViewer } from "@calcom/platform-libraries-0.0.2";
|
||||
import {
|
||||
EVENT_TYPE_READ,
|
||||
EVENT_TYPE_WRITE,
|
||||
SUCCESS_STATUS,
|
||||
X_CAL_CLIENT_ID,
|
||||
} from "@calcom/platform-constants";
|
||||
import { getPublicEvent, getEventTypesByViewer } from "@calcom/platform-libraries/event-types";
|
||||
import { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
@Controller({
|
||||
@@ -51,7 +58,8 @@ import { PrismaClient } from "@calcom/prisma";
|
||||
export class EventTypesController_2024_04_15 {
|
||||
constructor(
|
||||
private readonly eventTypesService: EventTypesService_2024_04_15,
|
||||
private readonly prismaReadService: PrismaReadService
|
||||
private readonly prismaReadService: PrismaReadService,
|
||||
private readonly organizationsRepository: OrganizationsRepository
|
||||
) {}
|
||||
|
||||
@Post("/")
|
||||
@@ -109,19 +117,30 @@ export class EventTypesController_2024_04_15 {
|
||||
async getPublicEventType(
|
||||
@Param("username") username: string,
|
||||
@Param("eventSlug") eventSlug: string,
|
||||
@Query() queryParams: GetPublicEventTypeQueryParams_2024_04_15
|
||||
@Query() queryParams: GetPublicEventTypeQueryParams_2024_04_15,
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string
|
||||
): Promise<GetEventTypePublicOutput> {
|
||||
try {
|
||||
let orgSlug = queryParams.org;
|
||||
|
||||
if (clientId && !orgSlug && username.includes(`-${clientId}`)) {
|
||||
const org = await this.organizationsRepository.findTeamIdAndSlugFromClientId(clientId).catch(() => null);
|
||||
if (org) {
|
||||
orgSlug = org.slug;
|
||||
}
|
||||
}
|
||||
|
||||
const event = await getPublicEvent(
|
||||
username.toLowerCase(),
|
||||
eventSlug,
|
||||
queryParams.isTeamEvent,
|
||||
queryParams.org || null,
|
||||
orgSlug ?? null,
|
||||
this.prismaReadService.prisma as unknown as PrismaClient,
|
||||
// We should be fine allowing unpublished orgs events to be servable through platform because Platform access is behind license
|
||||
// If there is ever a need to restrict this, we can introduce a new query param `fromRedirectOfNonOrgLink`
|
||||
true
|
||||
);
|
||||
|
||||
return {
|
||||
data: event,
|
||||
status: SUCCESS_STATUS,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { EventTypesController_2024_04_15 } from "@/ee/event-types/event-types_20
|
||||
import { EventTypesRepository_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.repository";
|
||||
import { EventTypesService_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/services/event-types.service";
|
||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { SelectedCalendarsModule } from "@/modules/selected-calendars/selected-calendars.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
@@ -9,7 +10,14 @@ import { UsersModule } from "@/modules/users/users.module";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, MembershipsModule, TokensModule, UsersModule, SelectedCalendarsModule],
|
||||
imports: [
|
||||
PrismaModule,
|
||||
MembershipsModule,
|
||||
TokensModule,
|
||||
UsersModule,
|
||||
SelectedCalendarsModule,
|
||||
OrganizationsModule,
|
||||
],
|
||||
providers: [EventTypesRepository_2024_04_15, EventTypesService_2024_04_15],
|
||||
controllers: [EventTypesController_2024_04_15],
|
||||
exports: [EventTypesService_2024_04_15, EventTypesRepository_2024_04_15],
|
||||
|
||||
+9
-1
@@ -1721,7 +1721,7 @@ describe("Event types Endpoints", () => {
|
||||
description: "undefined booking fields",
|
||||
length: 40,
|
||||
hidden: false,
|
||||
slug: "undefined-booking-fields",
|
||||
slug: `undefined-booking-fields-${randomString()}`,
|
||||
locations: [],
|
||||
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||
};
|
||||
@@ -1751,6 +1751,14 @@ describe("Event types Endpoints", () => {
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
disableOnPrefill: false,
|
||||
hidden: true,
|
||||
isDefault: true,
|
||||
required: false,
|
||||
slug: "attendeePhoneNumber",
|
||||
type: "phone",
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "radioInput",
|
||||
|
||||
+9
-11
@@ -46,11 +46,10 @@ export class SchedulesController_2024_04_15 {
|
||||
@Body() bodySchedule: CreateScheduleInput_2024_04_15
|
||||
): Promise<CreateScheduleOutput_2024_04_15> {
|
||||
const schedule = await this.schedulesService.createUserSchedule(user.id, bodySchedule);
|
||||
const scheduleFormatted = await this.schedulesService.formatScheduleForAtom(user, schedule);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: scheduleFormatted,
|
||||
data: schedule,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -60,13 +59,10 @@ export class SchedulesController_2024_04_15 {
|
||||
@GetUser() user: UserWithProfile
|
||||
): Promise<GetDefaultScheduleOutput_2024_04_15 | null> {
|
||||
const schedule = await this.schedulesService.getUserScheduleDefault(user.id);
|
||||
const scheduleFormatted = schedule
|
||||
? await this.schedulesService.formatScheduleForAtom(user, schedule)
|
||||
: null;
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: scheduleFormatted,
|
||||
data: schedule,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -77,23 +73,25 @@ export class SchedulesController_2024_04_15 {
|
||||
@Param("scheduleId") scheduleId: number
|
||||
): Promise<GetScheduleOutput_2024_04_15> {
|
||||
const schedule = await this.schedulesService.getUserSchedule(user.id, scheduleId);
|
||||
const scheduleFormatted = await this.schedulesService.formatScheduleForAtom(user, schedule);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: scheduleFormatted,
|
||||
data: schedule,
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/")
|
||||
@Permissions([SCHEDULE_READ])
|
||||
async getSchedules(@GetUser() user: UserWithProfile): Promise<GetSchedulesOutput_2024_04_15> {
|
||||
const schedules = await this.schedulesService.getUserSchedules(user.id);
|
||||
const schedulesFormatted = await this.schedulesService.formatSchedulesForAtom(user, schedules);
|
||||
const schedules = await this.schedulesService.getUserSchedules(
|
||||
user.id,
|
||||
user.timeZone,
|
||||
user.defaultScheduleId
|
||||
);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: schedulesFormatted,
|
||||
data: schedules,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
import { CreateAvailabilityInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-availability.input";
|
||||
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||
import { ScheduleOutput } from "@/ee/schedules/schedules_2024_04_15/outputs/schedule.output";
|
||||
import { SchedulesRepository_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.repository";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
||||
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { Schedule } from "@prisma/client";
|
||||
import { User } from "@prisma/client";
|
||||
|
||||
import type { ScheduleWithAvailabilities } from "@calcom/platform-libraries-0.0.2";
|
||||
import { updateScheduleHandler } from "@calcom/platform-libraries-0.0.2";
|
||||
import {
|
||||
transformWorkingHoursForClient,
|
||||
transformAvailabilityForClient,
|
||||
transformDateOverridesForClient,
|
||||
} from "@calcom/platform-libraries-0.0.2";
|
||||
import { updateSchedule } from "@calcom/platform-libraries/schedules";
|
||||
import { ScheduleRepository } from "@calcom/platform-libraries/schedules";
|
||||
import { UpdateScheduleInput_2024_04_15 } from "@calcom/platform-types";
|
||||
import { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
@Injectable()
|
||||
export class SchedulesService_2024_04_15 {
|
||||
constructor(
|
||||
private readonly schedulesRepository: SchedulesRepository_2024_04_15,
|
||||
private readonly usersRepository: UsersRepository
|
||||
private readonly usersRepository: UsersRepository,
|
||||
private readonly dbWrite: PrismaWriteService
|
||||
) {}
|
||||
|
||||
async createUserDefaultSchedule(userId: number, timeZone: string) {
|
||||
@@ -48,7 +44,9 @@ export class SchedulesService_2024_04_15 {
|
||||
await this.usersRepository.setDefaultSchedule(userId, createdSchedule.id);
|
||||
}
|
||||
|
||||
return createdSchedule;
|
||||
const formattedSchedule = await this.getUserSchedule(userId, createdSchedule.id);
|
||||
|
||||
return formattedSchedule;
|
||||
}
|
||||
|
||||
async getUserScheduleDefault(userId: number) {
|
||||
@@ -56,11 +54,29 @@ export class SchedulesService_2024_04_15 {
|
||||
|
||||
if (!user?.defaultScheduleId) return null;
|
||||
|
||||
return this.schedulesRepository.getScheduleById(user.defaultScheduleId);
|
||||
return await ScheduleRepository.findDetailedScheduleById({
|
||||
scheduleId: user.defaultScheduleId,
|
||||
isManagedEventType: undefined,
|
||||
userId,
|
||||
timeZone: user.timeZone,
|
||||
defaultScheduleId: user.defaultScheduleId,
|
||||
});
|
||||
}
|
||||
|
||||
async getUserSchedule(userId: number, scheduleId: number) {
|
||||
const existingSchedule = await this.schedulesRepository.getScheduleById(scheduleId);
|
||||
const user = await this.usersRepository.findById(userId);
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException(`User with ID=${userId} does not exist.`);
|
||||
}
|
||||
|
||||
const existingSchedule = await ScheduleRepository.findDetailedScheduleById({
|
||||
scheduleId: scheduleId,
|
||||
isManagedEventType: undefined,
|
||||
userId,
|
||||
timeZone: user.timeZone,
|
||||
defaultScheduleId: user.defaultScheduleId,
|
||||
});
|
||||
|
||||
if (!existingSchedule) {
|
||||
throw new NotFoundException(`Schedule with ID=${scheduleId} does not exist.`);
|
||||
@@ -71,8 +87,8 @@ export class SchedulesService_2024_04_15 {
|
||||
return existingSchedule;
|
||||
}
|
||||
|
||||
async getUserSchedules(userId: number) {
|
||||
return this.schedulesRepository.getSchedulesByUserId(userId);
|
||||
async getUserSchedules(userId: number, timeZone: string, defaultScheduleId: number | null) {
|
||||
return ScheduleRepository.findManyDetailedScheduleByUserId({ userId, timeZone, defaultScheduleId });
|
||||
}
|
||||
|
||||
async updateUserSchedule(
|
||||
@@ -89,7 +105,6 @@ export class SchedulesService_2024_04_15 {
|
||||
this.checkUserOwnsSchedule(user.id, existingSchedule);
|
||||
|
||||
const schedule = await this.getUserSchedule(user.id, Number(scheduleId));
|
||||
const scheduleFormatted = await this.formatScheduleForAtom(user, schedule);
|
||||
|
||||
if (!bodySchedule.schedule) {
|
||||
// note(Lauris): When updating an availability in cal web app, lets say only its name, also
|
||||
@@ -97,12 +112,16 @@ export class SchedulesService_2024_04_15 {
|
||||
// and they have same shape, so to match shapes I attach "scheduleFormatted.availability" to reflect
|
||||
// schedule that would be passed by the web app. If we don't, then updating schedule name will erase
|
||||
// schedule.
|
||||
bodySchedule.schedule = scheduleFormatted.availability;
|
||||
bodySchedule.schedule = schedule.availability;
|
||||
}
|
||||
|
||||
return updateScheduleHandler({
|
||||
input: { scheduleId: Number(scheduleId), ...bodySchedule },
|
||||
ctx: { user },
|
||||
return updateSchedule({
|
||||
input: {
|
||||
scheduleId: Number(scheduleId),
|
||||
...bodySchedule,
|
||||
},
|
||||
user,
|
||||
prisma: this.dbWrite.prisma as unknown as PrismaClient,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -118,44 +137,6 @@ export class SchedulesService_2024_04_15 {
|
||||
return this.schedulesRepository.deleteScheduleById(scheduleId);
|
||||
}
|
||||
|
||||
async formatScheduleForAtom(user: User, schedule: ScheduleWithAvailabilities): Promise<ScheduleOutput> {
|
||||
const usersSchedulesCount = await this.schedulesRepository.getUserSchedulesCount(user.id);
|
||||
return this.transformScheduleForAtom(schedule, usersSchedulesCount, user);
|
||||
}
|
||||
|
||||
async formatSchedulesForAtom(
|
||||
user: User,
|
||||
schedules: ScheduleWithAvailabilities[]
|
||||
): Promise<ScheduleOutput[]> {
|
||||
const usersSchedulesCount = await this.schedulesRepository.getUserSchedulesCount(user.id);
|
||||
return Promise.all(
|
||||
schedules.map((schedule) => this.transformScheduleForAtom(schedule, usersSchedulesCount, user))
|
||||
);
|
||||
}
|
||||
|
||||
async transformScheduleForAtom(
|
||||
schedule: ScheduleWithAvailabilities,
|
||||
userSchedulesCount: number,
|
||||
user: Pick<User, "id" | "defaultScheduleId" | "timeZone">
|
||||
): Promise<ScheduleOutput> {
|
||||
const timeZone = schedule.timeZone || user.timeZone;
|
||||
const defaultSchedule = await this.getUserScheduleDefault(user.id);
|
||||
|
||||
return {
|
||||
id: schedule.id,
|
||||
name: schedule.name,
|
||||
isManaged: schedule.userId !== user.id,
|
||||
workingHours: transformWorkingHoursForClient(schedule),
|
||||
schedule: schedule.availability,
|
||||
availability: transformAvailabilityForClient(schedule),
|
||||
timeZone,
|
||||
dateOverrides: transformDateOverridesForClient(schedule, timeZone),
|
||||
isDefault: defaultSchedule?.id === schedule.id,
|
||||
isLastSchedule: userSchedulesCount <= 1,
|
||||
readOnly: schedule.userId !== user.id,
|
||||
};
|
||||
}
|
||||
|
||||
checkUserOwnsSchedule(userId: number, schedule: Pick<Schedule, "id" | "userId">) {
|
||||
if (userId !== schedule.userId) {
|
||||
throw new ForbiddenException(`User with ID=${userId} does not own schedule with ID=${schedule.id}`);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { extractUserContext } from "@/lib/extract-user-context";
|
||||
import { filterReqHeaders } from "@/lib/filterReqHeaders";
|
||||
import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
@@ -39,6 +40,7 @@ export class CalendarServiceExceptionFilter implements ExceptionFilter {
|
||||
const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id";
|
||||
response.setHeader("X-Request-Id", requestId.toString());
|
||||
|
||||
const userContext = extractUserContext(request);
|
||||
this.logger.error(`Calendar Service Exception Filter: ${exception?.message}`, {
|
||||
exception,
|
||||
body: request.body,
|
||||
@@ -46,6 +48,7 @@ export class CalendarServiceExceptionFilter implements ExceptionFilter {
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
requestId,
|
||||
...userContext,
|
||||
});
|
||||
|
||||
response.status(400).json({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { extractUserContext } from "@/lib/extract-user-context";
|
||||
import { filterReqHeaders } from "@/lib/filterReqHeaders";
|
||||
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, Logger } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
@@ -16,6 +17,7 @@ export class HttpExceptionFilter implements ExceptionFilter<HttpException> {
|
||||
const statusCode = exception.getStatus();
|
||||
const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id";
|
||||
response.setHeader("X-Request-Id", requestId.toString());
|
||||
const userContext = extractUserContext(request);
|
||||
this.logger.error(`Http Exception Filter: ${exception?.message}`, {
|
||||
exception,
|
||||
body: request.body,
|
||||
@@ -23,6 +25,7 @@ export class HttpExceptionFilter implements ExceptionFilter<HttpException> {
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
requestId,
|
||||
...userContext,
|
||||
});
|
||||
|
||||
response.status(statusCode).json({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { extractUserContext } from "@/lib/extract-user-context";
|
||||
import { filterReqHeaders } from "@/lib/filterReqHeaders";
|
||||
import type { ArgumentsHost, ExceptionFilter } from "@nestjs/common";
|
||||
import { Catch, HttpStatus, Logger } from "@nestjs/common";
|
||||
@@ -43,6 +44,7 @@ export class PrismaExceptionFilter implements ExceptionFilter {
|
||||
const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id";
|
||||
response.setHeader("X-Request-Id", requestId.toString());
|
||||
|
||||
const userContext = extractUserContext(request);
|
||||
this.logger.error(`PrismaError: ${error.message}`, {
|
||||
error,
|
||||
body: request.body,
|
||||
@@ -50,6 +52,7 @@ export class PrismaExceptionFilter implements ExceptionFilter {
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
requestId,
|
||||
...userContext,
|
||||
});
|
||||
|
||||
let message = "There was an error, please try again later.";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { extractUserContext } from "@/lib/extract-user-context";
|
||||
import { filterReqHeaders } from "@/lib/filterReqHeaders";
|
||||
import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
@@ -120,6 +121,7 @@ export class TRPCExceptionFilter implements ExceptionFilter {
|
||||
const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id";
|
||||
response.setHeader("X-Request-Id", requestId.toString());
|
||||
|
||||
const userContext = extractUserContext(request);
|
||||
this.logger.error(`TRPC Exception Filter: ${exception?.message}`, {
|
||||
exception,
|
||||
body: request.body,
|
||||
@@ -127,6 +129,7 @@ export class TRPCExceptionFilter implements ExceptionFilter {
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
requestId,
|
||||
...userContext,
|
||||
});
|
||||
|
||||
response.status(statusCode).json({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { extractUserContext } from "@/lib/extract-user-context";
|
||||
import { filterReqHeaders } from "@/lib/filterReqHeaders";
|
||||
import type { ArgumentsHost, ExceptionFilter } from "@nestjs/common";
|
||||
import { Catch, HttpStatus, Logger } from "@nestjs/common";
|
||||
@@ -18,6 +19,7 @@ export class ZodExceptionFilter implements ExceptionFilter {
|
||||
const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id";
|
||||
response.setHeader("X-Request-Id", requestId.toString());
|
||||
|
||||
const userContext = extractUserContext(request);
|
||||
this.logger.error(`ZodError: ${error.message}`, {
|
||||
error,
|
||||
body: request.body,
|
||||
@@ -25,6 +27,7 @@ export class ZodExceptionFilter implements ExceptionFilter {
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
requestId,
|
||||
...userContext,
|
||||
});
|
||||
|
||||
response.status(HttpStatus.BAD_REQUEST).json({
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Request } from "express";
|
||||
|
||||
import { Team } from "@calcom/prisma/client";
|
||||
|
||||
import { ApiAuthGuardUser } from "../modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
|
||||
export interface UserContext {
|
||||
userId?: number;
|
||||
userEmail?: string;
|
||||
organizationId?: number;
|
||||
teamId?: number;
|
||||
}
|
||||
|
||||
export function extractUserContext(request: Request): UserContext {
|
||||
const context: UserContext = {};
|
||||
|
||||
const user = (request as any).user as ApiAuthGuardUser | undefined;
|
||||
if (user) {
|
||||
context.userId = user.id;
|
||||
context.userEmail = user.email;
|
||||
}
|
||||
|
||||
const organizationId = (request as any).organizationId as number | undefined;
|
||||
if (organizationId) {
|
||||
context.organizationId = organizationId;
|
||||
}
|
||||
|
||||
const team = (request as any).team as Team | undefined;
|
||||
if (team) {
|
||||
context.teamId = team.id;
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
@@ -69,7 +69,7 @@ export class OrganizationsRepository {
|
||||
return id;
|
||||
}
|
||||
|
||||
async findTeamIdFromClientId(clientId: string) {
|
||||
async findTeamIdAndSlugFromClientId(clientId: string) {
|
||||
return this.dbRead.prisma.team.findFirstOrThrow({
|
||||
where: {
|
||||
platformOAuthClient: {
|
||||
@@ -80,6 +80,7 @@ export class OrganizationsRepository {
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
slug: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -117,8 +117,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
// A booking that has been rescheduled to a new booking will also have a status of CANCELLED
|
||||
const isDisabledRescheduling = booking.eventType?.disableRescheduling;
|
||||
// This comes from query param and thus is considered forced
|
||||
const canRescheduleCancelledBooking =
|
||||
isForcedRescheduleForCancelledBooking || booking.eventType?.allowReschedulingCancelledBookings;
|
||||
const canBookThroughCancelledBookingRescheduleLink = booking.eventType?.allowReschedulingCancelledBookings;
|
||||
const isNonRescheduleableBooking =
|
||||
booking.status === BookingStatus.CANCELLED || booking.status === BookingStatus.REJECTED;
|
||||
|
||||
@@ -131,8 +130,9 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
};
|
||||
}
|
||||
|
||||
if (isNonRescheduleableBooking) {
|
||||
const canReschedule = booking.status === BookingStatus.CANCELLED && canRescheduleCancelledBooking;
|
||||
if (isNonRescheduleableBooking && !isForcedRescheduleForCancelledBooking) {
|
||||
const canReschedule =
|
||||
booking.status === BookingStatus.CANCELLED && canBookThroughCancelledBookingRescheduleLink;
|
||||
return {
|
||||
redirect: {
|
||||
destination: canReschedule ? eventUrl : `/booking/${uid}`,
|
||||
|
||||
@@ -652,8 +652,9 @@ const ProfileForm = ({
|
||||
/>
|
||||
</div>
|
||||
{extraField}
|
||||
<p className="text-subtle mt-1 flex items-center gap-1 text-sm">
|
||||
<Icon name="info" /> {t("tip_username_plus")}
|
||||
<p className="text-subtle mt-1 flex gap-1 text-sm">
|
||||
<Icon name="info" className="mt-0.5 flex-shrink-0" />
|
||||
<span className="flex-1">{t("tip_username_plus")}</span>
|
||||
</p>
|
||||
<div className="mt-6">
|
||||
<TextField label={t("full_name")} {...formMethods.register("name")} />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "5.4.9",
|
||||
"version": "5.4.11",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function Router({ form, message, errorMessage }: inferSSRProps<ty
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{form.name} | Cal.com Forms</title>
|
||||
<title>{form?.name} | Cal.com Forms</title>
|
||||
</Head>
|
||||
<div className="mx-auto my-0 max-w-3xl md:my-24">
|
||||
<div className="w-full max-w-4xl ltr:mr-2 rtl:ml-2">
|
||||
|
||||
@@ -1,11 +1,30 @@
|
||||
import { wrapGetServerSidePropsWithSentry } from "@sentry/nextjs";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import { getRoutedUrl } from "@calcom/lib/server/getRoutedUrl";
|
||||
import { getRoutedUrl, hasEmbedPath } from "@calcom/lib/server/getRoutedUrl";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
export const getServerSideProps = wrapGetServerSidePropsWithSentry(async function getServerSideProps(
|
||||
context: GetServerSidePropsContext
|
||||
) {
|
||||
return await getRoutedUrl(context);
|
||||
try {
|
||||
return await getRoutedUrl(context);
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError && error.code === "TOO_MANY_REQUESTS") {
|
||||
context.res.statusCode = 429;
|
||||
const isEmbed = hasEmbedPath(context.req.url || "");
|
||||
|
||||
return {
|
||||
props: {
|
||||
isEmbed,
|
||||
form: null,
|
||||
message: null,
|
||||
errorMessage: error.message,
|
||||
},
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
"/router");
|
||||
|
||||
+3170
-765
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1,114 @@
|
||||
import { oauth2_v2 } from "@googleapis/oauth2";
|
||||
import type { OAuth2Client } from "googleapis-common";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { uploadAvatar } from "@calcom/lib/server/avatar";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image";
|
||||
|
||||
import { updateProfilePhotoGoogle } from "./updateProfilePhotoGoogle";
|
||||
|
||||
vi.mock("@googleapis/oauth2", () => ({
|
||||
oauth2_v2: {
|
||||
Oauth2: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/lib/logger", () => ({
|
||||
default: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/lib/server/avatar", () => ({
|
||||
uploadAvatar: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/lib/server/resizeBase64Image", () => ({
|
||||
resizeBase64Image: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/lib/server/repository/user", () => ({
|
||||
UserRepository: {
|
||||
updateAvatar: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe("updateProfilePhotoGoogle", () => {
|
||||
const mockOAuth2Client = {} as OAuth2Client;
|
||||
const userId = 123;
|
||||
|
||||
let mockOauth2Instance: any;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
mockOauth2Instance = {
|
||||
userinfo: {
|
||||
get: vi.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
(oauth2_v2.Oauth2 as any).mockImplementation(() => mockOauth2Instance);
|
||||
(uploadAvatar as any).mockResolvedValue("/api/avatar/processed-123.png");
|
||||
(resizeBase64Image as any).mockResolvedValue("data:image/png;base64,resized-data");
|
||||
(UserRepository.updateAvatar as any).mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it("should update avatar with valid URL", async () => {
|
||||
const validUrl = "https://example.com/avatar.jpg";
|
||||
mockOauth2Instance.userinfo.get.mockResolvedValue({
|
||||
data: { picture: validUrl },
|
||||
});
|
||||
|
||||
await updateProfilePhotoGoogle(mockOAuth2Client, userId);
|
||||
|
||||
expect(UserRepository.updateAvatar).toHaveBeenCalledWith({
|
||||
id: userId,
|
||||
avatarUrl: validUrl,
|
||||
});
|
||||
expect(uploadAvatar).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should process base64 data", async () => {
|
||||
const base64Data =
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=";
|
||||
mockOauth2Instance.userinfo.get.mockResolvedValue({
|
||||
data: { picture: base64Data },
|
||||
});
|
||||
|
||||
await updateProfilePhotoGoogle(mockOAuth2Client, userId);
|
||||
|
||||
expect(resizeBase64Image).toHaveBeenCalledWith(base64Data);
|
||||
expect(uploadAvatar).toHaveBeenCalledWith({
|
||||
avatar: "data:image/png;base64,resized-data",
|
||||
userId,
|
||||
});
|
||||
expect(UserRepository.updateAvatar).toHaveBeenCalledWith({
|
||||
id: userId,
|
||||
avatarUrl: "/api/avatar/processed-123.png",
|
||||
});
|
||||
});
|
||||
|
||||
it("should do nothing when no picture provided", async () => {
|
||||
mockOauth2Instance.userinfo.get.mockResolvedValue({
|
||||
data: {},
|
||||
});
|
||||
|
||||
await updateProfilePhotoGoogle(mockOAuth2Client, userId);
|
||||
|
||||
expect(UserRepository.updateAvatar).not.toHaveBeenCalled();
|
||||
expect(uploadAvatar).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle errors gracefully", async () => {
|
||||
const error = new Error("OAuth API failed");
|
||||
mockOauth2Instance.userinfo.get.mockRejectedValue(error);
|
||||
|
||||
await updateProfilePhotoGoogle(mockOAuth2Client, userId);
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith("Error updating avatarUrl from google calendar connect", error);
|
||||
expect(UserRepository.updateAvatar).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,16 +1,32 @@
|
||||
import { oauth2_v2 } from "@googleapis/oauth2";
|
||||
import type { OAuth2Client } from "googleapis-common";
|
||||
|
||||
import { isBase64Image } from "@calcom/lib/isBase64Image";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { uploadAvatar } from "@calcom/lib/server/avatar";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image";
|
||||
|
||||
export async function updateProfilePhotoGoogle(oAuth2Client: OAuth2Client, userId: number) {
|
||||
try {
|
||||
const oauth2 = new oauth2_v2.Oauth2({ auth: oAuth2Client });
|
||||
const userDetails = await oauth2.userinfo.get();
|
||||
if (userDetails.data?.picture) {
|
||||
await UserRepository.updateAvatar({ id: userId, avatarUrl: userDetails.data.picture });
|
||||
const avatarUrl = userDetails.data?.picture;
|
||||
if (!avatarUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle base64 data
|
||||
if (isBase64Image(avatarUrl)) {
|
||||
const resizedAvatarUrl = await uploadAvatar({
|
||||
avatar: await resizeBase64Image(avatarUrl),
|
||||
userId,
|
||||
});
|
||||
await UserRepository.updateAvatar({ id: userId, avatarUrl: resizedAvatarUrl });
|
||||
return;
|
||||
}
|
||||
|
||||
await UserRepository.updateAvatar({ id: userId, avatarUrl });
|
||||
} catch (error) {
|
||||
logger.error("Error updating avatarUrl from google calendar connect", error);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,12 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import GoogleCalendarService from "@calcom/app-store/googlecalendar/lib/CalendarService";
|
||||
import { renewSelectedCalendarCredentialId } from "@calcom/lib/connectedCalendar";
|
||||
import { GOOGLE_CALENDAR_SCOPES, WEBAPP_URL, WEBAPP_URL_FOR_OAUTH } from "@calcom/lib/constants";
|
||||
import {
|
||||
GOOGLE_CALENDAR_SCOPES,
|
||||
SCOPE_USERINFO_PROFILE,
|
||||
WEBAPP_URL,
|
||||
WEBAPP_URL_FOR_OAUTH,
|
||||
} from "@calcom/lib/constants";
|
||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultHandler } from "@calcom/lib/server/defaultHandler";
|
||||
@@ -14,6 +19,7 @@ import { Prisma } from "@calcom/prisma/client";
|
||||
|
||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";
|
||||
import { updateProfilePhotoGoogle } from "../../_utils/oauth/updateProfilePhotoGoogle";
|
||||
import { getGoogleAppKeys } from "../lib/getGoogleAppKeys";
|
||||
|
||||
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
@@ -95,10 +101,9 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
}
|
||||
|
||||
// Only attempt to update the user's profile photo if the user has granted the required scope
|
||||
// TODO: Use the avatarUrl when setting the profile picture
|
||||
// if (grantedScopes.includes(SCOPE_USERINFO_PROFILE)) {
|
||||
// await updateProfilePhotoGoogle(oAuth2Client, req.session.user.id);
|
||||
// }
|
||||
if (grantedScopes.includes(SCOPE_USERINFO_PROFILE)) {
|
||||
await updateProfilePhotoGoogle(oAuth2Client, req.session.user.id);
|
||||
}
|
||||
|
||||
const selectedCalendarWhereUnique = {
|
||||
userId: req.session.user.id,
|
||||
|
||||
@@ -155,6 +155,18 @@ const ZoomVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter =>
|
||||
};
|
||||
};
|
||||
|
||||
// Zoom agenda field has a 2000 character limit; we set maxLength to 1900 to leave a safety buffer
|
||||
const truncateAgenda = (description?: string | null) => {
|
||||
if (!description) return description;
|
||||
|
||||
const maxLength = 1900;
|
||||
const trimmed = description.trimEnd();
|
||||
if (trimmed.length > maxLength) {
|
||||
return `${trimmed.substring(0, maxLength).trimEnd()}...`;
|
||||
}
|
||||
return trimmed;
|
||||
};
|
||||
|
||||
const userSettings = await getUserSettings();
|
||||
const recurrence = getRecurrence(event);
|
||||
// Documentation at: https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingcreate
|
||||
@@ -166,7 +178,7 @@ const ZoomVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter =>
|
||||
//schedule_for: "string", TODO: Used when scheduling the meeting for someone else (needed?)
|
||||
timezone: event.organizer.timeZone,
|
||||
password: userSettings?.schedule_meeting?.default_password_for_scheduled_meetings ?? undefined,
|
||||
agenda: event.description,
|
||||
agenda: truncateAgenda(event.description),
|
||||
settings: {
|
||||
host_video: true,
|
||||
participant_video: true,
|
||||
|
||||
@@ -4,6 +4,7 @@ import "dayjs/locale/cs";
|
||||
import "dayjs/locale/de";
|
||||
import "dayjs/locale/es";
|
||||
import "dayjs/locale/es-mx";
|
||||
import "dayjs/locale/fi";
|
||||
import "dayjs/locale/fr";
|
||||
import "dayjs/locale/he";
|
||||
import "dayjs/locale/hu";
|
||||
|
||||
@@ -1502,15 +1502,6 @@ async function handler(
|
||||
evt.videoCallData = undefined;
|
||||
// To prevent "The requested identifier already exists" error while updating event, we need to remove iCalUID
|
||||
evt.iCalUID = undefined;
|
||||
} else {
|
||||
// In case of rescheduling, we need to keep the previous host destination calendar
|
||||
evt = CalendarEventBuilder.fromEvent(evt)
|
||||
.withDestinationCalendar(
|
||||
originalRescheduledBooking?.destinationCalendar
|
||||
? [originalRescheduledBooking?.destinationCalendar]
|
||||
: evt.destinationCalendar
|
||||
)
|
||||
.build();
|
||||
}
|
||||
|
||||
const updateManager = await eventManager.reschedule(
|
||||
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
import {
|
||||
createBookingScenario,
|
||||
getGoogleCalendarCredential,
|
||||
TestData,
|
||||
getOrganizer,
|
||||
getBooker,
|
||||
getScenarioData,
|
||||
mockSuccessfulVideoMeetingCreation,
|
||||
mockCalendarToHaveNoBusySlots,
|
||||
getDate,
|
||||
BookingLocations,
|
||||
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
|
||||
import { expectBookingToBeInDatabase } from "@calcom/web/test/utils/bookingScenario/expects";
|
||||
import { getMockRequestDataForBooking } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking";
|
||||
import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown";
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { describe, expect, beforeEach } from "vitest";
|
||||
|
||||
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
||||
import { resetTestEmails } from "@calcom/lib/testEmails";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import { test } from "@calcom/web/test/fixtures/fixtures";
|
||||
|
||||
export type CustomNextApiRequest = NextApiRequest & Request;
|
||||
|
||||
export type CustomNextApiResponse = NextApiResponse & Response;
|
||||
|
||||
const timeout = process.env.CI ? 5000 : 20000;
|
||||
|
||||
describe("handleNewBooking", () => {
|
||||
setupAndTeardown();
|
||||
|
||||
beforeEach(() => {
|
||||
resetTestEmails();
|
||||
});
|
||||
|
||||
describe("Collective Event Reschedule with Destination Calendar Bug", () => {
|
||||
test(
|
||||
`should reschedule a collective event booking and ensure both hosts have the booking in their calendars when a second host is added
|
||||
1. Create a collective event with 1 host and book it
|
||||
2. Add a second host to the event type
|
||||
3. Reschedule the booking
|
||||
4. Verify both hosts have the booking in their calendars
|
||||
`,
|
||||
async ({ emails }) => {
|
||||
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
|
||||
|
||||
const booker = getBooker({
|
||||
email: "booker@example.com",
|
||||
name: "Booker",
|
||||
});
|
||||
|
||||
const organizer = getOrganizer({
|
||||
name: "Organizer",
|
||||
email: "organizer@example.com",
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
destinationCalendar: {
|
||||
integration: "google_calendar",
|
||||
externalId: "organizer@google-calendar.com",
|
||||
primaryEmail: "organizer@google-calendar.com",
|
||||
},
|
||||
});
|
||||
|
||||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
||||
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
||||
const uidOfBookingToBeRescheduled = "n5Wv3eHgconAED2j4gcVhP";
|
||||
const iCalUID = `${uidOfBookingToBeRescheduled}@Cal.com`;
|
||||
|
||||
await createBookingScenario(
|
||||
getScenarioData({
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slotInterval: 15,
|
||||
length: 30,
|
||||
schedulingType: SchedulingType.COLLECTIVE,
|
||||
users: [
|
||||
{
|
||||
id: 101, // organizer
|
||||
},
|
||||
{
|
||||
id: 102, // second host (will be added to event type)
|
||||
},
|
||||
],
|
||||
destinationCalendar: {
|
||||
integration: "google_calendar",
|
||||
externalId: "event-type-1@google-calendar.com",
|
||||
primaryEmail: "event-type-1@google-calendar.com",
|
||||
},
|
||||
},
|
||||
],
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
name: "Organizer",
|
||||
email: "organizer@example.com",
|
||||
username: "organizer",
|
||||
timeZone: "Asia/Kolkata",
|
||||
defaultScheduleId: null,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
destinationCalendar: {
|
||||
integration: "google_calendar",
|
||||
externalId: "organizer@google-calendar.com",
|
||||
primaryEmail: "organizer@google-calendar.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 102,
|
||||
name: "Second Host",
|
||||
email: "second-host@example.com",
|
||||
username: "second-host",
|
||||
timeZone: "Asia/Kolkata",
|
||||
defaultScheduleId: null,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
destinationCalendar: {
|
||||
integration: "google_calendar",
|
||||
externalId: "second-host@google-calendar.com",
|
||||
primaryEmail: "second-host@google-calendar.com",
|
||||
},
|
||||
},
|
||||
],
|
||||
bookings: [
|
||||
{
|
||||
uid: uidOfBookingToBeRescheduled,
|
||||
eventTypeId: 1,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
startTime: `${plus1DateString}T05:00:00.000Z`,
|
||||
endTime: `${plus1DateString}T05:30:00.000Z`,
|
||||
users: [
|
||||
{
|
||||
id: 101, // organizer
|
||||
},
|
||||
],
|
||||
references: [
|
||||
{
|
||||
type: appStoreMetadata.dailyvideo.type,
|
||||
uid: "MOCK_ID_1",
|
||||
meetingId: "MOCK_ID_1",
|
||||
meetingPassword: "MOCK_PASS_1",
|
||||
meetingUrl: "http://mock-dailyvideo.example.com/meeting-1",
|
||||
credentialId: null,
|
||||
},
|
||||
{
|
||||
type: appStoreMetadata.googlecalendar.type,
|
||||
uid: "MOCKED_GOOGLE_CALENDAR_EVENT_ID_1",
|
||||
meetingId: "MOCKED_GOOGLE_CALENDAR_EVENT_ID_1",
|
||||
meetingPassword: "MOCK_PASSWORD",
|
||||
meetingUrl: "https://UNUSED_URL",
|
||||
externalCalendarId: "event-type-1@google-calendar.com",
|
||||
credentialId: undefined,
|
||||
},
|
||||
],
|
||||
iCalUID,
|
||||
},
|
||||
],
|
||||
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
|
||||
})
|
||||
);
|
||||
|
||||
mockSuccessfulVideoMeetingCreation({
|
||||
metadataLookupKey: appStoreMetadata.dailyvideo.dirName,
|
||||
videoMeetingData: {
|
||||
id: "MOCK_ID_1", // Video meeting ID is not updated during reschedule
|
||||
password: "MOCK_PASS_1",
|
||||
url: `http://mock-dailyvideo.example.com/meeting-1`,
|
||||
},
|
||||
});
|
||||
|
||||
const organizerCalendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", {
|
||||
update: {
|
||||
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID_2",
|
||||
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID_2",
|
||||
},
|
||||
});
|
||||
|
||||
const rescheduleBookingData = getMockRequestDataForBooking({
|
||||
data: {
|
||||
start: `${plus2DateString}T11:00:00.000Z`,
|
||||
end: `${plus2DateString}T11:30:00.000Z`,
|
||||
eventTypeId: 1,
|
||||
rescheduleUid: uidOfBookingToBeRescheduled,
|
||||
responses: {
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
location: { optionValue: "", value: BookingLocations.CalVideo },
|
||||
},
|
||||
rescheduledBy: organizer.email,
|
||||
},
|
||||
});
|
||||
|
||||
const rescheduledBooking = await handleNewBooking({
|
||||
bookingData: rescheduleBookingData,
|
||||
});
|
||||
|
||||
await expectBookingToBeInDatabase({
|
||||
description: "",
|
||||
location: BookingLocations.CalVideo,
|
||||
responses: expect.objectContaining({
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
}),
|
||||
uid: rescheduledBooking.uid!,
|
||||
eventTypeId: rescheduleBookingData.eventTypeId,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
references: [
|
||||
{
|
||||
type: appStoreMetadata.dailyvideo.type,
|
||||
uid: "MOCK_ID_1",
|
||||
meetingId: "MOCK_ID_1",
|
||||
meetingPassword: "MOCK_PASS_1",
|
||||
meetingUrl: "http://mock-dailyvideo.example.com/meeting-1",
|
||||
},
|
||||
{
|
||||
type: appStoreMetadata.googlecalendar.type,
|
||||
uid: "MOCKED_GOOGLE_CALENDAR_EVENT_ID_1",
|
||||
externalCalendarId: "event-type-1@google-calendar.com",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(organizerCalendarMock.updateEventCalls.length).toBe(1);
|
||||
expect(organizerCalendarMock.createEventCalls.length).toBe(0);
|
||||
|
||||
// Check availability calls for both hosts
|
||||
expect(organizerCalendarMock.getAvailabilityCalls.length).toBe(2);
|
||||
},
|
||||
timeout
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -157,6 +157,8 @@ export async function handler(req: NextRequest) {
|
||||
}
|
||||
|
||||
if (message?.length && message?.length > 0 && sendTo) {
|
||||
const smsMessageWithoutOptOut = message;
|
||||
|
||||
if (process.env.TWILIO_OPT_OUT_ENABLED === "true") {
|
||||
message = await WorkflowOptOutService.addOptOutMessage(message, locale || "en");
|
||||
}
|
||||
@@ -167,6 +169,7 @@ export async function handler(req: NextRequest) {
|
||||
body: message,
|
||||
scheduledDate: reminder.scheduledDate,
|
||||
sender: senderID,
|
||||
bodyWithoutOptOut: smsMessageWithoutOptOut,
|
||||
bookingUid: reminder.booking.uid,
|
||||
userId,
|
||||
teamId,
|
||||
|
||||
@@ -14,6 +14,7 @@ export async function sendSmsOrFallbackEmail(props: {
|
||||
phoneNumber: string;
|
||||
body: string;
|
||||
sender: string;
|
||||
bodyWithoutOptOut?: string;
|
||||
bookingUid?: string | null;
|
||||
userId?: number | null;
|
||||
teamId?: number | null;
|
||||
@@ -40,7 +41,7 @@ export async function sendSmsOrFallbackEmail(props: {
|
||||
await sendOrScheduleWorkflowEmails({
|
||||
to: [fallbackData.email],
|
||||
subject: fallbackData.t("notification_about_your_booking"),
|
||||
html: twilioData.body,
|
||||
html: twilioData.bodyWithoutOptOut || twilioData.body,
|
||||
replyTo: fallbackData.replyTo,
|
||||
});
|
||||
}
|
||||
@@ -60,6 +61,7 @@ export async function scheduleSmsOrFallbackEmail(props: {
|
||||
body: string;
|
||||
scheduledDate: Date;
|
||||
sender: string;
|
||||
bodyWithoutOptOut?: string;
|
||||
bookingUid?: string | null;
|
||||
userId?: number | null;
|
||||
teamId?: number | null;
|
||||
@@ -96,7 +98,7 @@ export async function scheduleSmsOrFallbackEmail(props: {
|
||||
await sendOrScheduleWorkflowEmails({
|
||||
to: [fallbackData.email],
|
||||
subject: fallbackData.t("notification_about_your_booking"),
|
||||
html: twilioData.body,
|
||||
html: twilioData.bodyWithoutOptOut || twilioData.body,
|
||||
replyTo: fallbackData.replyTo,
|
||||
sendAt: twilioData.scheduledDate,
|
||||
referenceUid: reminder.uuid || undefined,
|
||||
|
||||
@@ -176,6 +176,8 @@ export const scheduleSMSReminder = async (args: ScheduleTextReminderArgs) => {
|
||||
}
|
||||
|
||||
if (smsMessage.length > 0) {
|
||||
const smsMessageWithoutOptOut = smsMessage;
|
||||
|
||||
if (process.env.TWILIO_OPT_OUT_ENABLED === "true") {
|
||||
smsMessage = await WorkflowOptOutService.addOptOutMessage(
|
||||
smsMessage,
|
||||
@@ -196,6 +198,7 @@ export const scheduleSMSReminder = async (args: ScheduleTextReminderArgs) => {
|
||||
phoneNumber: reminderPhone,
|
||||
body: smsMessage,
|
||||
sender: senderID,
|
||||
bodyWithoutOptOut: smsMessageWithoutOptOut,
|
||||
bookingUid: evt.uid,
|
||||
userId,
|
||||
teamId,
|
||||
|
||||
@@ -107,6 +107,66 @@ const getLocationInfo = ({
|
||||
return { locationAvailable, locationDetails };
|
||||
};
|
||||
|
||||
const LocationInput = (props: {
|
||||
eventLocationType: EventLocationType;
|
||||
defaultValue?: string;
|
||||
index: number;
|
||||
customClassNames?: LocationInputCustomClassNames;
|
||||
disableLocationProp?: boolean;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const { eventLocationType, index, customClassNames, disableLocationProp, ...remainingProps } = props;
|
||||
if (eventLocationType?.organizerInputType === "text") {
|
||||
const { defaultValue, ...rest } = remainingProps;
|
||||
|
||||
return (
|
||||
<Controller
|
||||
name={`locations.${index}.${eventLocationType.defaultValueVariable}`}
|
||||
defaultValue={defaultValue}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<Input
|
||||
name={`locations[${index}].${eventLocationType.defaultValueVariable}`}
|
||||
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
|
||||
type="text"
|
||||
required
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
{...(disableLocationProp ? { disabled: true } : {})}
|
||||
className={classNames("my-0", customClassNames?.addressInput)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else if (eventLocationType?.organizerInputType === "phone") {
|
||||
const { defaultValue, ...rest } = remainingProps;
|
||||
|
||||
return (
|
||||
<Controller
|
||||
name={`locations.${index}.${eventLocationType.defaultValueVariable}`}
|
||||
defaultValue={defaultValue}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<PhoneInput
|
||||
required
|
||||
disabled={disableLocationProp}
|
||||
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
|
||||
name={`locations[${index}].${eventLocationType.defaultValueVariable}`}
|
||||
className={customClassNames?.phoneInput}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const Locations: React.FC<LocationsProps> = ({
|
||||
isChildrenManagedEventType,
|
||||
disableLocationProp,
|
||||
@@ -168,64 +228,6 @@ const Locations: React.FC<LocationsProps> = ({
|
||||
locationOptions: props.locationOptions,
|
||||
});
|
||||
|
||||
const LocationInput = (props: {
|
||||
eventLocationType: EventLocationType;
|
||||
defaultValue?: string;
|
||||
index: number;
|
||||
customClassNames?: LocationInputCustomClassNames;
|
||||
}) => {
|
||||
const { eventLocationType, index, customClassNames, ...remainingProps } = props;
|
||||
if (eventLocationType?.organizerInputType === "text") {
|
||||
const { defaultValue, ...rest } = remainingProps;
|
||||
|
||||
return (
|
||||
<Controller
|
||||
name={`locations.${index}.${eventLocationType.defaultValueVariable}`}
|
||||
defaultValue={defaultValue}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<Input
|
||||
name={`locations[${index}].${eventLocationType.defaultValueVariable}`}
|
||||
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
|
||||
type="text"
|
||||
required
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
{...(disableLocationProp ? { disabled: true } : {})}
|
||||
className={classNames("my-0", customClassNames?.addressInput)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else if (eventLocationType?.organizerInputType === "phone") {
|
||||
const { defaultValue, ...rest } = remainingProps;
|
||||
|
||||
return (
|
||||
<Controller
|
||||
name={`locations.${index}.${eventLocationType.defaultValueVariable}`}
|
||||
defaultValue={defaultValue}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<PhoneInput
|
||||
required
|
||||
disabled={disableLocationProp}
|
||||
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
|
||||
name={`locations[${index}].${eventLocationType.defaultValueVariable}`}
|
||||
className={customClassNames?.phoneInput}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const [showEmptyLocationSelect, setShowEmptyLocationSelect] = useState(false);
|
||||
const defaultInitialLocation = defaultValue || null;
|
||||
const [selectedNewOption, setSelectedNewOption] = useState<SingleValueLocationOption | null>(
|
||||
@@ -459,6 +461,7 @@ const Locations: React.FC<LocationsProps> = ({
|
||||
eventLocationType={eventLocationType}
|
||||
index={index}
|
||||
customClassNames={customClassNames?.organizerContactInput?.locationInput}
|
||||
disableLocationProp={disableLocationProp}
|
||||
/>
|
||||
</div>
|
||||
<ErrorMessage
|
||||
|
||||
@@ -484,16 +484,34 @@ class EventsInsights {
|
||||
: booking.attendees;
|
||||
|
||||
const formattedAttendees = attendeeList
|
||||
.slice(0, 3)
|
||||
.map((attendee) => (attendee ? `${attendee.name} (${attendee.email})` : null));
|
||||
.map((attendee) => (attendee ? `${attendee.name} (${attendee.email})` : null))
|
||||
.filter(Boolean);
|
||||
|
||||
return [
|
||||
booking.uid,
|
||||
{ attendeeList: formattedAttendees, noShowGuest: attendeeList[0]?.noShow || false },
|
||||
];
|
||||
})
|
||||
);
|
||||
|
||||
const maxAttendees = Math.max(
|
||||
...Array.from(bookingMap.values()).map((data) => data.attendeeList.length),
|
||||
0
|
||||
);
|
||||
|
||||
const finalBookingMap = new Map(
|
||||
Array.from(bookingMap.entries()).map(([uid, data]) => {
|
||||
const attendeeFields: Record<string, string | null> = {};
|
||||
|
||||
for (let i = 1; i <= maxAttendees; i++) {
|
||||
attendeeFields[`attendee${i}`] = data.attendeeList[i - 1] || null;
|
||||
}
|
||||
|
||||
return [
|
||||
uid,
|
||||
{
|
||||
noShowGuest: attendeeList[0]?.noShow || false,
|
||||
attendee1: formattedAttendees[0] || null,
|
||||
attendee2: formattedAttendees[1] || null,
|
||||
attendee3: formattedAttendees[2] || null,
|
||||
noShowGuest: data.noShowGuest,
|
||||
...attendeeFields,
|
||||
},
|
||||
];
|
||||
})
|
||||
@@ -502,33 +520,37 @@ class EventsInsights {
|
||||
const data = csvData.map((bookingTimeStatus) => {
|
||||
if (!bookingTimeStatus.uid) {
|
||||
// should not be reached because we filtered above
|
||||
const nullAttendeeFields: Record<string, null> = {};
|
||||
for (let i = 1; i <= maxAttendees; i++) {
|
||||
nullAttendeeFields[`attendee${i}`] = null;
|
||||
}
|
||||
|
||||
return {
|
||||
...bookingTimeStatus,
|
||||
noShowGuest: false,
|
||||
attendee1: null,
|
||||
attendee2: null,
|
||||
attendee3: null,
|
||||
...nullAttendeeFields,
|
||||
};
|
||||
}
|
||||
|
||||
const attendeeData = bookingMap.get(bookingTimeStatus.uid);
|
||||
const attendeeData = finalBookingMap.get(bookingTimeStatus.uid);
|
||||
|
||||
if (!attendeeData) {
|
||||
const nullAttendeeFields: Record<string, null> = {};
|
||||
for (let i = 1; i <= maxAttendees; i++) {
|
||||
nullAttendeeFields[`attendee${i}`] = null;
|
||||
}
|
||||
|
||||
return {
|
||||
...bookingTimeStatus,
|
||||
noShowGuest: false,
|
||||
attendee1: null,
|
||||
attendee2: null,
|
||||
attendee3: null,
|
||||
...nullAttendeeFields,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...bookingTimeStatus,
|
||||
noShowGuest: attendeeData.noShowGuest,
|
||||
attendee1: attendeeData.attendee1,
|
||||
attendee2: attendeeData.attendee2,
|
||||
attendee3: attendeeData.attendee3,
|
||||
...Object.fromEntries(Object.entries(attendeeData).filter(([key]) => key.startsWith("attendee"))),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import type { CookieOption, CookiesOptions } from "next-auth";
|
||||
|
||||
import { isENVDev } from "@calcom/lib/env";
|
||||
|
||||
/**
|
||||
* Copy from 'https://github.com/nextauthjs/next-auth/blob/227ff2259f/src/core/lib/cookie.ts' as we can't import it directly
|
||||
*
|
||||
@@ -20,12 +18,7 @@ export function defaultCookies(useSecureCookies: boolean): CookiesOptions {
|
||||
const cookiePrefix = useSecureCookies ? "__Secure-" : "";
|
||||
|
||||
const defaultOptions: CookieOption["options"] = {
|
||||
domain: isENVDev
|
||||
? process.env.ORGANIZATIONS_ENABLED
|
||||
? //FIXME: This is causing login to not work if someone uses anything other .cal.local for testing
|
||||
".cal.local"
|
||||
: undefined
|
||||
: NEXTAUTH_COOKIE_DOMAIN,
|
||||
domain: NEXTAUTH_COOKIE_DOMAIN || undefined,
|
||||
// To enable cookies on widgets,
|
||||
// https://stackoverflow.com/questions/45094712/iframe-not-reading-cookies-in-chrome
|
||||
// But we need to set it as `lax` in development
|
||||
|
||||
@@ -104,6 +104,8 @@ export function getEventName(eventNameObj: EventNameObjectType, forAttendeeView
|
||||
}
|
||||
|
||||
dynamicEventName = dynamicEventName.replace(`{${variable}}`, fieldValue || "");
|
||||
} else {
|
||||
dynamicEventName = dynamicEventName.replace(`{${variable}}`, "");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Checks if a string is a supported base64 image format
|
||||
*/
|
||||
export function isBase64Image(input: string | null | undefined): boolean {
|
||||
if (!input) return false;
|
||||
|
||||
return (
|
||||
input.startsWith("data:image/png;base64,") ||
|
||||
input.startsWith("data:image/jpeg;base64,") ||
|
||||
input.startsWith("data:image/jpg;base64,")
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import "@calcom/lib/__mocks__/logger";
|
||||
|
||||
import { createHash } from "crypto";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
@@ -12,12 +13,14 @@ import { substituteVariables } from "@calcom/app-store/routing-forms/lib/substit
|
||||
import { getUrlSearchParamsToForward } from "@calcom/app-store/routing-forms/pages/routing-link/getUrlSearchParamsToForward";
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { isAuthorizedToViewFormOnOrgDomain } from "@calcom/features/routing-forms/lib/isAuthorizedToViewForm";
|
||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||
import { RoutingFormRepository } from "@calcom/lib/server/repository/routingForm";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
|
||||
import { getRoutedUrl } from "./getRoutedUrl";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@calcom/lib/checkRateLimitAndThrowError");
|
||||
vi.mock("@calcom/app-store/routing-forms/lib/handleResponse");
|
||||
vi.mock("@calcom/lib/server/repository/routingForm");
|
||||
vi.mock("@calcom/lib/server/repository/user");
|
||||
@@ -112,6 +115,7 @@ describe("getRoutedUrl", () => {
|
||||
it("should return notFound if form is not found", async () => {
|
||||
vi.mocked(RoutingFormRepository.findFormByIdIncludeUserTeamAndOrg).mockResolvedValue(null);
|
||||
const context = mockContext({});
|
||||
|
||||
const result = await getRoutedUrl(context);
|
||||
expect(result).toEqual({ notFound: true });
|
||||
expect(RoutingFormRepository.findFormByIdIncludeUserTeamAndOrg).toHaveBeenCalledWith("form-id");
|
||||
@@ -259,6 +263,19 @@ describe("getRoutedUrl", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw an error if rate limit is exceeded", async () => {
|
||||
vi.mocked(checkRateLimitAndThrowError).mockRejectedValue(new Error("Rate limit exceeded"));
|
||||
const context = mockContext({ email: "test@cal.com" });
|
||||
const expectedHash = createHash("sha256")
|
||||
.update(JSON.stringify({ email: "test@cal.com" }))
|
||||
.digest("hex");
|
||||
|
||||
await expect(getRoutedUrl(context)).rejects.toThrow("Rate limit exceeded");
|
||||
expect(checkRateLimitAndThrowError).toHaveBeenCalledWith({
|
||||
identifier: `form:form-id:hash:${expectedHash}`,
|
||||
});
|
||||
});
|
||||
|
||||
describe("Dry Run", () => {
|
||||
it("should call handleResponse with isPreview:true", async () => {
|
||||
vi.mocked(RoutingFormRepository.findFormByIdIncludeUserTeamAndOrg).mockResolvedValue(mockForm as never);
|
||||
|
||||
@@ -44,7 +44,7 @@ const getDeterministicHashForResponse = (fieldsResponses: Record<string, unknown
|
||||
return hash;
|
||||
};
|
||||
|
||||
function hasEmbedPath(pathWithQuery: string) {
|
||||
export function hasEmbedPath(pathWithQuery: string) {
|
||||
const onlyPath = pathWithQuery.split("?")[0];
|
||||
return onlyPath.endsWith("/embed") || onlyPath.endsWith("/embed/");
|
||||
}
|
||||
|
||||
@@ -138,6 +138,70 @@ export class ScheduleRepository {
|
||||
isDefault: !scheduleId || defaultScheduleId === schedule.id,
|
||||
isLastSchedule: schedulesCount <= 1,
|
||||
readOnly: schedule.userId !== userId && !isManagedEventType,
|
||||
userId: schedule.userId,
|
||||
};
|
||||
}
|
||||
|
||||
static async findManyDetailedScheduleByUserId({
|
||||
isManagedEventType,
|
||||
userId,
|
||||
defaultScheduleId,
|
||||
|
||||
timeZone: userTimeZone,
|
||||
}: {
|
||||
timeZone: string;
|
||||
userId: number;
|
||||
defaultScheduleId: number | null;
|
||||
|
||||
isManagedEventType?: boolean;
|
||||
}) {
|
||||
const schedules = await prisma.schedule.findMany({
|
||||
where: {
|
||||
userId: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
name: true,
|
||||
availability: true,
|
||||
timeZone: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!schedules?.length) {
|
||||
throw new Error("Schedules not found");
|
||||
}
|
||||
|
||||
const isCurrentUserPartOfTeam = await hasReadPermissionsForUserId({
|
||||
memberId: schedules[0].userId,
|
||||
userId,
|
||||
});
|
||||
|
||||
const schedulesFormatted = schedules.map((schedule) => {
|
||||
const isCurrentUserOwner = schedule?.userId === userId;
|
||||
|
||||
if (!isCurrentUserPartOfTeam && !isCurrentUserOwner) {
|
||||
throw new Error("UNAUTHORIZED");
|
||||
}
|
||||
|
||||
const timeZone = schedule.timeZone || userTimeZone;
|
||||
// disabling utc casting while fetching WorkingHours
|
||||
return {
|
||||
id: schedule.id,
|
||||
name: schedule.name,
|
||||
isManaged: schedule.userId !== userId,
|
||||
workingHours: transformWorkingHoursForAtom(schedule),
|
||||
schedule: schedule.availability,
|
||||
availability: transformAvailabilityForAtom(schedule),
|
||||
timeZone,
|
||||
isDefault: schedule.id === defaultScheduleId,
|
||||
dateOverrides: transformDateOverridesForAtom(schedule, timeZone),
|
||||
readOnly: schedule.userId !== userId && !isManagedEventType,
|
||||
isLastSchedule: schedules.length <= 1,
|
||||
userId: schedule.userId,
|
||||
};
|
||||
});
|
||||
|
||||
return schedulesFormatted;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,8 @@
|
||||
## 1.0.108
|
||||
|
||||
## 1.0.112
|
||||
## 1.0.114
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#22134](https://github.com/calcom/cal.com/pull/22134) [`3437c43`](https://github.com/calcom/cal.com/commit/3437c4331da892bd77afaea84be2db196543a207) Thanks [@supalarry](https://github.com/supalarry)! - ignore this test log
|
||||
|
||||
## 1.0.110
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#22107](https://github.com/calcom/cal.com/pull/22107) [`88a2bd3`](https://github.com/calcom/cal.com/commit/88a2bd3317a2cfaf6b367524e2736445544dbd63) Thanks [@supalarry](https://github.com/supalarry)! - test log can ignore
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#22103](https://github.com/calcom/cal.com/pull/22103) [`61274bc`](https://github.com/calcom/cal.com/commit/61274bc7efc67b162d46b59cd75bd376ad515c51) Thanks [@supalarry](https://github.com/supalarry)! - testing changesets - can ignore this
|
||||
|
||||
## 1.0.107
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#22095](https://github.com/calcom/cal.com/pull/22095) [`3c43ce4`](https://github.com/calcom/cal.com/commit/3c43ce4165bce1da29b203d5f2eb62090c5fddd2) Thanks [@supalarry](https://github.com/supalarry)! - test by updating changelog
|
||||
|
||||
- [#21864](https://github.com/calcom/cal.com/pull/21864) [`540bf86`](https://github.com/calcom/cal.com/commit/540bf868b5b60a98d9d3aeb565e2089f15c3dfd3) Thanks [@supalarry](https://github.com/supalarry)! - fix saving event type settings
|
||||
- [#22148](https://github.com/calcom/cal.com/pull/22148) [`d27490e`](https://github.com/calcom/cal.com/commit/d27490e6f2438d353dfcf3e793c8886e723307f3) Thanks [@supalarry](https://github.com/supalarry)! - test release
|
||||
|
||||
## 1.0.63
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"type": "module",
|
||||
"description": "Customizable UI components to integrate scheduling into your product.",
|
||||
"authors": "Cal.com, Inc.",
|
||||
"version": "1.0.112",
|
||||
"version": "1.0.114",
|
||||
"scripts": {
|
||||
"dev": "yarn vite build --watch & npx tailwindcss -i ./globals.css -o ./globals.min.css --postcss --minify --watch",
|
||||
"build": "NODE_OPTIONS='--max_old_space_size=12288' rm -rf dist && yarn vite build && npx tailwindcss -i ./globals.css -o ./globals.min.css --postcss --minify && mkdir ./dist/packages/prisma-client && cp -rf ../../../node_modules/.prisma/client/*.d.ts ./dist/packages/prisma-client",
|
||||
@@ -37,11 +37,9 @@
|
||||
"dist",
|
||||
"globals.min.css"
|
||||
],
|
||||
"main": "./dist/cal-atoms.umd.cjs",
|
||||
"module": "./dist/cal-atoms.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"require": "./dist/cal-atoms.umd.cjs",
|
||||
"import": "./dist/cal-atoms.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
@@ -83,7 +81,6 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-awesome-query-builder": "^5.1.2",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,16 +31,32 @@
|
||||
"../../types/@wojtekmaj__react-daterange-picker.d.ts",
|
||||
"../../types/business-days-plugin.d.ts",
|
||||
"../../types/window.d.ts",
|
||||
"../../lib",
|
||||
"../../features",
|
||||
"../types",
|
||||
"../constants",
|
||||
"../utils",
|
||||
"../libraries",
|
||||
"../../prisma",
|
||||
"../../dayjs",
|
||||
"../../trpc",
|
||||
"../../app-store"
|
||||
"../../prisma/zod-utils.ts",
|
||||
"../../prisma/zod",
|
||||
"../../trpc/react",
|
||||
"../../trpc/server/routers/viewer/slots",
|
||||
"../../trpc/server/types",
|
||||
"../../features/eventtypes",
|
||||
"../../features/schedules",
|
||||
"../../features/bookings/Booker",
|
||||
"../../features/bookings/Booker",
|
||||
"../../features/instant-meeting",
|
||||
"../../features/bookings/handleMarkNoShow.ts",
|
||||
"../../features/bookings/types.ts",
|
||||
"../../features/bookings/lib",
|
||||
"../../features/ee/workflows",
|
||||
"../../features/ee/payments",
|
||||
"../../features/ee/round-robin",
|
||||
"../../features/ee/billing",
|
||||
"../../features/shell",
|
||||
"../../features/components",
|
||||
"../../features/form-builder",
|
||||
"../../features/data-table"
|
||||
],
|
||||
"exclude": [
|
||||
"dist",
|
||||
|
||||
@@ -3,7 +3,6 @@ import path from "path";
|
||||
import { resolve } from "path";
|
||||
import { defineConfig, loadEnv } from "vite";
|
||||
import dts from "vite-plugin-dts";
|
||||
import Inspect from "vite-plugin-inspect";
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), ""); // .env inside of packages/platform/atoms
|
||||
@@ -20,7 +19,7 @@ export default defineConfig(({ mode }) => {
|
||||
"@calcom/platform-utils",
|
||||
],
|
||||
},
|
||||
plugins: [Inspect(), react(), dts({ insertTypesEntry: true })],
|
||||
plugins: [react(), dts({ insertTypesEntry: true })],
|
||||
define: {
|
||||
"process.env.NEXT_PUBLIC_WEBAPP_URL": `"${webAppUrl}"`,
|
||||
},
|
||||
@@ -28,17 +27,16 @@ export default defineConfig(({ mode }) => {
|
||||
noExternal: ["turndown"], // Example if you want to disable SSR for your library
|
||||
},
|
||||
build: {
|
||||
commonjsOptions: {
|
||||
include: [/@calcom\/lib/, /@calcom\/features/, /node_modules/],
|
||||
},
|
||||
lib: {
|
||||
entry: [resolve(__dirname, "index.ts")],
|
||||
name: "CalAtoms",
|
||||
fileName: "cal-atoms",
|
||||
formats: ["es"],
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ["react", "fs", "path", "os", "react-dom", "react-awesome-query-builder"],
|
||||
external: ["react", "fs", "path", "os", "react/jsx-runtime", "react-dom", "react-dom/client"],
|
||||
output: {
|
||||
format: "esm", // Force ESM output
|
||||
globals: {
|
||||
react: "React",
|
||||
"react-dom": "ReactDOM",
|
||||
@@ -58,6 +56,7 @@ export default defineConfig(({ mode }) => {
|
||||
"@radix-ui/react-dialog": path.resolve(__dirname, "./src/components/ui/dialog.tsx"),
|
||||
".prisma/client": path.resolve(__dirname, "../../prisma-client"),
|
||||
"@prisma/client": path.resolve(__dirname, "../../prisma-client"),
|
||||
"@calcom/prisma/client": path.resolve(__dirname, "../../prisma-client"),
|
||||
"@calcom/prisma": path.resolve(__dirname, "../../prisma"),
|
||||
"@calcom/dayjs": path.resolve(__dirname, "../../dayjs"),
|
||||
"@calcom/platform-constants": path.resolve(__dirname, "../constants/index.ts"),
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+148
@@ -0,0 +1,148 @@
|
||||
import type { User, Team, EventType } from "@prisma/client";
|
||||
import { describe, it, expect, afterAll, beforeAll } from "vitest";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { listWithTeamHandler } from "./listWithTeam.handler";
|
||||
|
||||
let user1: User;
|
||||
let user2: User;
|
||||
let team1: Team;
|
||||
let team2: Team;
|
||||
let eventType1: EventType;
|
||||
let eventType2: EventType;
|
||||
let eventType3: EventType;
|
||||
let eventType4: EventType;
|
||||
|
||||
describe("listWithTeamHandler", () => {
|
||||
beforeAll(async () => {
|
||||
// Create users, teams and event types
|
||||
user1 = await prisma.user.create({
|
||||
data: {
|
||||
username: "testuser-lwt-1",
|
||||
email: "testuser-lwt-1@example.com",
|
||||
name: "Test User 1",
|
||||
},
|
||||
});
|
||||
user2 = await prisma.user.create({
|
||||
data: {
|
||||
username: "testuser-lwt-2",
|
||||
email: "testuser-lwt-2@example.com",
|
||||
name: "Test User 2",
|
||||
},
|
||||
});
|
||||
|
||||
team1 = await prisma.team.create({
|
||||
data: {
|
||||
name: "Team 1 lwt",
|
||||
slug: "team-1-lwt",
|
||||
members: {
|
||||
create: {
|
||||
userId: user1.id,
|
||||
role: MembershipRole.MEMBER,
|
||||
accepted: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
team2 = await prisma.team.create({
|
||||
data: {
|
||||
name: "Team 2 lwt",
|
||||
slug: "team-2-lwt",
|
||||
},
|
||||
});
|
||||
|
||||
eventType1 = await prisma.eventType.create({
|
||||
data: {
|
||||
title: "User 1 Event",
|
||||
slug: "user1-event-lwt",
|
||||
length: 30,
|
||||
userId: user1.id,
|
||||
},
|
||||
});
|
||||
|
||||
eventType2 = await prisma.eventType.create({
|
||||
data: {
|
||||
title: "Team 1 Event",
|
||||
slug: "team1-event-lwt",
|
||||
length: 30,
|
||||
teamId: team1.id,
|
||||
userId: user1.id,
|
||||
},
|
||||
});
|
||||
|
||||
eventType3 = await prisma.eventType.create({
|
||||
data: {
|
||||
title: "User 2 Event",
|
||||
slug: "user2-event-lwt",
|
||||
length: 30,
|
||||
userId: user2.id,
|
||||
},
|
||||
});
|
||||
|
||||
eventType4 = await prisma.eventType.create({
|
||||
data: {
|
||||
title: "Team 2 Event",
|
||||
slug: "team2-event-lwt",
|
||||
length: 30,
|
||||
teamId: team2.id,
|
||||
userId: user2.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await prisma.eventType.deleteMany({
|
||||
where: {
|
||||
id: {
|
||||
in: [eventType1.id, eventType2.id, eventType3.id, eventType4.id],
|
||||
},
|
||||
},
|
||||
});
|
||||
await prisma.team.deleteMany({
|
||||
where: {
|
||||
id: {
|
||||
in: [team1.id, team2.id],
|
||||
},
|
||||
},
|
||||
});
|
||||
await prisma.user.deleteMany({
|
||||
where: {
|
||||
id: {
|
||||
in: [user1.id, user2.id],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should return user's own event types and event types of teams they are a member of", async () => {
|
||||
const result = await listWithTeamHandler({
|
||||
ctx: {
|
||||
// we only need the id from the user object
|
||||
user: { id: user1.id } as any,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
|
||||
const resultIds = result.map((e) => e.id);
|
||||
expect(resultIds).toContain(eventType1.id);
|
||||
expect(resultIds).toContain(eventType2.id);
|
||||
|
||||
const eventType1Result = result.find((e) => e.id === eventType1.id);
|
||||
expect(eventType1Result).toBeDefined();
|
||||
expect(eventType1Result?.title).toBe(eventType1.title);
|
||||
expect(eventType1Result?.slug).toBe(eventType1.slug);
|
||||
expect(eventType1Result?.team).toBeNull();
|
||||
|
||||
const eventType2Result = result.find((e) => e.id === eventType2.id);
|
||||
expect(eventType2Result).toBeDefined();
|
||||
expect(eventType2Result?.title).toBe(eventType2.title);
|
||||
expect(eventType2Result?.slug).toBe(eventType2.slug);
|
||||
expect(eventType2Result?.team).toBeDefined();
|
||||
expect(eventType2Result?.team?.id).toBe(team1.id);
|
||||
expect(eventType2Result?.team?.name).toBe(team1.name);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,6 @@
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
import db from "@calcom/prisma";
|
||||
|
||||
import type { TrpcSessionUser } from "../../../types";
|
||||
|
||||
@@ -9,31 +11,26 @@ type ListWithTeamOptions = {
|
||||
};
|
||||
|
||||
export const listWithTeamHandler = async ({ ctx }: ListWithTeamOptions) => {
|
||||
return await prisma.eventType.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ userId: ctx.user.id },
|
||||
{
|
||||
team: {
|
||||
members: {
|
||||
some: {
|
||||
userId: ctx.user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
title: true,
|
||||
slug: true,
|
||||
},
|
||||
});
|
||||
const userId = ctx.user.id;
|
||||
const query = Prisma.sql`SELECT "public"."EventType"."id", "public"."EventType"."teamId", "public"."EventType"."title", "public"."EventType"."slug", "j1"."name" as "teamName"
|
||||
FROM "public"."EventType"
|
||||
LEFT JOIN "public"."Team" AS "j1" ON ("j1"."id") = ("public"."EventType"."teamId")
|
||||
WHERE "public"."EventType"."userId" = ${userId}
|
||||
UNION
|
||||
SELECT "public"."EventType"."id", "public"."EventType"."teamId", "public"."EventType"."title", "public"."EventType"."slug", "j1"."name" as "teamName"
|
||||
FROM "public"."EventType"
|
||||
INNER JOIN "public"."Team" AS "j1" ON ("j1"."id") = ("public"."EventType"."teamId")
|
||||
INNER JOIN "public"."Membership" AS "t2" ON "t2"."teamId" = "j1"."id"
|
||||
WHERE "t2"."userId" = ${userId} AND "t2"."accepted" = true`;
|
||||
|
||||
const result = await db.$queryRaw<
|
||||
{ id: number; teamId: number | null; title: string; slug: string; teamName: string | null }[]
|
||||
>(query);
|
||||
|
||||
return result.map((row) => ({
|
||||
id: row.id,
|
||||
team: row.teamId ? { id: row.teamId, name: row.teamName || "" } : null,
|
||||
title: row.title,
|
||||
slug: row.slug,
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billlin
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { isBase64Image } from "@calcom/lib/isBase64Image";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { uploadAvatar } from "@calcom/lib/server/avatar";
|
||||
import { checkUsername } from "@calcom/lib/server/checkUsername";
|
||||
@@ -152,7 +153,7 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions)
|
||||
}
|
||||
|
||||
// if defined AND a base 64 string, upload and update the avatar URL
|
||||
if (input.avatarUrl && input.avatarUrl.startsWith("data:image/png;base64,")) {
|
||||
if (input.avatarUrl && isBase64Image(input.avatarUrl)) {
|
||||
data.avatarUrl = await uploadAvatar({
|
||||
avatar: await resizeBase64Image(input.avatarUrl),
|
||||
userId: user.id,
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Prisma } from "@prisma/client";
|
||||
|
||||
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
||||
import { getMetadataHelpers } from "@calcom/lib/getMetadataHelpers";
|
||||
import { isBase64Image } from "@calcom/lib/isBase64Image";
|
||||
import { uploadLogo } from "@calcom/lib/server/avatar";
|
||||
import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
|
||||
import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image";
|
||||
@@ -161,12 +162,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||
metadata: mergeMetadata({ ...input.metadata }),
|
||||
};
|
||||
|
||||
if (
|
||||
input.banner &&
|
||||
(input.banner.startsWith("data:image/png;base64,") ||
|
||||
input.banner.startsWith("data:image/jpeg;base64,") ||
|
||||
input.banner.startsWith("data:image/jpg;base64,"))
|
||||
) {
|
||||
if (input.banner && isBase64Image(input.banner)) {
|
||||
const banner = await resizeBase64Image(input.banner, { maxSize: 1500 });
|
||||
data.bannerUrl = await uploadLogo({
|
||||
logo: banner,
|
||||
@@ -177,12 +173,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||
data.bannerUrl = null;
|
||||
}
|
||||
|
||||
if (
|
||||
input.logoUrl &&
|
||||
(input.logoUrl.startsWith("data:image/png;base64,") ||
|
||||
input.logoUrl.startsWith("data:image/jpeg;base64,") ||
|
||||
input.logoUrl.startsWith("data:image/jpg;base64,"))
|
||||
) {
|
||||
if (input.logoUrl && isBase64Image(input.logoUrl)) {
|
||||
data.logoUrl = await uploadLogo({
|
||||
logo: await resizeBase64Image(input.logoUrl),
|
||||
teamId: currentOrgId,
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Prisma, PrismaPromise, User, Membership, Profile } from "@prisma/c
|
||||
|
||||
import { ensureOrganizationIsReviewed } from "@calcom/ee/organizations/lib/ensureOrganizationIsReviewed";
|
||||
import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
|
||||
import { isBase64Image } from "@calcom/lib/isBase64Image";
|
||||
import { uploadAvatar } from "@calcom/lib/server/avatar";
|
||||
import { checkRegularUsername } from "@calcom/lib/server/checkRegularUsername";
|
||||
import { isOrganisationAdmin, isOrganisationOwner } from "@calcom/lib/server/queries/organisations";
|
||||
@@ -119,7 +120,7 @@ export const updateUserHandler = async ({ ctx, input }: UpdateUserOptions) => {
|
||||
timeZone: input.timeZone,
|
||||
};
|
||||
|
||||
if (input.avatar && input.avatar.startsWith("data:image/png;base64,")) {
|
||||
if (input.avatar && isBase64Image(input.avatar)) {
|
||||
const avatar = await resizeBase64Image(input.avatar);
|
||||
data.avatarUrl = await uploadAvatar({
|
||||
avatar,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { generateTeamCheckoutSession } from "@calcom/features/ee/teams/lib/payments";
|
||||
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { isBase64Image } from "@calcom/lib/isBase64Image";
|
||||
import { uploadLogo } from "@calcom/lib/server/avatar";
|
||||
import { ProfileRepository } from "@calcom/lib/server/repository/profile";
|
||||
import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image";
|
||||
@@ -106,7 +107,7 @@ export const createHandler = async ({ ctx, input }: CreateOptions) => {
|
||||
},
|
||||
});
|
||||
// Upload logo, create doesn't allow logo removal
|
||||
if (input.logo && input.logo.startsWith("data:image/png;base64,")) {
|
||||
if (input.logo && isBase64Image(input.logo)) {
|
||||
const logoUrl = await uploadLogo({
|
||||
logo: await resizeBase64Image(input.logo),
|
||||
teamId: createdTeam.id,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains";
|
||||
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
||||
import type { IntervalLimit } from "@calcom/lib/intervalLimits/intervalLimitSchema";
|
||||
import { validateIntervalLimitOrder } from "@calcom/lib/intervalLimits/validateIntervalLimitOrder";
|
||||
import { isBase64Image } from "@calcom/lib/isBase64Image";
|
||||
import { uploadLogo } from "@calcom/lib/server/avatar";
|
||||
import { isTeamAdmin } from "@calcom/lib/server/queries/teams";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
@@ -70,7 +71,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||
rrTimestampBasis: input.rrTimestampBasis,
|
||||
};
|
||||
|
||||
if (input.logo && input.logo.startsWith("data:image/png;base64,")) {
|
||||
if (input.logo && isBase64Image(input.logo)) {
|
||||
data.logoUrl = await uploadLogo({ teamId: input.id, logo: input.logo });
|
||||
} else if (typeof input.logo !== "undefined" && !input.logo) {
|
||||
data.logoUrl = null;
|
||||
|
||||
@@ -2519,8 +2519,7 @@ __metadata:
|
||||
"@axiomhq/winston": ^1.2.0
|
||||
"@calcom/platform-constants": "*"
|
||||
"@calcom/platform-enums": "*"
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.231"
|
||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2"
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.236"
|
||||
"@calcom/platform-types": "*"
|
||||
"@calcom/platform-utils": "*"
|
||||
"@calcom/prisma": "*"
|
||||
@@ -2712,8 +2711,7 @@ __metadata:
|
||||
vite-plugin-node-polyfills: ^0.22.0
|
||||
peerDependencies:
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
react-awesome-query-builder: ^5.1.2
|
||||
react-dom: ^18.0.0 || ^19.0.0
|
||||
typescript: ^5.0.0
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
@@ -3567,23 +3565,13 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@calcom/platform-libraries-0.0.2@npm:@calcom/platform-libraries@0.0.2":
|
||||
version: 0.0.2
|
||||
resolution: "@calcom/platform-libraries@npm:0.0.2"
|
||||
"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.236":
|
||||
version: 0.0.236
|
||||
resolution: "@calcom/platform-libraries@npm:0.0.236"
|
||||
dependencies:
|
||||
"@calcom/features": "*"
|
||||
"@calcom/lib": "*"
|
||||
checksum: 61b6be1b9d0be8a54ca8b1dff4b1e4db122b3c30d9203467f5347232eaf600ca3892da45a2de8abfe75327086406c33c1f6f75cd12a7147744437bc85a0ee755
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.231":
|
||||
version: 0.0.231
|
||||
resolution: "@calcom/platform-libraries@npm:0.0.231"
|
||||
dependencies:
|
||||
"@calcom/features": "*"
|
||||
"@calcom/lib": "*"
|
||||
checksum: 0abd36b041cc2ed2068c56d36aa94286f381f425b88a0d0d7cdb14498263bbece4928162d8cea00bbf4fa01f4fb60a018933f3f8e4573331d7be2b22aab2eeaa
|
||||
checksum: 7670da54aa73dfe39838ad370fd5c48ab7c8fd99f812cd68bcde76fe79d2079f8885cee24b04e3f232ef18e55c2ef9ebb940d5022751baf435d3be9c517710d9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user