Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18059e50fa | ||
|
|
852eb346ed | ||
|
|
b7e11dc112 | ||
|
|
35a373e70a | ||
|
|
02c0b721b9 | ||
|
|
29349845f8 | ||
|
|
28cb2cff64 | ||
|
|
9cb2eb3b8c | ||
|
|
6ff20b98f4 | ||
|
|
aa618dbaa2 | ||
|
|
6e3de84067 | ||
|
|
c6b19200a9 | ||
|
|
91932212e3 | ||
|
|
f81bb92beb | ||
|
|
1d1a242a72 | ||
|
|
ad4af64036 | ||
|
+5 |
2cc7827b0e | ||
|
|
ef6618743d | ||
|
|
3601a9adf1 |
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"@calcom/atoms": patch
|
||||
---
|
||||
|
||||
This change fixes an issue in the CalendarSettings atom where the redirect urls were getting only one search param
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@calcom/atoms": patch
|
||||
---
|
||||
|
||||
This change fixes date overrides breaking for availability settings atom
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
undecided:
|
||||
- "@calcom/prisma"
|
||||
@@ -38,7 +38,7 @@
|
||||
"next-axiom": "^0.17.0",
|
||||
"next-swagger-doc": "^0.3.6",
|
||||
"next-validations": "^0.2.0",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript": "^5.9.0-beta",
|
||||
"tzdata": "^1.0.30",
|
||||
"uuid": "^8.3.2",
|
||||
"zod": "^3.22.4"
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"moduleFileExtensions": ["js", "json", "ts"],
|
||||
"rootDir": ".",
|
||||
"moduleNameMapper": {
|
||||
"@/(.*)": "<rootDir>/src/$1",
|
||||
"test/(.*)": "<rootDir>/test/$1"
|
||||
},
|
||||
"testEnvironment": "node",
|
||||
"testRegex": ".e2e-spec.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"setupFiles": ["<rootDir>/test/setEnvVars.ts", "jest-date-mock"],
|
||||
"setupFilesAfterEnv": ["<rootDir>/test/jest.setup-e2e.ts"],
|
||||
"reporters": ["default", "jest-summarizing-reporter"],
|
||||
"workerIdleMemoryLimit": "512MB",
|
||||
"maxWorkers": 8
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { Config } from "jest";
|
||||
|
||||
const config: Config = {
|
||||
preset: "ts-jest",
|
||||
moduleFileExtensions: ["js", "json", "ts"],
|
||||
rootDir: ".",
|
||||
moduleNameMapper: {
|
||||
"@/(.*)": "<rootDir>/src/$1",
|
||||
"test/(.*)": "<rootDir>/test/$1",
|
||||
},
|
||||
testEnvironment: "node",
|
||||
testRegex: ".e2e-spec.ts$",
|
||||
transform: {
|
||||
"^.+\\.ts$": "ts-jest",
|
||||
},
|
||||
setupFiles: ["<rootDir>/test/setEnvVars.ts", "jest-date-mock"],
|
||||
setupFilesAfterEnv: ["<rootDir>/test/jest.setup-e2e.ts"],
|
||||
reporters: ["default", "jest-summarizing-reporter"],
|
||||
workerIdleMemoryLimit: "512MB",
|
||||
maxWorkers: 8,
|
||||
testPathIgnorePatterns: ["/dist/", "/node_modules/"],
|
||||
transformIgnorePatterns: ["/dist/", "/node_modules/"],
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"moduleFileExtensions": ["js", "json", "ts"],
|
||||
"rootDir": ".",
|
||||
"moduleNameMapper": {
|
||||
"@/(.*)": "<rootDir>/src/$1",
|
||||
"test/(.*)": "<rootDir>/test/$1"
|
||||
},
|
||||
"testEnvironment": "node",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.ts$": "ts-jest"
|
||||
},
|
||||
"setupFiles": ["<rootDir>/test/setEnvVars.ts"]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { Config } from "jest";
|
||||
|
||||
const config: Config = {
|
||||
preset: "ts-jest",
|
||||
moduleFileExtensions: ["ts", "js", "json"],
|
||||
rootDir: ".",
|
||||
moduleNameMapper: {
|
||||
"@/(.*)": "<rootDir>/src/$1",
|
||||
"test/(.*)": "<rootDir>/test/$1",
|
||||
},
|
||||
testEnvironment: "node",
|
||||
testRegex: ".*\\.spec\\.ts$",
|
||||
transform: {
|
||||
"^.+\\.ts$": "ts-jest",
|
||||
},
|
||||
setupFiles: ["<rootDir>/test/setEnvVars.ts"],
|
||||
testPathIgnorePatterns: ["/dist/", "/node_modules/"],
|
||||
transformIgnorePatterns: ["/dist/", "/node_modules/"],
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -25,9 +25,9 @@
|
||||
"test:watch": "yarn dev:build && jest --watch",
|
||||
"test:cov": "yarn dev:build && jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "yarn dev:build && NODE_OPTIONS='--max_old_space_size=8192 --experimental-vm-modules' jest --ci --forceExit --config ./jest-e2e.json",
|
||||
"test:e2e": "yarn dev:build && NODE_OPTIONS='--max_old_space_size=8192 --experimental-vm-modules' jest --ci --forceExit --config ./jest-e2e.ts",
|
||||
"test:e2e:local": "yarn test:e2e --maxWorkers=4",
|
||||
"test:e2e:watch": "yarn dev:build && jest --runInBand --detectOpenHandles --forceExit --config ./jest-e2e.json --watch",
|
||||
"test:e2e:watch": "yarn dev:build && jest --runInBand --detectOpenHandles --forceExit --config ./jest-e2e.ts --watch",
|
||||
"prisma": "yarn workspace @calcom/prisma prisma",
|
||||
"generate-schemas": "yarn prisma generate && yarn prisma format",
|
||||
"copy-swagger-module": "ts-node -r tsconfig-paths/register swagger/copy-swagger-module.ts",
|
||||
@@ -38,7 +38,7 @@
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.271",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.276",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
"@calcom/prisma": "*",
|
||||
@@ -108,7 +108,7 @@
|
||||
"ts-loader": "^9.4.3",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.1.0",
|
||||
"typescript": "^5.7.2"
|
||||
"typescript": "^5.9.0-beta"
|
||||
},
|
||||
"prisma": {
|
||||
"schema": "../../../packages/prisma/schema.prisma"
|
||||
|
||||
@@ -1834,6 +1834,88 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should return who rescheduled the booking, in the new booking", async () => {
|
||||
const rescheduledByEmail = `user-bookings-rescheduler-${randomString()}@rescheduler.com`;
|
||||
// Create the original booking that will be rescheduled
|
||||
const originalBooking = await bookingsRepositoryFixture.create({
|
||||
uid: `original-booking-uid-${eventTypeId}`,
|
||||
title: "original booking title",
|
||||
startTime: "2050-09-05T10:00:00.000Z",
|
||||
endTime: "2050-09-05T11:00:00.000Z",
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeId,
|
||||
},
|
||||
},
|
||||
status: "CANCELLED",
|
||||
rescheduledBy: rescheduledByEmail,
|
||||
metadata: {},
|
||||
responses: {
|
||||
name: "original tester",
|
||||
email: "original@example.com",
|
||||
guests: [],
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Create the new booking that is the result of the reschedule
|
||||
const newBooking = await bookingsRepositoryFixture.create({
|
||||
uid: `new-booking-uid-${eventTypeId}`,
|
||||
title: "rescheduled booking title",
|
||||
startTime: "2050-09-05T14:00:00.000Z",
|
||||
endTime: "2050-09-05T15:00:00.000Z",
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeId,
|
||||
},
|
||||
},
|
||||
status: "ACCEPTED",
|
||||
fromReschedule: originalBooking.uid,
|
||||
metadata: {},
|
||||
responses: {
|
||||
name: "new tester",
|
||||
email: "newtester@example.com",
|
||||
guests: [],
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${newBooking.uid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
// Fetch the original booking to get its rescheduledBy value
|
||||
const originalBookingFromDb = await bookingsRepositoryFixture.getByUid(originalBooking.uid);
|
||||
const expectedRescheduledBy = originalBookingFromDb?.rescheduledBy;
|
||||
|
||||
await bookingsRepositoryFixture.deleteById(originalBooking.id);
|
||||
await bookingsRepositoryFixture.deleteById(newBooking.id);
|
||||
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.uid).toEqual(newBooking.uid);
|
||||
expect(rescheduledByEmail).toEqual(expectedRescheduledBy);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("book by username and event type slug", () => {
|
||||
|
||||
@@ -101,9 +101,13 @@ export class OutputBookingsService_2024_08_13 {
|
||||
);
|
||||
const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata);
|
||||
const location = metadata?.videoCallUrl || databaseBooking.location;
|
||||
const rescheduledToUid = databaseBooking.rescheduled
|
||||
? await this.getRescheduledToUid(databaseBooking.uid)
|
||||
const rescheduledToInfo = databaseBooking.rescheduled
|
||||
? await this.getRescheduledToInfo(databaseBooking.uid)
|
||||
: undefined;
|
||||
|
||||
const rescheduledToUid = rescheduledToInfo?.uid;
|
||||
const rescheduledByEmail = databaseBooking.rescheduled ? rescheduledToInfo?.rescheduledBy : databaseBooking.rescheduledBy;
|
||||
|
||||
|
||||
const booking = {
|
||||
id: databaseBooking.id,
|
||||
@@ -116,7 +120,6 @@ export class OutputBookingsService_2024_08_13 {
|
||||
databaseBooking.status === "CANCELLED" ? databaseBooking.cancellationReason : undefined,
|
||||
cancelledByEmail: databaseBooking.status === "CANCELLED" ? databaseBooking.cancelledBy : undefined,
|
||||
reschedulingReason: bookingResponses?.rescheduledReason,
|
||||
rescheduledByEmail: databaseBooking.rescheduledBy || undefined,
|
||||
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
||||
start: databaseBooking.startTime,
|
||||
end: databaseBooking.endTime,
|
||||
@@ -142,6 +145,7 @@ export class OutputBookingsService_2024_08_13 {
|
||||
rating: databaseBooking.rating,
|
||||
icsUid: databaseBooking.iCalUID,
|
||||
rescheduledToUid,
|
||||
rescheduledByEmail,
|
||||
};
|
||||
|
||||
const bookingTransformed = plainToClass(BookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
|
||||
@@ -151,11 +155,15 @@ export class OutputBookingsService_2024_08_13 {
|
||||
return bookingTransformed;
|
||||
}
|
||||
|
||||
async getRescheduledToUid(bookingUid: string) {
|
||||
async getRescheduledToInfo(bookingUid: string): Promise<{ uid?: string; rescheduledBy?: string | null }> {
|
||||
const rescheduledTo = await this.bookingsRepository.getByFromReschedule(bookingUid);
|
||||
return rescheduledTo?.uid;
|
||||
return {
|
||||
uid: rescheduledTo?.uid,
|
||||
rescheduledBy: rescheduledTo?.rescheduledBy
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
getUserDefinedMetadata(databaseMetadata: DatabaseMetadata) {
|
||||
if (databaseMetadata === null) return {};
|
||||
|
||||
@@ -218,7 +226,6 @@ export class OutputBookingsService_2024_08_13 {
|
||||
databaseBooking.status === "CANCELLED" ? databaseBooking.cancellationReason : undefined,
|
||||
cancelledByEmail: databaseBooking.status === "CANCELLED" ? databaseBooking.cancelledBy : undefined,
|
||||
reschedulingReason: bookingResponses?.rescheduledReason,
|
||||
rescheduledByEmail: databaseBooking.rescheduledBy || undefined,
|
||||
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
||||
start: databaseBooking.startTime,
|
||||
end: databaseBooking.endTime,
|
||||
@@ -269,9 +276,12 @@ export class OutputBookingsService_2024_08_13 {
|
||||
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
||||
const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata);
|
||||
const location = metadata?.videoCallUrl || databaseBooking.location;
|
||||
const rescheduledToUid = databaseBooking.rescheduled
|
||||
? await this.getRescheduledToUid(databaseBooking.uid)
|
||||
const rescheduledToInfo = databaseBooking.rescheduled
|
||||
? await this.getRescheduledToInfo(databaseBooking.uid)
|
||||
: undefined;
|
||||
|
||||
const rescheduledToUid = rescheduledToInfo?.uid;
|
||||
const rescheduledByEmail = databaseBooking.rescheduled ? rescheduledToInfo?.rescheduledBy : databaseBooking.rescheduledBy;
|
||||
|
||||
const booking = {
|
||||
id: databaseBooking.id,
|
||||
@@ -299,6 +309,7 @@ export class OutputBookingsService_2024_08_13 {
|
||||
rating: databaseBooking.rating,
|
||||
icsUid: databaseBooking.iCalUID,
|
||||
rescheduledToUid,
|
||||
rescheduledByEmail,
|
||||
};
|
||||
|
||||
const parsed = plainToClass(GetSeatedBookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
|
||||
import { PrismaEventTypeRepository } from "@/lib/repositories/prisma-event-type.repository";
|
||||
import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository";
|
||||
import { PrismaOOORepository } from "@/lib/repositories/prisma-ooo.repository";
|
||||
import { PrismaRoutingFormResponseRepository } from "@/lib/repositories/prisma-routing-form-response.repository";
|
||||
import { PrismaScheduleRepository } from "@/lib/repositories/prisma-schedule.repository";
|
||||
@@ -7,6 +8,7 @@ import { PrismaSelectedSlotRepository } from "@/lib/repositories/prisma-selected
|
||||
import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository";
|
||||
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
|
||||
import { AvailableSlotsService } from "@/lib/services/available-slots.service";
|
||||
import { CacheService } from "@/lib/services/cache.service";
|
||||
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { Module } from "@nestjs/common";
|
||||
@@ -22,7 +24,9 @@ import { Module } from "@nestjs/common";
|
||||
PrismaEventTypeRepository,
|
||||
PrismaRoutingFormResponseRepository,
|
||||
PrismaTeamRepository,
|
||||
PrismaFeaturesRepository,
|
||||
CheckBookingLimitsService,
|
||||
CacheService,
|
||||
AvailableSlotsService,
|
||||
],
|
||||
exports: [AvailableSlotsService],
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { PrismaFeaturesRepository as PrismaFeaturesRepositoryLib } from "@calcom/platform-libraries/repositories";
|
||||
import { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
@Injectable()
|
||||
export class PrismaFeaturesRepository extends PrismaFeaturesRepositoryLib {
|
||||
constructor(private readonly dbWrite: PrismaWriteService) {
|
||||
super(dbWrite.prisma as unknown as PrismaClient);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
|
||||
import { PrismaEventTypeRepository } from "@/lib/repositories/prisma-event-type.repository";
|
||||
import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository";
|
||||
import { PrismaOOORepository } from "@/lib/repositories/prisma-ooo.repository";
|
||||
import { PrismaRoutingFormResponseRepository } from "@/lib/repositories/prisma-routing-form-response.repository";
|
||||
import { PrismaScheduleRepository } from "@/lib/repositories/prisma-schedule.repository";
|
||||
import { PrismaSelectedSlotRepository } from "@/lib/repositories/prisma-selected-slot.repository";
|
||||
import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository";
|
||||
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
|
||||
import { CacheService } from "@/lib/services/cache.service";
|
||||
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
@@ -21,7 +23,8 @@ export class AvailableSlotsService extends BaseAvailableSlotsService {
|
||||
bookingRepository: PrismaBookingRepository,
|
||||
selectedSlotRepository: PrismaSelectedSlotRepository,
|
||||
eventTypeRepository: PrismaEventTypeRepository,
|
||||
userRepository: PrismaUserRepository
|
||||
userRepository: PrismaUserRepository,
|
||||
featuresRepository: PrismaFeaturesRepository
|
||||
) {
|
||||
super({
|
||||
oooRepo: oooRepoDependency,
|
||||
@@ -32,7 +35,8 @@ export class AvailableSlotsService extends BaseAvailableSlotsService {
|
||||
selectedSlotRepo: selectedSlotRepository,
|
||||
eventTypeRepo: eventTypeRepository,
|
||||
userRepo: userRepository,
|
||||
checkBookingLimitsService: new CheckBookingLimitsService(bookingRepository),
|
||||
checkBookingLimitsService: new CheckBookingLimitsService(bookingRepository) as any,
|
||||
cacheService: new CacheService(featuresRepository),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { CacheService as BaseCacheService } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class CacheService extends BaseCacheService {
|
||||
constructor(featuresRepository: PrismaFeaturesRepository) {
|
||||
super({ featuresRepository });
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,6 @@
|
||||
"eslint-config-next": "15.1.6",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
"typescript": "^5.9.0-beta"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { cookies, headers } from "next/headers";
|
||||
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import { loadTranslations } from "@calcom/lib/server/i18n";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
import { buildLegacyCtx, decodeParams } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
|
||||
@@ -17,7 +18,7 @@ import type { PageProps as LegacyPageProps } from "~/team/type-view";
|
||||
import CachedTeamBooker, { generateMetadata as generateCachedMetadata } from "./pageWithCachedData";
|
||||
|
||||
async function isCachedTeamBookingEnabled(searchParams: SearchParams): Promise<boolean> {
|
||||
const featuresRepository = new FeaturesRepository();
|
||||
const featuresRepository = new FeaturesRepository(prisma);
|
||||
const isGloballyEnabled = await featuresRepository.checkIfFeatureIsEnabledGlobally(
|
||||
"team-booking-page-cache"
|
||||
);
|
||||
|
||||
@@ -102,7 +102,7 @@ export async function getEnrichedEventType({
|
||||
}
|
||||
|
||||
export async function shouldUseApiV2ForTeamSlots(teamId: number): Promise<boolean> {
|
||||
const featureRepo = new FeaturesRepository();
|
||||
const featureRepo = new FeaturesRepository(prisma);
|
||||
const teamHasApiV2Route = await featureRepo.checkIfTeamHasFeature(teamId, "use-api-v2-for-team-slots");
|
||||
const useApiV2 = teamHasApiV2Route && Boolean(process.env.NEXT_PUBLIC_API_V2_URL);
|
||||
|
||||
|
||||
@@ -4,11 +4,12 @@ import { notFound } from "next/navigation";
|
||||
import { CTA_CONTAINER_CLASS_NAME } from "@calcom/features/data-table/lib/utils";
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
import UpgradeTipWrapper from "./UpgradeTipWrapper";
|
||||
|
||||
export default async function InsightsLayout({ children }: { children: React.ReactNode }) {
|
||||
const featuresRepository = new FeaturesRepository();
|
||||
const featuresRepository = new FeaturesRepository(prisma);
|
||||
const insightsEnabled = await featuresRepository.checkIfFeatureIsEnabledGlobally("insights");
|
||||
|
||||
if (!insightsEnabled) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import { PermissionMapper } from "@calcom/features/pbac/domain/mappers/PermissionMapper";
|
||||
import { Resource, CrudAction } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
@@ -17,7 +18,7 @@ import SettingsLayoutAppDirClient from "./SettingsLayoutAppDirClient";
|
||||
|
||||
const getTeamFeatures = unstable_cache(
|
||||
async (teamId: number) => {
|
||||
const featuresRepository = new FeaturesRepository();
|
||||
const featuresRepository = new FeaturesRepository(prisma);
|
||||
return await featuresRepository.getTeamFeatures(teamId);
|
||||
},
|
||||
["team-features"],
|
||||
|
||||
+2
-1
@@ -11,6 +11,7 @@ import { Resource, CrudAction } from "@calcom/features/pbac/domain/types/permiss
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
import { RoleService } from "@calcom/features/pbac/services/role.service";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
@@ -29,7 +30,7 @@ const getCachedTeamRoles = unstable_cache(
|
||||
|
||||
const getCachedTeamFeature = unstable_cache(
|
||||
async (teamId: number, feature: keyof AppFlags) => {
|
||||
const featureRepo = new FeaturesRepository();
|
||||
const featureRepo = new FeaturesRepository(prisma);
|
||||
const res = await featureRepo.checkIfTeamHasFeature(teamId, feature);
|
||||
return res;
|
||||
},
|
||||
|
||||
+2
-1
@@ -12,6 +12,7 @@ import { PermissionCheckService } from "@calcom/features/pbac/services/permissio
|
||||
import { RoleService } from "@calcom/features/pbac/services/role.service";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
@@ -32,7 +33,7 @@ const getCachedTeamRoles = (teamId: number) =>
|
||||
const getCachedTeamFeature = (teamId: number, feature: keyof AppFlags) =>
|
||||
unstable_cache(
|
||||
async () => {
|
||||
const featureRepo = new FeaturesRepository();
|
||||
const featureRepo = new FeaturesRepository(prisma);
|
||||
const res = await featureRepo.checkIfTeamHasFeature(teamId, feature);
|
||||
return res;
|
||||
},
|
||||
|
||||
@@ -10,6 +10,7 @@ import getIP from "@calcom/lib/getIP";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { checkCfTurnstileToken } from "@calcom/lib/server/checkCfTurnstileToken";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { signupSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
async function ensureSignupIsEnabled(body: Record<string, string>) {
|
||||
@@ -22,7 +23,7 @@ async function ensureSignupIsEnabled(body: Record<string, string>) {
|
||||
// Still allow signups if there is a team invite
|
||||
if (token) return;
|
||||
|
||||
const featuresRepository = new FeaturesRepository();
|
||||
const featuresRepository = new FeaturesRepository(prisma);
|
||||
const signupDisabled = await featuresRepository.checkIfFeatureIsEnabledGlobally("disable-signup");
|
||||
|
||||
if (process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true" || signupDisabled) {
|
||||
|
||||
@@ -208,7 +208,7 @@ async function getHandler(request: NextRequest) {
|
||||
}
|
||||
|
||||
// Create a new response with the image buffer
|
||||
const imageResponse = new NextResponse(buffer);
|
||||
const imageResponse = new NextResponse(buffer as BodyInit);
|
||||
|
||||
// Set the appropriate headers
|
||||
imageResponse.headers.set("Content-Type", response.headers.get("content-type") || "image/png");
|
||||
|
||||
@@ -120,7 +120,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||
// Check if team has API v2 feature flag enabled (same logic as team pages)
|
||||
let useApiV2 = false;
|
||||
if (isTeamEvent && hashedLink.eventType.team?.id) {
|
||||
const featureRepo = new FeaturesRepository();
|
||||
const featureRepo = new FeaturesRepository(prisma);
|
||||
const teamHasApiV2Route = await featureRepo.checkIfTeamHasFeature(
|
||||
hashedLink.eventType.team.id,
|
||||
"use-api-v2-for-team-slots"
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { GetServerSidePropsResult } from "next";
|
||||
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
export const getServerSideProps = async (): Promise<GetServerSidePropsResult<{ isOrg: boolean }>> => {
|
||||
const featuresRepository = new FeaturesRepository();
|
||||
const featuresRepository = new FeaturesRepository(prisma);
|
||||
const organizations = await featuresRepository.checkIfFeatureIsEnabledGlobally("organizations");
|
||||
// Check if organizations are enabled
|
||||
if (!organizations) {
|
||||
|
||||
@@ -24,7 +24,7 @@ const querySchema = z.object({
|
||||
|
||||
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
|
||||
const featuresRepository = new FeaturesRepository();
|
||||
const featuresRepository = new FeaturesRepository(prisma);
|
||||
const emailVerificationEnabled = await featuresRepository.checkIfFeatureIsEnabledGlobally(
|
||||
"email-verification"
|
||||
);
|
||||
|
||||
@@ -127,7 +127,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
const organizationSettings = getOrganizationSEOSettings(team);
|
||||
const allowSEOIndexing = organizationSettings?.allowSEOIndexing ?? false;
|
||||
|
||||
const featureRepo = new FeaturesRepository();
|
||||
const featureRepo = new FeaturesRepository(prisma);
|
||||
const teamHasApiV2Route = await featureRepo.checkIfTeamHasFeature(team.id, "use-api-v2-for-team-slots");
|
||||
const useApiV2 = teamHasApiV2Route && hasApiV2RouteInEnv();
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
|
||||
// Provided by Rewrite from next.config.js
|
||||
const isOrgProfile = context.query?.isOrgProfile === "1";
|
||||
const featuresRepository = new FeaturesRepository();
|
||||
const featuresRepository = new FeaturesRepository(prisma);
|
||||
const organizationsEnabled = await featuresRepository.checkIfFeatureIsEnabledGlobally("organizations");
|
||||
|
||||
log.debug("getServerSideProps", {
|
||||
|
||||
@@ -247,7 +247,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
|
||||
const videoReference = getCalVideoReference(bookingObj.references);
|
||||
|
||||
const featureRepo = new FeaturesRepository();
|
||||
const featureRepo = new FeaturesRepository(prisma);
|
||||
const displayLogInOverlay = profile?.organizationId
|
||||
? await featureRepo.checkIfTeamHasFeature(profile.organizationId, "cal-video-log-in-overlay")
|
||||
: false;
|
||||
|
||||
@@ -1229,8 +1229,21 @@ function RecurringBookings({
|
||||
<div key={idx} className={classNames("mb-2", isCancelled ? "line-through" : "")}>
|
||||
{formatToLocalizedDate(dayjs.tz(dateStr, tz), language, "full", tz)}
|
||||
<br />
|
||||
{formatToLocalizedTime(dayjs(dateStr), language, undefined, !is24h, tz)} -{" "}
|
||||
{formatToLocalizedTime(dayjs(dateStr).add(duration, "m"), language, undefined, !is24h, tz)}{" "}
|
||||
{formatToLocalizedTime({
|
||||
date: dayjs(dateStr),
|
||||
locale: language,
|
||||
timeStyle: undefined,
|
||||
hour12: !is24h,
|
||||
timeZone: tz,
|
||||
})}{" "}
|
||||
-{" "}
|
||||
{formatToLocalizedTime({
|
||||
date: dayjs(dateStr).add(duration, "m"),
|
||||
locale: language,
|
||||
timeStyle: undefined,
|
||||
hour12: !is24h,
|
||||
timeZone: tz,
|
||||
})}{" "}
|
||||
<span className="text-bookinglight">
|
||||
({formatToLocalizedTimezone(dayjs(dateStr), language, tz)})
|
||||
</span>
|
||||
@@ -1249,14 +1262,19 @@ function RecurringBookings({
|
||||
<div key={idx} className={classNames("mb-2", isCancelled ? "line-through" : "")}>
|
||||
{formatToLocalizedDate(dayjs.tz(dateStr, tz), language, "full", tz)}
|
||||
<br />
|
||||
{formatToLocalizedTime(dayjs(dateStr), language, undefined, !is24h, tz)} -{" "}
|
||||
{formatToLocalizedTime(
|
||||
dayjs(dateStr).add(duration, "m"),
|
||||
language,
|
||||
undefined,
|
||||
!is24h,
|
||||
tz
|
||||
)}{" "}
|
||||
{formatToLocalizedTime({
|
||||
date: dayjs(dateStr),
|
||||
locale: language,
|
||||
hour12: !is24h,
|
||||
timeZone: tz,
|
||||
})}{" "}
|
||||
-{" "}
|
||||
{formatToLocalizedTime({
|
||||
date: dayjs(dateStr).add(duration, "m"),
|
||||
locale: language,
|
||||
hour12: !is24h,
|
||||
timeZone: tz,
|
||||
})}{" "}
|
||||
<span className="text-bookinglight">
|
||||
({formatToLocalizedTimezone(dayjs(dateStr), language, tz)})
|
||||
</span>
|
||||
@@ -1273,8 +1291,13 @@ function RecurringBookings({
|
||||
<div className={classNames(isCancelled ? "line-through" : "")}>
|
||||
{formatToLocalizedDate(date, language, "full", tz)}
|
||||
<br />
|
||||
{formatToLocalizedTime(date, language, undefined, !is24h, tz)} -{" "}
|
||||
{formatToLocalizedTime(dayjs(date).add(duration, "m"), language, undefined, !is24h, tz)}{" "}
|
||||
{formatToLocalizedTime({ date, locale: language, hour12: !is24h, timeZone: tz })} -{" "}
|
||||
{formatToLocalizedTime({
|
||||
date: dayjs(date).add(duration, "m"),
|
||||
locale: language,
|
||||
hour12: !is24h,
|
||||
timeZone: tz,
|
||||
})}{" "}
|
||||
<span className="text-bookinglight">({formatToLocalizedTimezone(date, language, tz)})</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -288,6 +288,7 @@ export function VideoMeetingInfo(props: VideoMeetingInfo) {
|
||||
|
||||
const endTime = new Date(booking.endTime);
|
||||
const startTime = new Date(booking.startTime);
|
||||
const timeZone = booking.user?.timeZone;
|
||||
|
||||
useDailyEvent("left-meeting", () => {
|
||||
if (rediectAttendeeToOnExit) {
|
||||
@@ -306,11 +307,11 @@ export function VideoMeetingInfo(props: VideoMeetingInfo) {
|
||||
<h3>{t("what")}:</h3>
|
||||
<p>{booking.title}</p>
|
||||
<h3>{t("invitee_timezone")}:</h3>
|
||||
<p>{booking.user?.timeZone}</p>
|
||||
<p>{timeZone}</p>
|
||||
<h3>{t("when")}:</h3>
|
||||
<p suppressHydrationWarning={true}>
|
||||
{formatToLocalizedDate(startTime)} <br />
|
||||
{formatToLocalizedTime(startTime)}
|
||||
{formatToLocalizedTime({ date: startTime, timeZone })}
|
||||
</p>
|
||||
<h3>{t("time_left")}</h3>
|
||||
<ProgressBar
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "5.5.9",
|
||||
"version": "5.5.10",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
@@ -195,7 +195,7 @@
|
||||
"tailwindcss": "^3.3.3",
|
||||
"tailwindcss-animate": "^1.0.6",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.7.2"
|
||||
"typescript": "^5.9.0-beta"
|
||||
},
|
||||
"nextBundleAnalysis": {
|
||||
"budget": 358400,
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
---
|
||||
title: "How to Add a New Booking Chart to Cal.com Insights Page"
|
||||
---
|
||||
|
||||
This guide walks you through creating a new booking chart component for the insights page, covering the entire stack from UI component to backend service.
|
||||
|
||||
## Overview
|
||||
|
||||
The insights booking system follows this architecture:
|
||||
|
||||
```
|
||||
UI Component → tRPC Handler → Insights Service → Database Query → Response
|
||||
```
|
||||
|
||||
<Steps>
|
||||
<Step title="Create the UI Component">
|
||||
|
||||
Create your chart component in `packages/features/insights/components/booking/`:
|
||||
|
||||
```typescript
|
||||
// packages/features/insights/components/booking/MyNewChart.tsx
|
||||
import { LineChart, XAxis, YAxis, CartesianGrid, Tooltip, Line, ResponsiveContainer } from "recharts";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
|
||||
import { useInsightsBookingParameters } from "../../hooks/useInsightsBookingParameters";
|
||||
import { ChartCard } from "../ChartCard";
|
||||
import { LoadingInsight } from "../LoadingInsights";
|
||||
|
||||
export const MyNewChart = () => {
|
||||
const { t } = useLocale();
|
||||
const insightsBookingParams = useInsightsBookingParameters();
|
||||
|
||||
const { data, isSuccess, isPending } = trpc.viewer.insights.myNewChartData.useQuery(insightsBookingParams, {
|
||||
staleTime: 180000, // 3 minutes
|
||||
refetchOnWindowFocus: false,
|
||||
trpc: { context: { skipBatch: true } },
|
||||
});
|
||||
|
||||
if (isPending) return <LoadingInsight />;
|
||||
|
||||
return (
|
||||
<ChartCard title={t("my_new_chart_title")}>
|
||||
{isSuccess && data?.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<LineChart data={data}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="date" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Line type="monotone" dataKey="value" stroke="#8884d8" strokeWidth={2} />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<p className="text-gray-500">{t("no_data_yet")}</p>
|
||||
</div>
|
||||
)}
|
||||
</ChartCard>
|
||||
);
|
||||
};
|
||||
```
|
||||
</Step>
|
||||
<Step title="Add Component to Barrel Export">
|
||||
Update the booking components index file:
|
||||
|
||||
```typescript
|
||||
// packages/features/insights/components/booking/index.ts
|
||||
export { AverageEventDurationChart } from "./AverageEventDurationChart";
|
||||
export { BookingKPICards } from "./BookingKPICards";
|
||||
// ... existing exports
|
||||
export { MyNewChart } from "./MyNewChart"; // Add this line
|
||||
```
|
||||
</Step>
|
||||
<Step title="Add Component to Insights View">
|
||||
|
||||
Add your component to the main insights page:
|
||||
|
||||
```typescript
|
||||
// apps/web/modules/insights/insights-view.tsx
|
||||
import {
|
||||
AverageEventDurationChart,
|
||||
BookingKPICards, // ... existing imports
|
||||
MyNewChart, // Add this import
|
||||
} from "@calcom/features/insights/components/booking";
|
||||
|
||||
export default function InsightsPage() {
|
||||
// ... existing code
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Existing components */}
|
||||
<BookingKPICards />
|
||||
<EventTrendsChart />
|
||||
|
||||
{/* Add your new chart */}
|
||||
<MyNewChart />
|
||||
|
||||
{/* Other existing components */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
</Step>
|
||||
<Step title="Create tRPC Handler">
|
||||
|
||||
Add the tRPC endpoint in the insights router using the `getInsightsBookingService()` DI container function:
|
||||
|
||||
```typescript
|
||||
// packages/features/insights/server/trpc-router.ts
|
||||
import { bookingRepositoryBaseInputSchema } from "@calcom/features/insights/server/raw-data.schema";
|
||||
import { userBelongsToTeamProcedure } from "@calcom/trpc/server/procedures/authedProcedure";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
export const insightsRouter = router({
|
||||
// ... existing procedures
|
||||
|
||||
myNewChartData: userBelongsToTeamProcedure
|
||||
.input(bookingRepositoryBaseInputSchema)
|
||||
.query(async ({ ctx, input }) => {
|
||||
// `createInsightsBookingService` is defined at the root level in this file
|
||||
const insightsBookingService = createInsightsBookingService(ctx, input);
|
||||
|
||||
try {
|
||||
return await insightsBookingService.getMyNewChartData();
|
||||
} catch (e) {
|
||||
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
||||
}
|
||||
}),
|
||||
});
|
||||
```
|
||||
</Step>
|
||||
<Step title="Add Service Method to InsightsBookingBaseService">
|
||||
Add your new method to the `InsightsBookingBaseService` class:
|
||||
|
||||
```typescript
|
||||
// packages/lib/server/service/InsightsBookingBaseService.ts
|
||||
export class InsightsBookingBaseService {
|
||||
// ... existing methods
|
||||
|
||||
async getMyNewChartData() {
|
||||
const baseConditions = await this.getBaseConditions();
|
||||
|
||||
// Example: Get booking counts by day using raw SQL for performance
|
||||
const data = await this.prisma.$queryRaw<
|
||||
Array<{
|
||||
date: Date;
|
||||
bookingsCount: number;
|
||||
}>
|
||||
>`
|
||||
SELECT
|
||||
DATE("createdAt") as date,
|
||||
COUNT(*)::int as "bookingsCount"
|
||||
FROM "BookingTimeStatusDenormalized"
|
||||
WHERE ${baseConditions}
|
||||
GROUP BY DATE("createdAt")
|
||||
ORDER BY date ASC
|
||||
`;
|
||||
|
||||
// Transform the data for the chart
|
||||
return data.map((item) => ({
|
||||
date: item.date.toISOString().split("T")[0], // Format as YYYY-MM-DD
|
||||
value: item.bookingsCount,
|
||||
}));
|
||||
}
|
||||
}
|
||||
```
|
||||
</Step>
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use `getInsightsBookingService()`**: Always use the DI container function for consistent service creation
|
||||
2. **Raw SQL for Performance**: Use `$queryRaw` for complex aggregations and better performance
|
||||
3. **Base Conditions**: Always use `await this.getBaseConditions()` for proper filtering and permissions
|
||||
4. **Error Handling**: Wrap service calls in try-catch blocks with `TRPCError`
|
||||
5. **Loading States**: Always show loading indicators with `LoadingInsight`
|
||||
6. **Consistent Styling**: Use `recharts` for new charts.
|
||||
7. **Date Handling**: Use `getDateRanges()` and `getTimeView()` for time-based charts
|
||||
@@ -110,6 +110,11 @@
|
||||
"icon": "code-simple",
|
||||
"pages": ["developing/guides/embeds/embed-events"]
|
||||
}
|
||||
{
|
||||
"group": "Insights",
|
||||
"icon": "chart-bar",
|
||||
"pages": ["developing/guides/insights/add-new-booking-charts"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -90,6 +90,7 @@ Below is a list of props that can be passed to the booker atom.
|
||||
| customClassNames | No | To pass in custom classnames from outside for styling the atom |
|
||||
| month | No | The exact month for displaying a user's availability; defaults to the current month |
|
||||
| selectedDate | No | Default selected date for which the slot picker opens |
|
||||
| startTime | No | Custom start time for the Booker that allows users to decide the first available date. Accepts JavaScript Date object or date string in format `YYYY-MM-DD` (e.g., `"2025-08-20"` or `new Date("2025-08-20")`) |
|
||||
| hideBranding | No | For hiding any branding on the booker |
|
||||
| isAway | No | Sets the booker component to the away state |
|
||||
| allowsDynamicBooking | No | Boolean indicating if the booking is a dynamic booking |
|
||||
|
||||
+1
-1
@@ -109,7 +109,7 @@
|
||||
"prismock": "^1.33.4",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"tsc-absolute": "^1.0.0",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript": "5.9.0-beta",
|
||||
"vitest": "^2.1.1",
|
||||
"vitest-fetch-mock": "^0.3.0",
|
||||
"vitest-mock-extended": "^2.0.2"
|
||||
|
||||
@@ -31,6 +31,6 @@
|
||||
"eslint-plugin-react": "^7.30.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.7.2"
|
||||
"typescript": "^5.9.0-beta"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"dependencies": {
|
||||
"@calcom/eslint-plugin-eslint": "*",
|
||||
"@todesktop/tailwind-variants": "^1.0.0",
|
||||
"eslint-config-next": "^13.2.1",
|
||||
"eslint-config-next": "^15.4.5",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-config-turbo": "^0.0.7",
|
||||
"eslint-plugin-playwright": "^0.12.0",
|
||||
@@ -33,6 +33,6 @@
|
||||
"prettier-plugin-tailwindcss": "^0.2.5",
|
||||
"tailwind-scrollbar": "^2.0.1",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "^5.7.2"
|
||||
"typescript": "^5.9.0-beta"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { getErrorFromUnknown } from "@calcom/lib/errors";
|
||||
import isSmsCalEmail from "@calcom/lib/isSmsCalEmail";
|
||||
import { serverConfig } from "@calcom/lib/serverConfig";
|
||||
import { setTestEmail } from "@calcom/lib/testEmails";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
import { sanitizeDisplayName } from "../lib/sanitizeDisplayName";
|
||||
|
||||
@@ -29,7 +30,7 @@ export default class BaseEmail {
|
||||
return {};
|
||||
}
|
||||
public async sendEmail() {
|
||||
const featuresRepository = new FeaturesRepository();
|
||||
const featuresRepository = new FeaturesRepository(prisma);
|
||||
const emailsDisabled = await featuresRepository.checkIfFeatureIsEnabledGlobally("emails");
|
||||
/** If email kill switch exists and is active, we prevent emails being sent. */
|
||||
if (emailsDisabled) {
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss": "^8.4.18",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript": "^5.9.0-beta",
|
||||
"vite": "^4.1.2",
|
||||
"vite-plugin-environment": "^1.1.3"
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
"@vitejs/plugin-react": "^2.2.0",
|
||||
"eslint": "^8.34.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript": "^5.9.0-beta",
|
||||
"vite": "^4.5.2"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
],
|
||||
"types": "./dist/index.d.ts",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7.2",
|
||||
"typescript": "^5.9.0-beta",
|
||||
"vite": "^4.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"@typescript-eslint/parser": "^5.52.0",
|
||||
"@typescript-eslint/utils": "^5.52.0",
|
||||
"ts-node-maintained": "^10.9.5",
|
||||
"typescript": "^5.7.2"
|
||||
"typescript": "^5.9.0-beta"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/eslint": "^8.4.5"
|
||||
|
||||
@@ -34,7 +34,7 @@ export const sendEmailVerification = async ({
|
||||
}: VerifyEmailType) => {
|
||||
const token = randomBytes(32).toString("hex");
|
||||
const translation = await getTranslation(language ?? "en", "common");
|
||||
const featuresRepository = new FeaturesRepository();
|
||||
const featuresRepository = new FeaturesRepository(prisma);
|
||||
const emailVerification = await featuresRepository.checkIfFeatureIsEnabledGlobally("email-verification");
|
||||
|
||||
if (!emailVerification) {
|
||||
@@ -127,7 +127,7 @@ interface ChangeOfEmail {
|
||||
export const sendChangeOfEmailVerification = async ({ user, language }: ChangeOfEmail) => {
|
||||
const token = randomBytes(32).toString("hex");
|
||||
const translation = await getTranslation(language ?? "en", "common");
|
||||
const featuresRepository = new FeaturesRepository();
|
||||
const featuresRepository = new FeaturesRepository(prisma);
|
||||
const emailVerification = await featuresRepository.checkIfFeatureIsEnabledGlobally("email-verification");
|
||||
|
||||
if (!emailVerification) {
|
||||
|
||||
@@ -102,7 +102,7 @@ export const DatePicker = ({
|
||||
moveToNextMonthOnNoAvailability();
|
||||
|
||||
// Determine if this is a compact sidebar view based on layout
|
||||
const isCompact = layout !== "month_view";
|
||||
const isCompact = layout !== "month_view" && layout !== "mobile";
|
||||
|
||||
const periodData: PeriodData = {
|
||||
...{
|
||||
|
||||
@@ -27,7 +27,6 @@ import getICalUID from "@calcom/emails/lib/getICalUID";
|
||||
import { CalendarEventBuilder } from "@calcom/features/CalendarEventBuilder";
|
||||
import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger";
|
||||
import { isEventTypeLoggingEnabled } from "@calcom/features/bookings/lib/isEventTypeLoggingEnabled";
|
||||
import { getShouldServeCache } from "@calcom/features/calendar-cache/lib/getShouldServeCache";
|
||||
import AssignmentReasonRecorder from "@calcom/features/ee/round-robin/assignmentReason/AssignmentReasonRecorder";
|
||||
import {
|
||||
allowDisablingAttendeeConfirmationEmails,
|
||||
@@ -52,6 +51,7 @@ import {
|
||||
getFirstDelegationConferencingCredentialAppLocation,
|
||||
} from "@calcom/lib/delegationCredential/server";
|
||||
import { getCheckBookingAndDurationLimitsService } from "@calcom/lib/di/containers/booking-limits";
|
||||
import { getCacheService } from "@calcom/lib/di/containers/cache";
|
||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
||||
import { getEventName, updateHostInEventName } from "@calcom/lib/event";
|
||||
@@ -387,6 +387,23 @@ export type BookingHandlerInput = {
|
||||
forcedSlug?: string;
|
||||
} & PlatformParams;
|
||||
|
||||
function formatAvailabilitySnapshot(data: {
|
||||
dateRanges: { start: dayjs.Dayjs; end: dayjs.Dayjs }[];
|
||||
oooExcludedDateRanges: { start: dayjs.Dayjs; end: dayjs.Dayjs }[];
|
||||
}) {
|
||||
return {
|
||||
...data,
|
||||
dateRanges: data.dateRanges.map(({ start, end }) => ({
|
||||
start: start.toISOString(),
|
||||
end: end.toISOString(),
|
||||
})),
|
||||
oooExcludedDateRanges: data.oooExcludedDateRanges.map(({ start, end }) => ({
|
||||
start: start.toISOString(),
|
||||
end: end.toISOString(),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
async function handler(
|
||||
input: BookingHandlerInput,
|
||||
bookingDataSchemaGetter: BookingDataSchemaGetter = getBookingDataSchema
|
||||
@@ -539,7 +556,8 @@ async function handler(
|
||||
}
|
||||
}
|
||||
|
||||
const shouldServeCache = await getShouldServeCache(_shouldServeCache, eventType.team?.id);
|
||||
const cacheService = getCacheService();
|
||||
const shouldServeCache = await cacheService.getShouldServeCache(_shouldServeCache, eventType.team?.id);
|
||||
|
||||
const isTeamEventType =
|
||||
!!eventType.schedulingType && ["COLLECTIVE", "ROUND_ROBIN"].includes(eventType.schedulingType);
|
||||
@@ -1396,7 +1414,10 @@ async function handler(
|
||||
|
||||
logger.info(`Booking created`, {
|
||||
bookingUid: booking.uid,
|
||||
availabilitySnapshot: organizerUserAvailability?.availabilityData,
|
||||
selectedCalendarIds: organizerUser.allSelectedCalendars?.map((c) => c.id) ?? [],
|
||||
availabilitySnapshot: organizerUserAvailability?.availabilityData
|
||||
? formatAvailabilitySnapshot(organizerUserAvailability.availabilityData)
|
||||
: null,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import { getCredentialForCalendarCache } from "@calcom/lib/delegationCredential/server";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { Calendar } from "@calcom/types/Calendar";
|
||||
|
||||
import { CalendarCacheRepository } from "./calendar-cache.repository";
|
||||
@@ -20,7 +21,7 @@ export class CalendarCache {
|
||||
return await CalendarCache.init(calendarForCalendarCache);
|
||||
}
|
||||
static async init(calendar: Calendar | null): Promise<ICalendarCacheRepository> {
|
||||
const featureRepo = new FeaturesRepository();
|
||||
const featureRepo = new FeaturesRepository(prisma);
|
||||
const isCalendarCacheEnabledGlobally = await featureRepo.checkIfFeatureIsEnabledGlobally(
|
||||
"calendar-cache"
|
||||
);
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import type { IFeaturesRepository } from "@calcom/features/flags/features.repository.interface";
|
||||
|
||||
export async function getShouldServeCache(shouldServeCache?: boolean | undefined, teamId?: number) {
|
||||
if (typeof shouldServeCache === "boolean") return shouldServeCache;
|
||||
if (!teamId) return undefined;
|
||||
const featureRepo = new FeaturesRepository();
|
||||
return await featureRepo.checkIfTeamHasFeature(teamId, "calendar-cache-serve");
|
||||
export interface ICacheService {
|
||||
featuresRepository: IFeaturesRepository;
|
||||
}
|
||||
|
||||
export class CacheService {
|
||||
constructor(private readonly dependencies: ICacheService) {}
|
||||
|
||||
async getShouldServeCache(shouldServeCache?: boolean | undefined, teamId?: number) {
|
||||
if (typeof shouldServeCache === "boolean") return shouldServeCache;
|
||||
if (!teamId) return undefined;
|
||||
return await this.dependencies.featuresRepository.checkIfTeamHasFeature(teamId, "calendar-cache-serve");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,8 +194,10 @@ const Days = ({
|
||||
const getPadding = (day: number) => (browsingDate.set("date", day).day() - weekStart + 7) % 7;
|
||||
const totalDays = daysInMonth(browsingDate);
|
||||
|
||||
const showNextMonthDays = isSecondWeekOver && !isCompact;
|
||||
|
||||
// Only apply end-of-month logic for main monthly view (not compact sidebar)
|
||||
if (isSecondWeekOver && !isCompact) {
|
||||
if (showNextMonthDays) {
|
||||
const startDay = 8;
|
||||
const pad = getPadding(startDay);
|
||||
days = Array(pad).fill(null);
|
||||
@@ -337,7 +339,7 @@ const Days = ({
|
||||
active={isActive(day)}
|
||||
away={away}
|
||||
emoji={emoji}
|
||||
showMonthTooltip={isSecondWeekOver && !isCompact}
|
||||
showMonthTooltip={showNextMonthDays && !disabled && day.month() !== browsingDate.month()}
|
||||
isFirstDayOfNextMonth={isFirstDayOfNextMonth}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { MembershipRole } from "@calcom/prisma/client";
|
||||
|
||||
export const getServerSideProps = async ({ req }: GetServerSidePropsContext) => {
|
||||
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
|
||||
const featuresRepository = new FeaturesRepository();
|
||||
const featuresRepository = new FeaturesRepository(prisma);
|
||||
const organizationsEnabled = await featuresRepository.checkIfFeatureIsEnabledGlobally("organizations");
|
||||
// Check if organizations are enabled
|
||||
if (!organizationsEnabled) {
|
||||
|
||||
@@ -277,7 +277,7 @@ export const scheduleEmailReminder = async (args: scheduleEmailReminderArgs) =>
|
||||
|
||||
const isSendgridEnabled = !!(process.env.SENDGRID_API_KEY && process.env.SENDGRID_EMAIL);
|
||||
|
||||
const featureRepo = new FeaturesRepository();
|
||||
const featureRepo = new FeaturesRepository(prisma);
|
||||
|
||||
const isWorkflowSmtpEmailsEnabled = teamId
|
||||
? await featureRepo.checkIfTeamHasFeature(teamId, "workflow-smtp-emails")
|
||||
|
||||
@@ -1103,7 +1103,6 @@ describe("Workflow SMTP Emails Feature Flag", () => {
|
||||
});
|
||||
|
||||
test("should use SMTP when team has workflow-smtp-emails feature", async () => {
|
||||
const featuresRepository = new FeaturesRepository();
|
||||
vi.spyOn(FeaturesRepository.prototype, "checkIfTeamHasFeature").mockResolvedValue(true);
|
||||
|
||||
await scheduleEmailReminder({
|
||||
@@ -1115,7 +1114,6 @@ describe("Workflow SMTP Emails Feature Flag", () => {
|
||||
});
|
||||
|
||||
test("should use SMTP when user has workflow-smtp-emails feature", async () => {
|
||||
const featuresRepository = new FeaturesRepository();
|
||||
vi.spyOn(FeaturesRepository.prototype, "checkIfUserHasFeature").mockResolvedValue(true);
|
||||
|
||||
await scheduleEmailReminder({
|
||||
@@ -1127,7 +1125,6 @@ describe("Workflow SMTP Emails Feature Flag", () => {
|
||||
});
|
||||
|
||||
test("should use SendGrid when workflow-smtp-emails feature is not enabled for team", async () => {
|
||||
const featuresRepository = new FeaturesRepository();
|
||||
vi.spyOn(FeaturesRepository.prototype, "checkIfTeamHasFeature").mockResolvedValue(false);
|
||||
|
||||
await scheduleEmailReminder({
|
||||
@@ -1140,7 +1137,6 @@ describe("Workflow SMTP Emails Feature Flag", () => {
|
||||
});
|
||||
|
||||
test("should use SendGrid when workflow-smtp-emails feature is not enabled for user", async () => {
|
||||
const featuresRepository = new FeaturesRepository();
|
||||
vi.spyOn(FeaturesRepository.prototype, "checkIfUserHasFeature").mockResolvedValue(false);
|
||||
|
||||
await scheduleEmailReminder({
|
||||
@@ -1153,9 +1149,9 @@ describe("Workflow SMTP Emails Feature Flag", () => {
|
||||
});
|
||||
|
||||
test("should use SMTP when SendGrid is not configured", async () => {
|
||||
const featuresRepository = new FeaturesRepository();
|
||||
vi.spyOn(featuresRepository, "checkIfTeamHasFeature").mockResolvedValue(false);
|
||||
vi.spyOn(featuresRepository, "checkIfUserHasFeature").mockResolvedValue(false);
|
||||
vi.spyOn(FeaturesRepository.prototype, "checkIfTeamHasFeature").mockResolvedValue(false);
|
||||
|
||||
vi.spyOn(FeaturesRepository.prototype, "checkIfUserHasFeature").mockResolvedValue(false);
|
||||
|
||||
delete process.env.SENDGRID_API_KEY;
|
||||
delete process.env.SENDGRID_EMAIL;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { describe, expect, beforeAll, afterAll, beforeEach, it } from "vitest";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import type { AppFlags } from "./config";
|
||||
import { FeaturesRepository } from "./features.repository";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
const featuresRepository = new FeaturesRepository();
|
||||
const featuresRepository = new FeaturesRepository(prisma);
|
||||
|
||||
// Access private clearCache method through type assertion
|
||||
const clearCache = () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { captureException } from "@sentry/nextjs";
|
||||
|
||||
import db from "@calcom/prisma";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
import type { AppFlags, TeamFeatures } from "./config";
|
||||
import type { IFeaturesRepository } from "./features.repository.interface";
|
||||
@@ -18,6 +18,8 @@ interface CacheOptions {
|
||||
export class FeaturesRepository implements IFeaturesRepository {
|
||||
private static featuresCache: { data: any[]; expiry: number } | null = null;
|
||||
|
||||
constructor(private prismaClient: PrismaClient) {}
|
||||
|
||||
private clearCache() {
|
||||
FeaturesRepository.featuresCache = null;
|
||||
}
|
||||
@@ -32,7 +34,7 @@ export class FeaturesRepository implements IFeaturesRepository {
|
||||
return FeaturesRepository.featuresCache.data;
|
||||
}
|
||||
|
||||
const features = await db.feature.findMany({
|
||||
const features = await this.prismaClient.feature.findMany({
|
||||
orderBy: { slug: "asc" },
|
||||
cacheStrategy: { swr: 300, ttl: 300 },
|
||||
});
|
||||
@@ -64,7 +66,7 @@ export class FeaturesRepository implements IFeaturesRepository {
|
||||
* @returns Promise<{ [slug: string]: boolean } | null>
|
||||
*/
|
||||
public async getTeamFeatures(teamId: number) {
|
||||
const result = await db.teamFeatures.findMany({
|
||||
const result = await this.prismaClient.teamFeatures.findMany({
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
@@ -122,7 +124,7 @@ export class FeaturesRepository implements IFeaturesRepository {
|
||||
* FIXME refactor when upgrading prismock
|
||||
* https://github.com/morintd/prismock/issues/592
|
||||
*/
|
||||
const userHasFeature = await db.userFeatures.findFirst({
|
||||
const userHasFeature = await this.prismaClient.userFeatures.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
featureId: slug,
|
||||
@@ -180,7 +182,7 @@ export class FeaturesRepository implements IFeaturesRepository {
|
||||
LIMIT 1;
|
||||
`;
|
||||
|
||||
const result = await db.$queryRaw<unknown[]>(query);
|
||||
const result = await this.prismaClient.$queryRaw<unknown[]>(query);
|
||||
return result.length > 0;
|
||||
} catch (err) {
|
||||
captureException(err);
|
||||
@@ -199,7 +201,7 @@ export class FeaturesRepository implements IFeaturesRepository {
|
||||
async checkIfTeamHasFeature(teamId: number, featureId: keyof AppFlags): Promise<boolean> {
|
||||
try {
|
||||
// Early return if team has feature directly assigned
|
||||
const teamHasFeature = await db.teamFeatures.findUnique({
|
||||
const teamHasFeature = await this.prismaClient.teamFeatures.findUnique({
|
||||
where: {
|
||||
teamId_featureId: {
|
||||
teamId,
|
||||
@@ -238,7 +240,7 @@ export class FeaturesRepository implements IFeaturesRepository {
|
||||
LIMIT 1;
|
||||
`;
|
||||
|
||||
const result = await db.$queryRaw<unknown[]>(query);
|
||||
const result = await this.prismaClient.$queryRaw<unknown[]>(query);
|
||||
return result.length > 0;
|
||||
} catch (err) {
|
||||
captureException(err);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { startSpan } from "@sentry/nextjs";
|
||||
|
||||
import { FeaturesRepository } from "../features.repository";
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
/**
|
||||
* Use Cases represent individual operations, like "Create Feature" or "Sign In" or "Toggle Feature".
|
||||
@@ -11,7 +12,7 @@ import { FeaturesRepository } from "../features.repository";
|
||||
*/
|
||||
export function checkIfUserHasFeatureUseCase(userId: number, slug: string): Promise<boolean> {
|
||||
return startSpan({ name: "checkIfUserHasFeature UseCase", op: "function" }, async () => {
|
||||
const featuresRepository = new FeaturesRepository();
|
||||
const featuresRepository = new FeaturesRepository(prisma);
|
||||
|
||||
return await featuresRepository.checkIfUserHasFeature(userId, slug);
|
||||
});
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import prisma from "@calcom/prisma";
|
||||
import publicProcedure from "@calcom/trpc/server/procedures/publicProcedure";
|
||||
|
||||
import { FeaturesRepository } from "../../features.repository";
|
||||
|
||||
/**
|
||||
* TRPC procedure that returns a map of all feature flags and their enabled status.
|
||||
* Uses the FeaturesRepository to handle caching and database access.
|
||||
*/
|
||||
export const map = publicProcedure.query(async () => {
|
||||
const featuresRepository = new FeaturesRepository();
|
||||
const featuresRepository = new FeaturesRepository(prisma);
|
||||
return featuresRepository.getFeatureFlagMap();
|
||||
});
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import type { AppFlags } from "@calcom/features/flags/config";
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import publicProcedure from "@calcom/trpc/server/procedures/publicProcedure";
|
||||
import { router } from "@calcom/trpc/server/trpc";
|
||||
|
||||
import { FeaturesRepository } from "../features.repository";
|
||||
import { map } from "./procedures/map";
|
||||
|
||||
export const featureFlagRouter = router({
|
||||
list: publicProcedure.query(async () => {
|
||||
const featuresRepository = new FeaturesRepository();
|
||||
const featuresRepository = new FeaturesRepository(prisma);
|
||||
return featuresRepository.getAllFeatures();
|
||||
}),
|
||||
checkTeamFeature: publicProcedure
|
||||
@@ -20,7 +21,7 @@ export const featureFlagRouter = router({
|
||||
})
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
const featuresRepository = new FeaturesRepository();
|
||||
const featuresRepository = new FeaturesRepository(prisma);
|
||||
return featuresRepository.checkIfTeamHasFeature(input.teamId, input.feature as keyof AppFlags);
|
||||
}),
|
||||
map,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Controller, useFormContext } from "react-hook-form";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
import { InfoBadge } from "@calcom/ui/components/badge";
|
||||
import { Label } from "@calcom/ui/components/form";
|
||||
@@ -19,6 +20,19 @@ import {
|
||||
} from "./useShouldBeDisabledDueToPrefill";
|
||||
import { getTranslatedConfig as getTranslatedVariantsConfig } from "./utils/variantsConfig";
|
||||
|
||||
// helper to render markdown label safely
|
||||
const renderLabel = (field: Partial<RhfFormField>) => {
|
||||
if (field.labelAsSafeHtml) {
|
||||
return (
|
||||
<span
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: markdownToSafeHTML(field.labelAsSafeHtml) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <span>{field.label}</span>;
|
||||
};
|
||||
|
||||
type RhfForm = {
|
||||
fields: z.infer<typeof fieldsSchema>;
|
||||
};
|
||||
@@ -155,7 +169,7 @@ const WithLabel = ({
|
||||
field.label && (
|
||||
<div className="mb-2 flex items-center">
|
||||
<Label className="!mb-0 flex items-center" htmlFor={htmlFor}>
|
||||
<span>{field.label}</span>
|
||||
{renderLabel(field)}
|
||||
<span className="text-emphasis -mb-1 ml-1 text-sm font-medium leading-none">
|
||||
{!readOnly && field.required ? "*" : ""}
|
||||
</span>
|
||||
|
||||
@@ -4,4 +4,4 @@ import type { FieldType } from "./schema";
|
||||
* Once a component supports `labelAsSafeHtml`, add it's field's type here
|
||||
* A whitelist is needed because unless we explicitly use dangerouslySetInnerHTML, React will escape the HTML
|
||||
*/
|
||||
export const fieldsThatSupportLabelAsSafeHtml: FieldType[] = ["boolean"];
|
||||
export const fieldsThatSupportLabelAsSafeHtml: FieldType[] = ["boolean", "radio"];
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
import prismock from "../../../tests/libs/__mocks__/prisma";
|
||||
|
||||
import {
|
||||
createBookingScenario,
|
||||
getScenarioData,
|
||||
getGoogleCalendarCredential,
|
||||
TestData,
|
||||
getOrganizer,
|
||||
mockSuccessfulVideoMeetingCreation,
|
||||
mockCalendarToHaveNoBusySlots,
|
||||
mockNoTranslations,
|
||||
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
|
||||
|
||||
import type { NextApiRequest } from "next";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
|
||||
vi.mock("@calcom/features/notifications/sendNotification", () => ({
|
||||
sendNotification: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/lib/videoClient", () => ({
|
||||
createInstantMeetingWithCalVideo: vi.fn().mockResolvedValue({
|
||||
type: "daily_video",
|
||||
id: "MOCK_INSTANT_MEETING_ID",
|
||||
password: "MOCK_INSTANT_PASS",
|
||||
url: "http://mock-dailyvideo.example.com/instant-meeting-url",
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("handleInstantMeeting", () => {
|
||||
beforeEach(() => {
|
||||
mockNoTranslations();
|
||||
});
|
||||
describe("team event instant meeting", () => {
|
||||
it("should successfully create instant meeting for team event", async () => {
|
||||
const handler = (await import("./handleInstantMeeting")).default;
|
||||
const organizer = getOrganizer({
|
||||
name: "Organizer",
|
||||
email: "organizer@example.com",
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
});
|
||||
|
||||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
||||
|
||||
await createBookingScenario(
|
||||
getScenarioData({
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slotInterval: 45,
|
||||
length: 45,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
team: {
|
||||
id: 1,
|
||||
},
|
||||
instantMeetingExpiryTimeOffsetInSeconds: 90,
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
apps: [TestData.apps["daily-video"], TestData.apps["google-calendar"]],
|
||||
})
|
||||
);
|
||||
|
||||
mockSuccessfulVideoMeetingCreation({
|
||||
metadataLookupKey: "dailyvideo",
|
||||
videoMeetingData: {
|
||||
id: "MOCK_ID",
|
||||
password: "MOCK_PASS",
|
||||
url: `http://mock-dailyvideo.example.com/meeting-1`,
|
||||
},
|
||||
});
|
||||
mockCalendarToHaveNoBusySlots("googlecalendar", {
|
||||
create: {
|
||||
uid: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
|
||||
},
|
||||
});
|
||||
|
||||
const mockReqBody = {
|
||||
eventTypeId: 1,
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
timeZone: "UTC",
|
||||
language: "en",
|
||||
start: `${plus1DateString}T04:00:00.000Z`,
|
||||
end: `${plus1DateString}T04:45:00.000Z`,
|
||||
responses: {
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
attendeePhoneNumber: "+918888888888",
|
||||
},
|
||||
metadata: {},
|
||||
};
|
||||
|
||||
const mockRequest = {
|
||||
body: mockReqBody,
|
||||
method: "POST",
|
||||
headers: {},
|
||||
query: {},
|
||||
cookies: {},
|
||||
url: "/api/instant-meeting",
|
||||
} as NextApiRequest;
|
||||
|
||||
const result = await handler(mockRequest);
|
||||
|
||||
expect(result.message).toBe("Success");
|
||||
expect(result.bookingId).toBeDefined();
|
||||
expect(result.bookingUid).toBeDefined();
|
||||
expect(result.meetingTokenId).toBeDefined();
|
||||
expect(result.expires).toBeInstanceOf(Date);
|
||||
|
||||
const booking = await prismock.booking.findUnique({
|
||||
where: { id: result.bookingId },
|
||||
select: { status: true, attendees: true, references: true },
|
||||
});
|
||||
|
||||
expect(booking).toBeDefined();
|
||||
expect(booking?.status).toBe(BookingStatus.AWAITING_HOST);
|
||||
expect(booking?.attendees).toHaveLength(1);
|
||||
expect(booking?.attendees[0].email).toBe("test@example.com");
|
||||
expect(booking?.attendees[0].phoneNumber).toBe("+918888888888");
|
||||
expect(booking?.references).toHaveLength(1);
|
||||
expect(booking?.references[0].type).toBe("daily_video");
|
||||
});
|
||||
|
||||
it("should throw error for non-team event types", async () => {
|
||||
const handler = (await import("./handleInstantMeeting")).default;
|
||||
const organizer = getOrganizer({
|
||||
name: "Organizer",
|
||||
email: "organizer@example.com",
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
});
|
||||
|
||||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
||||
|
||||
await createBookingScenario(
|
||||
getScenarioData({
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slotInterval: 45,
|
||||
length: 45,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
apps: [TestData.apps["daily-video"], TestData.apps["google-calendar"]],
|
||||
})
|
||||
);
|
||||
|
||||
const mockReqBody = {
|
||||
eventTypeId: 1,
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
timeZone: "UTC",
|
||||
language: "en",
|
||||
start: `${plus1DateString}T04:00:00.000Z`,
|
||||
end: `${plus1DateString}T04:45:00.000Z`,
|
||||
responses: {
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
attendeePhoneNumber: "+918888888888",
|
||||
},
|
||||
metadata: {},
|
||||
};
|
||||
|
||||
const mockRequest = {
|
||||
body: mockReqBody,
|
||||
method: "POST",
|
||||
headers: {},
|
||||
query: {},
|
||||
cookies: {},
|
||||
url: "/api/instant-meeting",
|
||||
} as NextApiRequest;
|
||||
|
||||
await expect(handler(mockRequest)).rejects.toThrow(
|
||||
"Only Team Event Types are supported for Instant Meeting"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function getDate(param: { dateIncrement?: number } = {}) {
|
||||
const { dateIncrement = 0 } = param;
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() + dateIncrement);
|
||||
return {
|
||||
date,
|
||||
dateString: date.toISOString().split("T")[0],
|
||||
};
|
||||
}
|
||||
@@ -200,6 +200,7 @@ async function handler(req: NextApiRequest) {
|
||||
name: fullName,
|
||||
timeZone: attendeeTimezone,
|
||||
locale: attendeeLanguage ?? "en",
|
||||
phoneNumber: reqBody.attendeePhoneNumber ?? null,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -209,6 +210,7 @@ async function handler(req: NextApiRequest) {
|
||||
name: "",
|
||||
timeZone: attendeeTimezone,
|
||||
locale: "en",
|
||||
phoneNumber: null,
|
||||
});
|
||||
return guestArray;
|
||||
}, [] as typeof invitee);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { PermissionMapper } from "../domain/mappers/PermissionMapper";
|
||||
@@ -53,7 +54,7 @@ export const getResourcePermissions = async ({
|
||||
userRole,
|
||||
fallbackRoles = {},
|
||||
}: ResourcePermissionsOptions): Promise<ResourcePermissions> => {
|
||||
const featureRepo = new FeaturesRepository();
|
||||
const featureRepo = new FeaturesRepository(prisma);
|
||||
const permissionService = new PermissionCheckService();
|
||||
|
||||
const pbacEnabled = await featureRepo.checkIfTeamHasFeature(teamId, "pbac");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { MembershipRepository } from "@calcom/lib/server/repository/membership";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { PermissionMapper } from "../domain/mappers/PermissionMapper";
|
||||
@@ -24,7 +25,7 @@ export class PermissionCheckService {
|
||||
|
||||
constructor(
|
||||
private readonly repository: IPermissionRepository = new PermissionRepository(),
|
||||
featuresRepository: FeaturesRepository = new FeaturesRepository(),
|
||||
featuresRepository: FeaturesRepository = new FeaturesRepository(prisma),
|
||||
permissionService: PermissionService = new PermissionService()
|
||||
) {
|
||||
this.featuresRepository = featuresRepository;
|
||||
|
||||
@@ -150,8 +150,7 @@ export class RoleManagementFactory {
|
||||
private permissionCheckService: PermissionCheckService;
|
||||
|
||||
private constructor() {
|
||||
this.featuresRepository = new FeaturesRepository();
|
||||
this.roleService = new RoleService();
|
||||
(this.featuresRepository = new FeaturesRepository(prisma)), (this.roleService = new RoleService());
|
||||
this.permissionCheckService = new PermissionCheckService();
|
||||
}
|
||||
|
||||
|
||||
@@ -1153,7 +1153,7 @@ export default class EventManager {
|
||||
private async createAllCRMEvents(event: CalendarEvent) {
|
||||
const createdEvents = [];
|
||||
|
||||
const featureRepo = new FeaturesRepository();
|
||||
const featureRepo = new FeaturesRepository(prisma);
|
||||
const isTaskerEnabledForSalesforceCrm = event.team?.id
|
||||
? await featureRepo.checkIfTeamHasFeature(event.team.id, "salesforce-crm-tasker")
|
||||
: false;
|
||||
|
||||
@@ -52,6 +52,8 @@ const createBooking = (overrides = {}) => ({
|
||||
},
|
||||
destinationCalendar: null,
|
||||
user: null,
|
||||
iCalSequence: 0,
|
||||
iCalUID: "icaluid",
|
||||
...overrides,
|
||||
});
|
||||
|
||||
@@ -121,6 +123,10 @@ describe("buildCalEventFromBooking", () => {
|
||||
destinationCalendar: [],
|
||||
seatsPerTimeSlot: booking.eventType?.seatsPerTimeSlot,
|
||||
seatsShowAttendees: true,
|
||||
customReplyToEmail: undefined,
|
||||
hideOrganizerEmail: undefined,
|
||||
iCalSequence: 0,
|
||||
iCalUID: booking.iCalUID,
|
||||
});
|
||||
|
||||
expect(parseRecurringEvent).toHaveBeenCalledWith(booking.eventType?.recurringEvent);
|
||||
@@ -135,6 +141,8 @@ describe("buildCalEventFromBooking", () => {
|
||||
userPrimaryEmail: null,
|
||||
attendees: [],
|
||||
eventType: null,
|
||||
iCalUID: "icaluid",
|
||||
iCalSequence: 0,
|
||||
});
|
||||
|
||||
const organizer = createOrganizer({ name: null, locale: null });
|
||||
@@ -170,6 +178,10 @@ describe("buildCalEventFromBooking", () => {
|
||||
destinationCalendar: [],
|
||||
seatsPerTimeSlot: undefined,
|
||||
seatsShowAttendees: undefined,
|
||||
customReplyToEmail: undefined,
|
||||
hideOrganizerEmail: undefined,
|
||||
iCalSequence: 0,
|
||||
iCalUID: "icaluid",
|
||||
});
|
||||
|
||||
// @ts-expect-error - locale is set in mock
|
||||
@@ -192,6 +204,8 @@ describe("buildCalEventFromBooking", () => {
|
||||
credentialId: 1,
|
||||
},
|
||||
},
|
||||
iCalUID: "icaluid",
|
||||
iCalSequence: 0,
|
||||
});
|
||||
|
||||
const organizer = createOrganizer();
|
||||
|
||||
@@ -55,6 +55,8 @@ type Booking = {
|
||||
} | null;
|
||||
attendees: Attendee[];
|
||||
eventType: EventType | null;
|
||||
iCalUID: string | null;
|
||||
iCalSequence: number;
|
||||
};
|
||||
|
||||
export const buildCalEventFromBooking = async ({
|
||||
@@ -110,5 +112,7 @@ export const buildCalEventFromBooking = async ({
|
||||
seatsPerTimeSlot: booking.eventType?.seatsPerTimeSlot,
|
||||
seatsShowAttendees: booking.eventType?.seatsShowAttendees,
|
||||
customReplyToEmail: booking.eventType?.customReplyToEmail,
|
||||
iCalUID: booking.iCalUID ?? booking.uid,
|
||||
iCalSequence: booking.iCalSequence ?? 0,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -79,13 +79,19 @@ export const formatToLocalizedDate = (
|
||||
* given Date object and locale. Undefined values mean the defaults
|
||||
* associated with the browser's current locale will be used.
|
||||
*/
|
||||
export const formatToLocalizedTime = (
|
||||
date: Date | Dayjs,
|
||||
locale: string | undefined = undefined,
|
||||
timeStyle: Intl.DateTimeFormatOptions["timeStyle"] = "short",
|
||||
hour12: Intl.DateTimeFormatOptions["hour12"] = undefined,
|
||||
timeZone?: string
|
||||
) => formatLocalizedDateTime(date, { timeStyle, hour12, timeZone }, locale);
|
||||
export const formatToLocalizedTime = ({
|
||||
date,
|
||||
locale = undefined,
|
||||
timeStyle = "short",
|
||||
hour12 = undefined,
|
||||
timeZone,
|
||||
}: {
|
||||
date: Date | Dayjs;
|
||||
locale?: string | undefined;
|
||||
timeStyle?: Intl.DateTimeFormatOptions["timeStyle"];
|
||||
hour12?: Intl.DateTimeFormatOptions["hour12"];
|
||||
timeZone?: string;
|
||||
}) => formatLocalizedDateTime(date, { timeStyle, hour12, timeZone }, locale);
|
||||
|
||||
/**
|
||||
* Returns a translated timezone based on the given Date object and
|
||||
|
||||
@@ -6,8 +6,10 @@ import type { AvailableSlotsService } from "@calcom/trpc/server/routers/viewer/s
|
||||
|
||||
import { availableSlotsModule } from "../modules/available-slots";
|
||||
import { bookingRepositoryModule } from "../modules/booking";
|
||||
import { cacheModule } from "../modules/cache";
|
||||
import { checkBookingLimitsModule } from "../modules/check-booking-limits";
|
||||
import { eventTypeRepositoryModule } from "../modules/eventType";
|
||||
import { featuresRepositoryModule } from "../modules/features";
|
||||
import { oooRepositoryModule } from "../modules/ooo";
|
||||
import { routingFormResponseRepositoryModule } from "../modules/routingFormResponse";
|
||||
import { scheduleRepositoryModule } from "../modules/schedule";
|
||||
@@ -25,6 +27,8 @@ container.load(DI_TOKENS.USER_REPOSITORY_MODULE, userRepositoryModule);
|
||||
container.load(DI_TOKENS.BOOKING_REPOSITORY_MODULE, bookingRepositoryModule);
|
||||
container.load(DI_TOKENS.EVENT_TYPE_REPOSITORY_MODULE, eventTypeRepositoryModule);
|
||||
container.load(DI_TOKENS.ROUTING_FORM_RESPONSE_REPOSITORY_MODULE, routingFormResponseRepositoryModule);
|
||||
container.load(DI_TOKENS.FEATURES_REPOSITORY_MODULE, featuresRepositoryModule);
|
||||
container.load(DI_TOKENS.CACHE_SERVICE_MODULE, cacheModule);
|
||||
container.load(DI_TOKENS.CHECK_BOOKING_LIMITS_SERVICE_MODULE, checkBookingLimitsModule);
|
||||
container.load(DI_TOKENS.AVAILABLE_SLOTS_SERVICE_MODULE, availableSlotsModule);
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { createContainer } from "@evyweb/ioctopus";
|
||||
|
||||
import { DI_TOKENS } from "@calcom/lib/di/tokens";
|
||||
import { prismaModule } from "@calcom/prisma/prisma.module";
|
||||
|
||||
import type { CacheService } from "../../../features/calendar-cache/lib/getShouldServeCache";
|
||||
import { cacheModule } from "../modules/cache";
|
||||
import { featuresRepositoryModule } from "../modules/features";
|
||||
|
||||
const container = createContainer();
|
||||
container.load(DI_TOKENS.PRISMA_MODULE, prismaModule);
|
||||
container.load(DI_TOKENS.FEATURES_REPOSITORY_MODULE, featuresRepositoryModule);
|
||||
container.load(DI_TOKENS.CACHE_SERVICE_MODULE, cacheModule);
|
||||
|
||||
export function getCacheService() {
|
||||
return container.get<CacheService>(DI_TOKENS.CACHE_SERVICE);
|
||||
}
|
||||
@@ -15,5 +15,6 @@ availableSlotsModule.bind(DI_TOKENS.AVAILABLE_SLOTS_SERVICE).toClass(AvailableSl
|
||||
bookingRepo: DI_TOKENS.BOOKING_REPOSITORY,
|
||||
eventTypeRepo: DI_TOKENS.EVENT_TYPE_REPOSITORY,
|
||||
routingFormResponseRepo: DI_TOKENS.ROUTING_FORM_RESPONSE_REPOSITORY,
|
||||
cacheService: DI_TOKENS.CACHE_SERVICE,
|
||||
checkBookingLimitsService: DI_TOKENS.CHECK_BOOKING_LIMITS_SERVICE,
|
||||
} satisfies Record<keyof IAvailableSlotsService, symbol>);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createModule } from "@evyweb/ioctopus";
|
||||
|
||||
import type { ICacheService } from "@calcom/features/calendar-cache/lib/getShouldServeCache";
|
||||
import { CacheService } from "@calcom/features/calendar-cache/lib/getShouldServeCache";
|
||||
|
||||
import { DI_TOKENS } from "../tokens";
|
||||
|
||||
export const cacheModule = createModule();
|
||||
cacheModule.bind(DI_TOKENS.CACHE_SERVICE).toClass(CacheService, {
|
||||
featuresRepository: DI_TOKENS.FEATURES_REPOSITORY,
|
||||
} satisfies Record<keyof ICacheService, symbol>);
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createModule } from "@evyweb/ioctopus";
|
||||
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import { DI_TOKENS } from "@calcom/lib/di/tokens";
|
||||
|
||||
export const featuresRepositoryModule = createModule();
|
||||
featuresRepositoryModule
|
||||
.bind(DI_TOKENS.FEATURES_REPOSITORY)
|
||||
.toClass(FeaturesRepository, [DI_TOKENS.PRISMA_CLIENT]);
|
||||
@@ -22,6 +22,10 @@ export const DI_TOKENS = {
|
||||
AVAILABLE_SLOTS_SERVICE_MODULE: Symbol("AvailableSlotsModule"),
|
||||
INSIGHTS_ROUTING_SERVICE: Symbol("InsightsRoutingService"),
|
||||
INSIGHTS_ROUTING_SERVICE_MODULE: Symbol("InsightsRoutingServiceModule"),
|
||||
FEATURES_REPOSITORY: Symbol("FeaturesRepository"),
|
||||
FEATURES_REPOSITORY_MODULE: Symbol("FeaturesRepositoryModule"),
|
||||
CACHE_SERVICE: Symbol("CacheService"),
|
||||
CACHE_SERVICE_MODULE: Symbol("CacheServiceModule"),
|
||||
CHECK_BOOKING_LIMITS_SERVICE: Symbol("CheckBookingLimitsService"),
|
||||
CHECK_BOOKING_LIMITS_SERVICE_MODULE: Symbol("CheckBookingLimitsServiceModule"),
|
||||
CHECK_BOOKING_AND_DURATION_LIMITS_SERVICE: Symbol("CheckBookingAndDurationLimitsService"),
|
||||
|
||||
@@ -252,6 +252,10 @@ const _getBusyTimes = async (params: {
|
||||
console.log("videoBusyTimes", videoBusyTimes);
|
||||
busyTimes.push(...videoBusyTimes);
|
||||
*/
|
||||
} else {
|
||||
logger.warn(`No credentials found for user ${userId}`, {
|
||||
selectedCalendarIds: selectedCalendars.map((calendar) => calendar.id),
|
||||
});
|
||||
}
|
||||
logger.debug(
|
||||
"getBusyTimes:",
|
||||
|
||||
@@ -9,11 +9,12 @@ const logger = new Logger({
|
||||
prettyErrorStackTemplate: " • {{fileName}}\t{{method}}\n\t{{filePathWithLine}}", // default
|
||||
prettyErrorTemplate: "\n{{errorName}} {{errorMessage}}\nerror stack:\n{{errorStack}}", // default
|
||||
prettyLogTemplate: "{{hh}}:{{MM}}:{{ss}}:{{ms}} [{{logLevelName}}] ", // default with exclusion of `{{filePathWithLine}}`
|
||||
stylePrettyLogs: true,
|
||||
stylePrettyLogs: !IS_PRODUCTION,
|
||||
prettyLogStyles: {
|
||||
name: "yellow",
|
||||
dateIsoStr: "blue",
|
||||
},
|
||||
type: IS_PRODUCTION ? "json" : "pretty",
|
||||
});
|
||||
|
||||
export default logger;
|
||||
|
||||
@@ -34,6 +34,6 @@
|
||||
"@calcom/tsconfig": "*",
|
||||
"@calcom/types": "*",
|
||||
"@faker-js/faker": "^7.3.0",
|
||||
"typescript": "^5.7.2"
|
||||
"typescript": "^5.9.0-beta"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
/**
|
||||
* Checks if the restriction schedule feature is enabled for a team
|
||||
@@ -10,6 +11,6 @@ export async function isRestrictionScheduleEnabled(teamId?: number): Promise<boo
|
||||
return false; // Personal events don't have restriction schedules
|
||||
}
|
||||
|
||||
const featureRepo = new FeaturesRepository();
|
||||
const featureRepo = new FeaturesRepository(prisma);
|
||||
return await featureRepo.checkIfTeamHasFeature(teamId, "restriction-schedule");
|
||||
}
|
||||
|
||||
@@ -522,13 +522,15 @@ export class BookingRepository {
|
||||
|
||||
async updateLocationById({
|
||||
where: { id },
|
||||
data: { location, metadata, referencesToCreate },
|
||||
data: { location, metadata, referencesToCreate, responses, iCalSequence },
|
||||
}: {
|
||||
where: { id: number };
|
||||
data: {
|
||||
location: string;
|
||||
metadata: Record<string, unknown>;
|
||||
referencesToCreate: Prisma.BookingReferenceCreateInput[];
|
||||
responses?: Record<string, unknown>;
|
||||
iCalSequence?: number;
|
||||
};
|
||||
}) {
|
||||
await this.prismaClient.booking.update({
|
||||
@@ -538,6 +540,8 @@ export class BookingRepository {
|
||||
data: {
|
||||
location,
|
||||
metadata,
|
||||
...(responses && { responses }),
|
||||
...(iCalSequence !== undefined && { iCalSequence }),
|
||||
references: {
|
||||
create: referencesToCreate,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import type { Person } from "@calcom/types/Calendar";
|
||||
import type { JsonValue } from "@calcom/types/JsonObject";
|
||||
|
||||
function isObjectButNotArray(obj: unknown): obj is Record<string, unknown> {
|
||||
return typeof obj === "object" && !Array.isArray(obj);
|
||||
}
|
||||
|
||||
type DestinationCalendar = {
|
||||
id: number;
|
||||
integration: string;
|
||||
externalId: string;
|
||||
primaryEmail: string | null;
|
||||
userId: number | null;
|
||||
eventTypeId: number | null;
|
||||
credentialId: number | null;
|
||||
createdAt: Date | null;
|
||||
updatedAt: Date | null;
|
||||
delegationCredentialId: string | null;
|
||||
domainWideDelegationCredentialId: string | null;
|
||||
};
|
||||
|
||||
type Response = {
|
||||
label: string;
|
||||
value: string | boolean | string[] | { value: string; optionValue: string } | Record<string, string>;
|
||||
isHidden?: boolean | undefined;
|
||||
};
|
||||
|
||||
interface BaseWebhookPayload {
|
||||
bookingId: number;
|
||||
title: string;
|
||||
eventSlug: string | null;
|
||||
description: string | null;
|
||||
customInputs: JsonValue | null;
|
||||
responses: Record<string, Response>;
|
||||
userFieldsResponses: Record<string, Response>;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
organizer: Person;
|
||||
attendees: Person[];
|
||||
uid: string;
|
||||
location: string | null;
|
||||
destinationCalendar: DestinationCalendar | null;
|
||||
cancellationReason: string | null;
|
||||
iCalUID: string | null;
|
||||
smsReminderNumber?: string;
|
||||
cancelledBy: string | null;
|
||||
}
|
||||
|
||||
interface CancelledEventPayload extends BaseWebhookPayload {
|
||||
cancelledBy: string;
|
||||
cancellationReason: string;
|
||||
}
|
||||
|
||||
export class BookingWebhookFactory {
|
||||
private getType(params: BaseWebhookPayload) {
|
||||
return params.eventSlug || params.title || "";
|
||||
}
|
||||
|
||||
private getTitle(params: BaseWebhookPayload) {
|
||||
return params.title || "";
|
||||
}
|
||||
|
||||
private getDestinationCalendar(params: BaseWebhookPayload) {
|
||||
return params.destinationCalendar ? [params.destinationCalendar] : [];
|
||||
}
|
||||
|
||||
private getCustomInputs(params: BaseWebhookPayload) {
|
||||
return isObjectButNotArray(params.customInputs) ? params.customInputs : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates base webhook payload with common fields
|
||||
*/
|
||||
private createBasePayload(params: BaseWebhookPayload) {
|
||||
const {
|
||||
bookingId,
|
||||
title,
|
||||
eventSlug,
|
||||
description,
|
||||
customInputs,
|
||||
startTime,
|
||||
endTime,
|
||||
uid,
|
||||
location,
|
||||
organizer,
|
||||
attendees,
|
||||
responses,
|
||||
userFieldsResponses,
|
||||
destinationCalendar,
|
||||
smsReminderNumber,
|
||||
iCalUID,
|
||||
} = params;
|
||||
|
||||
const basePayload = {
|
||||
bookingId,
|
||||
type: this.getType(params),
|
||||
title: this.getTitle(params),
|
||||
description,
|
||||
customInputs: this.getCustomInputs(params),
|
||||
responses,
|
||||
userFieldsResponses,
|
||||
startTime,
|
||||
endTime,
|
||||
organizer,
|
||||
attendees,
|
||||
uid,
|
||||
location,
|
||||
destinationCalendar: this.getDestinationCalendar(params),
|
||||
iCalUID,
|
||||
smsReminderNumber,
|
||||
};
|
||||
|
||||
return basePayload;
|
||||
}
|
||||
|
||||
public createCancelledEventPayload(params: CancelledEventPayload) {
|
||||
const basePayload = this.createBasePayload({
|
||||
...params,
|
||||
});
|
||||
|
||||
return {
|
||||
...basePayload,
|
||||
cancelledBy: params.cancelledBy,
|
||||
cancellationReason: params.cancellationReason,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import type { Person } from "@calcom/types/Calendar";
|
||||
|
||||
import { BookingWebhookFactory } from "../BookingWebhookFactory";
|
||||
|
||||
const createTestOrganizer = (overrides?: Partial<Person>): Person => ({
|
||||
email: "organizer@example.com",
|
||||
name: "Test Organizer",
|
||||
timeZone: "UTC",
|
||||
language: { locale: "en", translate: (() => "") as any },
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const createTestAttendee = (overrides?: Partial<Person>): Person => ({
|
||||
email: "attendee@example.com",
|
||||
name: "Test Attendee",
|
||||
timeZone: "UTC",
|
||||
language: { locale: "en", translate: (() => "") as any },
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const createTestBooking = (overrides?: Record<string, any>) => ({
|
||||
id: 12345,
|
||||
uid: "test-uid-123",
|
||||
title: "Test Event",
|
||||
description: "Test Description",
|
||||
startTime: new Date("2025-01-24T10:00:00Z"),
|
||||
endTime: new Date("2025-01-24T11:00:00Z"),
|
||||
location: "https://meet.example.com",
|
||||
customInputs: { field1: "value1" },
|
||||
responses: { name: { value: "Test User" } },
|
||||
userFieldsResponses: { company: { value: "Test Corp" } },
|
||||
smsReminderNumber: "+1234567890",
|
||||
iCalUID: "ical-uid-123",
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe("BookingWebhookFactory", () => {
|
||||
describe("createCancelledEventPayload", () => {
|
||||
it("should create a basic cancelled event payload with required fields", () => {
|
||||
const factory = new BookingWebhookFactory();
|
||||
const booking = createTestBooking();
|
||||
const organizer = createTestOrganizer();
|
||||
const attendees = [createTestAttendee()];
|
||||
|
||||
const payload = factory.createCancelledEventPayload({
|
||||
bookingId: booking.id,
|
||||
uid: booking.uid,
|
||||
title: booking.title,
|
||||
eventSlug: "test-event",
|
||||
description: booking.description,
|
||||
startTime: booking.startTime.toISOString(),
|
||||
endTime: booking.endTime.toISOString(),
|
||||
location: booking.location,
|
||||
customInputs: booking.customInputs,
|
||||
responses: booking.responses,
|
||||
userFieldsResponses: booking.userFieldsResponses,
|
||||
smsReminderNumber: booking.smsReminderNumber,
|
||||
iCalUID: booking.iCalUID,
|
||||
organizer,
|
||||
attendees,
|
||||
destinationCalendar: null,
|
||||
cancelledBy: "test@example.com",
|
||||
cancellationReason: "User requested",
|
||||
});
|
||||
|
||||
expect(payload.bookingId).toBe(12345);
|
||||
expect(payload.uid).toBe("test-uid-123");
|
||||
expect(payload.title).toBe("Test Event");
|
||||
expect(payload.description).toBe("Test Description");
|
||||
expect(payload.organizer).toEqual(organizer);
|
||||
expect(payload.attendees).toEqual(attendees);
|
||||
expect(payload.type).toBe("test-event");
|
||||
expect(payload.startTime).toBe(booking.startTime.toISOString());
|
||||
expect(payload.endTime).toBe(booking.endTime.toISOString());
|
||||
expect(payload.location).toBe("https://meet.example.com");
|
||||
expect(payload.customInputs).toEqual({ field1: "value1" });
|
||||
expect(payload.responses).toEqual({ name: { value: "Test User" } });
|
||||
expect(payload.userFieldsResponses).toEqual({ company: { value: "Test Corp" } });
|
||||
expect(payload.smsReminderNumber).toBe("+1234567890");
|
||||
expect(payload.iCalUID).toBe("ical-uid-123");
|
||||
expect(payload.destinationCalendar).toEqual([]);
|
||||
expect(payload.cancellationReason).toBe("User requested");
|
||||
expect(payload.cancelledBy).toBe("test@example.com");
|
||||
|
||||
// Verify no extra properties exist
|
||||
const expectedKeys = [
|
||||
"bookingId",
|
||||
"type",
|
||||
"title",
|
||||
"description",
|
||||
"customInputs",
|
||||
"responses",
|
||||
"userFieldsResponses",
|
||||
"startTime",
|
||||
"endTime",
|
||||
"organizer",
|
||||
"attendees",
|
||||
"uid",
|
||||
"location",
|
||||
"destinationCalendar",
|
||||
"iCalUID",
|
||||
"smsReminderNumber",
|
||||
"cancellationReason",
|
||||
"cancelledBy",
|
||||
];
|
||||
const actualKeys = Object.keys(payload).sort();
|
||||
expect(actualKeys).toEqual(expectedKeys.sort());
|
||||
});
|
||||
|
||||
it("should handle responses and userFieldsResponses", () => {
|
||||
const factory = new BookingWebhookFactory();
|
||||
const booking = createTestBooking();
|
||||
const responses = { field1: "response1", field2: "response2" };
|
||||
const userFieldsResponses = { company: "Test Corp", department: "Engineering" };
|
||||
|
||||
const payload = factory.createCancelledEventPayload({
|
||||
bookingId: booking.id,
|
||||
uid: booking.uid,
|
||||
title: booking.title,
|
||||
eventSlug: null,
|
||||
description: null,
|
||||
startTime: booking.startTime.toISOString(),
|
||||
endTime: booking.endTime.toISOString(),
|
||||
location: null,
|
||||
customInputs: null,
|
||||
responses,
|
||||
userFieldsResponses,
|
||||
smsReminderNumber: null,
|
||||
iCalUID: null,
|
||||
organizer: createTestOrganizer(),
|
||||
attendees: [createTestAttendee()],
|
||||
destinationCalendar: null,
|
||||
cancelledBy: "user@example.com",
|
||||
cancellationReason: "Test",
|
||||
});
|
||||
|
||||
expect(payload.responses).toEqual(responses);
|
||||
expect(payload.userFieldsResponses).toEqual(userFieldsResponses);
|
||||
});
|
||||
|
||||
it("should handle destination calendar as single object", () => {
|
||||
const factory = new BookingWebhookFactory();
|
||||
const booking = createTestBooking();
|
||||
const destinationCalendar = {
|
||||
id: 1,
|
||||
integration: "google",
|
||||
externalId: "cal123",
|
||||
primaryEmail: null,
|
||||
userId: null,
|
||||
eventTypeId: null,
|
||||
credentialId: null,
|
||||
createdAt: null,
|
||||
updatedAt: null,
|
||||
delegationCredentialId: null,
|
||||
domainWideDelegationCredentialId: null,
|
||||
};
|
||||
|
||||
const payload = factory.createCancelledEventPayload({
|
||||
bookingId: booking.id,
|
||||
uid: booking.uid,
|
||||
title: booking.title,
|
||||
eventSlug: null,
|
||||
description: null,
|
||||
startTime: booking.startTime.toISOString(),
|
||||
endTime: booking.endTime.toISOString(),
|
||||
location: null,
|
||||
customInputs: null,
|
||||
responses: {},
|
||||
userFieldsResponses: {},
|
||||
smsReminderNumber: null,
|
||||
iCalUID: null,
|
||||
organizer: createTestOrganizer(),
|
||||
attendees: [],
|
||||
destinationCalendar,
|
||||
cancelledBy: "user@example.com",
|
||||
cancellationReason: "Test",
|
||||
});
|
||||
|
||||
expect(payload.destinationCalendar).toEqual([destinationCalendar]);
|
||||
});
|
||||
|
||||
it("should handle null destination calendar", () => {
|
||||
const factory = new BookingWebhookFactory();
|
||||
const booking = createTestBooking();
|
||||
|
||||
const payload = factory.createCancelledEventPayload({
|
||||
bookingId: booking.id,
|
||||
uid: booking.uid,
|
||||
title: booking.title,
|
||||
eventSlug: null,
|
||||
description: null,
|
||||
startTime: booking.startTime.toISOString(),
|
||||
endTime: booking.endTime.toISOString(),
|
||||
location: null,
|
||||
customInputs: null,
|
||||
responses: {},
|
||||
userFieldsResponses: {},
|
||||
smsReminderNumber: null,
|
||||
iCalUID: null,
|
||||
organizer: createTestOrganizer(),
|
||||
attendees: [],
|
||||
destinationCalendar: null,
|
||||
cancelledBy: "user@example.com",
|
||||
cancellationReason: "Test",
|
||||
});
|
||||
|
||||
expect(payload.destinationCalendar).toEqual([]);
|
||||
});
|
||||
|
||||
it("should handle sms reminder number and iCal UID", () => {
|
||||
const factory = new BookingWebhookFactory();
|
||||
const booking = createTestBooking();
|
||||
|
||||
const payload = factory.createCancelledEventPayload({
|
||||
bookingId: booking.id,
|
||||
uid: booking.uid,
|
||||
title: booking.title,
|
||||
eventSlug: null,
|
||||
description: null,
|
||||
startTime: booking.startTime.toISOString(),
|
||||
endTime: booking.endTime.toISOString(),
|
||||
location: null,
|
||||
customInputs: null,
|
||||
responses: {},
|
||||
userFieldsResponses: {},
|
||||
smsReminderNumber: "+1234567890",
|
||||
iCalUID: "ical-uid-123",
|
||||
organizer: createTestOrganizer(),
|
||||
attendees: [],
|
||||
destinationCalendar: null,
|
||||
cancelledBy: "user@example.com",
|
||||
cancellationReason: "Test",
|
||||
});
|
||||
|
||||
expect(payload.smsReminderNumber).toBe("+1234567890");
|
||||
expect(payload.iCalUID).toBe("ical-uid-123");
|
||||
});
|
||||
|
||||
it("should derive type from eventSlug when available, otherwise from title", () => {
|
||||
const factory = new BookingWebhookFactory();
|
||||
const booking = createTestBooking();
|
||||
|
||||
// Test with eventSlug provided
|
||||
const payloadWithSlug = factory.createCancelledEventPayload({
|
||||
bookingId: booking.id,
|
||||
uid: booking.uid,
|
||||
title: "Meeting Title",
|
||||
eventSlug: "team-standup",
|
||||
description: null,
|
||||
startTime: booking.startTime.toISOString(),
|
||||
endTime: booking.endTime.toISOString(),
|
||||
location: null,
|
||||
customInputs: null,
|
||||
responses: {},
|
||||
userFieldsResponses: {},
|
||||
smsReminderNumber: null,
|
||||
iCalUID: null,
|
||||
organizer: createTestOrganizer(),
|
||||
attendees: [],
|
||||
destinationCalendar: null,
|
||||
cancelledBy: "user@example.com",
|
||||
cancellationReason: "Test",
|
||||
});
|
||||
|
||||
expect(payloadWithSlug.type).toBe("team-standup");
|
||||
|
||||
// Test with eventSlug null, should use title
|
||||
const payloadWithoutSlug = factory.createCancelledEventPayload({
|
||||
bookingId: booking.id,
|
||||
uid: booking.uid,
|
||||
title: "Meeting Title",
|
||||
eventSlug: null,
|
||||
description: null,
|
||||
startTime: booking.startTime.toISOString(),
|
||||
endTime: booking.endTime.toISOString(),
|
||||
location: null,
|
||||
customInputs: null,
|
||||
responses: {},
|
||||
userFieldsResponses: {},
|
||||
smsReminderNumber: null,
|
||||
iCalUID: null,
|
||||
organizer: createTestOrganizer(),
|
||||
attendees: [],
|
||||
destinationCalendar: null,
|
||||
cancelledBy: "user@example.com",
|
||||
cancellationReason: "Test",
|
||||
});
|
||||
|
||||
expect(payloadWithoutSlug.type).toBe("Meeting Title");
|
||||
|
||||
// Test with both eventSlug and title empty
|
||||
const payloadEmpty = factory.createCancelledEventPayload({
|
||||
bookingId: booking.id,
|
||||
uid: booking.uid,
|
||||
title: "",
|
||||
eventSlug: "",
|
||||
description: null,
|
||||
startTime: booking.startTime.toISOString(),
|
||||
endTime: booking.endTime.toISOString(),
|
||||
location: null,
|
||||
customInputs: null,
|
||||
responses: {},
|
||||
userFieldsResponses: {},
|
||||
smsReminderNumber: null,
|
||||
iCalUID: null,
|
||||
organizer: createTestOrganizer(),
|
||||
attendees: [],
|
||||
destinationCalendar: null,
|
||||
cancelledBy: "user@example.com",
|
||||
cancellationReason: "Test",
|
||||
});
|
||||
|
||||
expect(payloadEmpty.type).toBe("");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,12 @@
|
||||
## 1.0.114
|
||||
## 1.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#22731](https://github.com/calcom/cal.com/pull/22731) [`ef66187`](https://github.com/calcom/cal.com/commit/ef6618743d5c1367fadf7b98cf15f7ba829535e3) Thanks [@devin-ai-integration](https://github.com/apps/devin-ai-integration)! - Added new startTime prop to the Booker component that allows users to decide the first available date when the booker loads
|
||||
|
||||
- [#22815](https://github.com/calcom/cal.com/pull/22815) [`dc967cf`](https://github.com/calcom/cal.com/commit/dc967cffbecdf573aa48777ca7a245a6e633356c) Thanks [@Ryukemeister](https://github.com/Ryukemeister)! - This change fixes an issue in the CalendarSettings atom where the redirect urls were getting only one search param
|
||||
|
||||
- [#22701](https://github.com/calcom/cal.com/pull/22701) [`babd514`](https://github.com/calcom/cal.com/commit/babd514c64d0a4b8e05a46619808f201d3e8b0a5) Thanks [@Ryukemeister](https://github.com/Ryukemeister)! - This change fixes date overrides breaking for availability settings atom
|
||||
|
||||
## 1.1.0
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ export const BookerPlatformWrapper = (
|
||||
isBookingDryRun,
|
||||
handleSlotReservation,
|
||||
onTimeslotsLoaded,
|
||||
startTime: customStartTime,
|
||||
} = props;
|
||||
const layout = BookerLayouts[view];
|
||||
|
||||
@@ -207,7 +208,7 @@ export const BookerPlatformWrapper = (
|
||||
dayjs(date).month() !== dayjs(date).add(bookerLayout.extraDays, "day").month()) ||
|
||||
(bookerLayout.layout === BookerLayouts.COLUMN_VIEW &&
|
||||
dayjs(date).month() !== dayjs(date).add(bookerLayout.columnViewExtraDays.current, "day").month()) ||
|
||||
(bookerLayout.layout === BookerLayouts.MONTH_VIEW &&
|
||||
((bookerLayout.layout === BookerLayouts.MONTH_VIEW || bookerLayout.layout === "mobile") &&
|
||||
(!dayjs(date).isValid() || dayjs().isSame(dayjs(month), "month")) &&
|
||||
dayjs().isAfter(dayjs(month).startOf("month").add(2, "week")));
|
||||
|
||||
@@ -220,7 +221,7 @@ export const BookerPlatformWrapper = (
|
||||
: undefined;
|
||||
const { timezone } = useTimePreferences();
|
||||
|
||||
const [startTime, endTime] = useTimesForSchedule({
|
||||
const [calculatedStartTime, calculatedEndTime] = useTimesForSchedule({
|
||||
month,
|
||||
monthCount,
|
||||
dayCount,
|
||||
@@ -228,6 +229,12 @@ export const BookerPlatformWrapper = (
|
||||
selectedDate,
|
||||
});
|
||||
|
||||
const startTime =
|
||||
customStartTime && dayjs(customStartTime).isAfter(dayjs(calculatedStartTime))
|
||||
? dayjs(customStartTime).toISOString()
|
||||
: calculatedStartTime;
|
||||
const endTime = calculatedEndTime;
|
||||
|
||||
const [routingParams, setRoutingParams] = useState<{
|
||||
routedTeamMemberIds?: number[];
|
||||
_shouldServeCache?: boolean;
|
||||
|
||||
@@ -128,7 +128,7 @@ export const BookerWebWrapper = (props: BookerWebWrapperAtomProps) => {
|
||||
dayjs(date).month() !== dayjs(date).add(bookerLayout.extraDays, "day").month()) ||
|
||||
(bookerLayout.layout === BookerLayouts.COLUMN_VIEW &&
|
||||
dayjs(date).month() !== dayjs(date).add(bookerLayout.columnViewExtraDays.current, "day").month()) ||
|
||||
(bookerLayout.layout === BookerLayouts.MONTH_VIEW &&
|
||||
((bookerLayout.layout === BookerLayouts.MONTH_VIEW || bookerLayout.layout === "mobile") &&
|
||||
(!dayjs(date).isValid() || dayjs().isSame(dayjs(month), "month")) &&
|
||||
dayjs().isAfter(dayjs(month).startOf("month").add(2, "week")));
|
||||
|
||||
|
||||
@@ -83,6 +83,7 @@ export type BookerPlatformWrapperAtomProps = Omit<
|
||||
isBookingDryRun?: boolean;
|
||||
eventMetaChildren?: React.ReactNode;
|
||||
onTimeslotsLoaded?: (slots: Record<string, Slot[]>) => void;
|
||||
startTime?: string | Date;
|
||||
};
|
||||
|
||||
type VIEW_TYPE = keyof typeof BookerLayouts;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"type": "module",
|
||||
"description": "Customizable UI components to integrate scheduling into your product.",
|
||||
"authors": "Cal.com, Inc.",
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.2",
|
||||
"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",
|
||||
@@ -29,7 +29,7 @@
|
||||
"postcss-prefixwrap": "1.46.0",
|
||||
"rollup-plugin-node-builtins": "^2.1.2",
|
||||
"ts-jest": "^29.1.2",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript": "^5.9.0-beta",
|
||||
"vite": "^5.0.10",
|
||||
"vite-plugin-dts": "^3.7.3",
|
||||
"vite-plugin-inspect": "^0.8.4",
|
||||
|
||||
@@ -26,6 +26,6 @@
|
||||
"eslint-config-next": "14.0.4",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.3.0",
|
||||
"typescript": "^5.7.2"
|
||||
"typescript": "^5.9.0-beta"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,3 +119,5 @@ export type { TFindTeamMembersMatchingAttributeLogicInputSchema } from "@calcom/
|
||||
export { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
|
||||
|
||||
export { verifyPhoneNumber, sendVerificationCode };
|
||||
|
||||
export { CacheService } from "@calcom/features/calendar-cache/lib/getShouldServeCache";
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.3.1",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript": "^5.9.0-beta",
|
||||
"vite": "^5.0.12",
|
||||
"vite-plugin-dts": "^3.7.2",
|
||||
"vite-plugin-environment": "^1.1.3"
|
||||
|
||||
@@ -6,3 +6,4 @@ export { RoutingFormResponseRepository as PrismaRoutingFormResponseRepository }
|
||||
export { PrismaSelectedSlotRepository } from "@calcom/lib/server/repository/PrismaSelectedSlotRepository";
|
||||
export { TeamRepository as PrismaTeamRepository } from "@calcom/lib/server/repository/team";
|
||||
export { UserRepository as PrismaUserRepository } from "@calcom/lib/server/repository/user";
|
||||
export { FeaturesRepository as PrismaFeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
|
||||
Binary file not shown.
@@ -22,6 +22,6 @@
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7.2"
|
||||
"typescript": "^5.9.0-beta"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { getUsersCredentialsIncludeServiceAccountKey } from "@calcom/lib/server/getUsersCredentials";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import { BookingRepository } from "@calcom/lib/server/repository/booking";
|
||||
import { CredentialRepository } from "@calcom/lib/server/repository/credential";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
@@ -97,19 +98,16 @@ async function updateBookingLocationInDb({
|
||||
};
|
||||
});
|
||||
|
||||
await prisma.booking.update({
|
||||
where: {
|
||||
id: booking.id,
|
||||
},
|
||||
const bookingRepository = new BookingRepository(prisma);
|
||||
await bookingRepository.updateLocationById({
|
||||
where: { id: booking.id },
|
||||
data: {
|
||||
location: evt.location,
|
||||
metadata: {
|
||||
...(typeof booking.metadata === "object" && booking.metadata),
|
||||
...bookingMetadataUpdate,
|
||||
},
|
||||
references: {
|
||||
create: referencesToCreate,
|
||||
},
|
||||
referencesToCreate,
|
||||
responses: {
|
||||
...(typeof booking.responses === "object" && booking.responses),
|
||||
location: {
|
||||
@@ -117,6 +115,7 @@ async function updateBookingLocationInDb({
|
||||
optionValue: "",
|
||||
},
|
||||
},
|
||||
iCalSequence: (evt.iCalSequence || 0) + 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,18 +14,18 @@ import { getDelegationCredentialOrRegularCredential } from "@calcom/lib/delegati
|
||||
import { getBookerBaseUrl } from "@calcom/lib/getBookerUrl/server";
|
||||
import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId";
|
||||
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
|
||||
import { isPrismaObjOrUndefined } from "@calcom/lib/isPrismaObj";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { getUsersCredentialsIncludeServiceAccountKey } from "@calcom/lib/server/getUsersCredentials";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import { WorkflowRepository } from "@calcom/lib/server/repository/workflow";
|
||||
import { BookingWebhookFactory } from "@calcom/lib/server/service/BookingWebhookFactory";
|
||||
import { deleteMeeting } from "@calcom/lib/videoClient";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import type { EventTypeMetadata } from "@calcom/prisma/zod-utils";
|
||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||
import type { Person } from "@calcom/types/Calendar";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
@@ -109,7 +109,7 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule
|
||||
|
||||
if (bookingBelongsToTeam && bookingToReschedule.eventType?.teamId) {
|
||||
const userTeamIds = userTeams.teams.map((item) => item.teamId);
|
||||
if (userTeamIds.indexOf(bookingToReschedule?.eventType?.teamId) === -1) {
|
||||
if (userTeamIds.indexOf(bookingToReschedule.eventType?.teamId) === -1) {
|
||||
throw new TRPCError({ code: "FORBIDDEN", message: "User isn't a member on the team" });
|
||||
}
|
||||
log.debug(
|
||||
@@ -182,7 +182,7 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule
|
||||
const [userAsPeopleType] = usersToPeopleType([user], userTranslation);
|
||||
const organizer = {
|
||||
...userAsPeopleType,
|
||||
email: bookingToReschedule?.userPrimaryEmail ?? userAsPeopleType.email,
|
||||
email: bookingToReschedule.userPrimaryEmail ?? userAsPeopleType.email,
|
||||
};
|
||||
|
||||
const builder = new CalendarEventBuilder();
|
||||
@@ -276,31 +276,38 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule
|
||||
eventType?.metadata as EventTypeMetadata
|
||||
);
|
||||
|
||||
const evt: CalendarEvent = {
|
||||
title: bookingToReschedule?.title,
|
||||
type: event && event.slug ? event.slug : bookingToReschedule.title,
|
||||
description: bookingToReschedule?.description || "",
|
||||
customInputs: isPrismaObjOrUndefined(bookingToReschedule.customInputs),
|
||||
...getCalEventResponses({
|
||||
booking: bookingToReschedule,
|
||||
bookingFields: bookingToReschedule.eventType?.bookingFields ?? null,
|
||||
}),
|
||||
startTime: bookingToReschedule?.startTime ? dayjs(bookingToReschedule.startTime).format() : "",
|
||||
endTime: bookingToReschedule?.endTime ? dayjs(bookingToReschedule.endTime).format() : "",
|
||||
const calEventResponses = getCalEventResponses({
|
||||
booking: bookingToReschedule,
|
||||
bookingFields: bookingToReschedule.eventType?.bookingFields ?? null,
|
||||
});
|
||||
|
||||
const webhookFactory = new BookingWebhookFactory();
|
||||
const payload = webhookFactory.createCancelledEventPayload({
|
||||
bookingId: bookingToReschedule.id,
|
||||
title: bookingToReschedule.title,
|
||||
eventSlug: event.slug ?? null,
|
||||
description: bookingToReschedule.description,
|
||||
customInputs: bookingToReschedule.customInputs,
|
||||
responses: calEventResponses.responses,
|
||||
userFieldsResponses: calEventResponses.userFieldsResponses,
|
||||
startTime: bookingToReschedule.startTime ? dayjs(bookingToReschedule.startTime).format() : "",
|
||||
endTime: bookingToReschedule.endTime ? dayjs(bookingToReschedule.endTime).format() : "",
|
||||
organizer,
|
||||
attendees: usersToPeopleType(
|
||||
// username field doesn't exists on attendee but could be in the future
|
||||
bookingToReschedule.attendees as unknown as PersonAttendeeCommonFields[],
|
||||
tAttendees
|
||||
),
|
||||
uid: bookingToReschedule?.uid,
|
||||
location: bookingToReschedule?.location,
|
||||
destinationCalendar: bookingToReschedule?.destinationCalendar
|
||||
? [bookingToReschedule?.destinationCalendar]
|
||||
: [],
|
||||
uid: bookingToReschedule.uid,
|
||||
location: bookingToReschedule.location,
|
||||
destinationCalendar: bookingToReschedule.destinationCalendar,
|
||||
cancellationReason: `Please reschedule. ${cancellationReason}`, // TODO::Add i18-next for this
|
||||
iCalUID: bookingToReschedule?.iCalUID,
|
||||
};
|
||||
iCalUID: bookingToReschedule.iCalUID,
|
||||
...(bookingToReschedule.smsReminderNumber && {
|
||||
smsReminderNumber: bookingToReschedule.smsReminderNumber,
|
||||
}),
|
||||
cancelledBy: user.email,
|
||||
});
|
||||
|
||||
// Send webhook
|
||||
const eventTrigger: WebhookTriggerEvents = "BOOKING_CANCELLED";
|
||||
@@ -308,7 +315,7 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule
|
||||
const teamId = await getTeamIdFromEventType({
|
||||
eventType: {
|
||||
team: { id: bookingToReschedule.eventType?.teamId ?? null },
|
||||
parentId: bookingToReschedule?.eventType?.parentId ?? null,
|
||||
parentId: bookingToReschedule.eventType?.parentId ?? null,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -327,13 +334,9 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule
|
||||
const webhooks = await getWebhooks(subscriberOptions);
|
||||
|
||||
const promises = webhooks.map((webhook) =>
|
||||
sendPayload(webhook.secret, eventTrigger, new Date().toISOString(), webhook, {
|
||||
...evt,
|
||||
smsReminderNumber: bookingToReschedule.smsReminderNumber || undefined,
|
||||
cancelledBy: user.email,
|
||||
}).catch((e) => {
|
||||
sendPayload(webhook.secret, eventTrigger, new Date().toISOString(), webhook, payload).catch((e) => {
|
||||
log.error(
|
||||
`Error executing webhook for event: ${eventTrigger}, URL: ${webhook.subscriberUrl}, bookingId: ${evt.bookingId}, bookingUid: ${evt.uid}`,
|
||||
`Error executing webhook for event: ${eventTrigger}, URL: ${webhook.subscriberUrl}, bookingId: ${payload.bookingId}, bookingUid: ${payload.uid}`,
|
||||
safeStringify(e)
|
||||
);
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
@@ -22,7 +23,7 @@ const checkDelegationCredentialFeature = async ({
|
||||
}) => {
|
||||
const user = ctx.user;
|
||||
const t = await getTranslation(user.locale ?? "en", "common");
|
||||
const featureRepo = new FeaturesRepository();
|
||||
const featureRepo = new FeaturesRepository(prisma);
|
||||
|
||||
if (!user.organizationId) {
|
||||
throw new TRPCError({
|
||||
|
||||
@@ -40,7 +40,7 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions)
|
||||
const billingService = new StripeBillingService();
|
||||
const userMetadata = handleUserMetadata({ ctx, input });
|
||||
const locale = input.locale || user.locale;
|
||||
const featuresRepository = new FeaturesRepository();
|
||||
const featuresRepository = new FeaturesRepository(prisma);
|
||||
const emailVerification = await featuresRepository.checkIfFeatureIsEnabledGlobally("email-verification");
|
||||
|
||||
const { travelSchedules, ...rest } = input;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import { OrganizationRepository } from "@calcom/lib/server/repository/organization";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
@@ -26,7 +27,7 @@ export const listHandler = async ({ ctx }: ListHandlerInput) => {
|
||||
return currentOrg;
|
||||
}
|
||||
|
||||
const featureRepo = new FeaturesRepository();
|
||||
const featureRepo = new FeaturesRepository(prisma);
|
||||
const hasDelegationCredential = await featureRepo.checkIfTeamHasFeature(
|
||||
organizationId,
|
||||
"delegation-credential"
|
||||
|
||||
@@ -39,7 +39,7 @@ export const listMembersHandler = async ({ ctx, input }: GetOptions) => {
|
||||
const expand = input.expand;
|
||||
const filters = input.filters || [];
|
||||
|
||||
const featuresRepository = new FeaturesRepository();
|
||||
const featuresRepository = new FeaturesRepository(prisma);
|
||||
const pbacFeatureEnabled = await featuresRepository.checkIfTeamHasFeature(organizationId, "pbac");
|
||||
|
||||
const allAttributeOptions = await prisma.attributeOption.findMany({
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Resource, CrudAction, CustomAction } from "@calcom/features/pbac/domain
|
||||
import type { PermissionString } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
import { RoleService } from "@calcom/features/pbac/services/role.service";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { RoleType, MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import authedProcedure from "../../../procedures/authedProcedure";
|
||||
@@ -193,7 +194,7 @@ export const permissionsRouter = router({
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
|
||||
const featureRepo = new FeaturesRepository();
|
||||
const featureRepo = new FeaturesRepository(prisma);
|
||||
const teamHasPBACFeature = await featureRepo.checkIfTeamHasFeature(input.teamId, "pbac");
|
||||
|
||||
if (!teamHasPBACFeature) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user